Skip to content

Commit cd0895e

Browse files
authored
Add rating per user and other fixes. (#3899)
1 parent 15d66ca commit cd0895e

File tree

84 files changed

+5680
-62
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

84 files changed

+5680
-62
lines changed

app/Actions/Album/Create.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,8 @@ private function setStatistics(Album $album): void
150150
'download_count' => 0,
151151
'favourite_count' => 0,
152152
'shared_count' => 0,
153+
'rating_sum' => 0,
154+
'rating_count' => 0,
153155
]);
154156
}
155157
}

app/Actions/Album/PositionData.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ public function get(AbstractAlbum $album, bool $include_sub_albums = false): Pos
3636
},
3737
'palette',
3838
'tags',
39+
'rating',
3940
])
4041
->whereNotNull('latitude')
4142
->whereNotNull('longitude');

app/Actions/Albums/Flow.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ private function getQuery(Album|null $base, bool $with_relations): AlbumBuilder
113113

114114
$base_query = Album::query();
115115
if ($with_relations) {
116-
$base_query->with(['cover', 'cover.size_variants', 'statistics', 'photos', 'photos.statistics', 'photos.size_variants', 'photos.palette', 'photos.tags']);
116+
$base_query->with(['cover', 'cover.size_variants', 'statistics', 'photos', 'photos.statistics', 'photos.size_variants', 'photos.palette', 'photos.tags', 'photos.rating']);
117117
}
118118

119119
// Only join what we need for ordering.

app/Actions/Albums/PositionData.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ public function do(): PositionDataResource
4747
},
4848
'palette',
4949
'tags',
50+
'rating',
5051
])
5152
->whereNotNull('latitude')
5253
->whereNotNull('longitude'),

app/Actions/Photo/Pipes/Shared/SaveStatistics.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ public function handle(PhotoDTO $state, \Closure $next): PhotoDTO
2626
'download_count' => 0,
2727
'favourite_count' => 0,
2828
'shared_count' => 0,
29+
'rating_sum' => 0,
30+
'rating_count' => 0,
2931
]);
3032

3133
$state->getPhoto()->setRelation('statistics', $stats);

app/Actions/Photo/Rating.php

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<?php
2+
3+
/**
4+
* SPDX-License-Identifier: MIT
5+
* Copyright (c) 2017-2018 Tobias Reich
6+
* Copyright (c) 2018-2025 LycheeOrg.
7+
*/
8+
9+
namespace App\Actions\Photo;
10+
11+
use App\Exceptions\ConflictingPropertyException;
12+
use App\Models\Photo;
13+
use App\Models\PhotoRating;
14+
use App\Models\Statistics;
15+
use App\Models\User;
16+
use Illuminate\Support\Facades\DB;
17+
18+
class Rating
19+
{
20+
/**
21+
* Set or update the rating for the photo.
22+
*
23+
* Handles:
24+
* - Creating new ratings (rating > 0, no existing rating)
25+
* - Updating existing ratings (rating > 0, existing rating)
26+
* - Removing ratings (rating == 0)
27+
* - Atomic statistics updates
28+
*
29+
* @param Photo $photo The photo to rate
30+
* @param User $user The user rating the photo
31+
* @param int $rating The rating value (0-5, where 0 removes the rating)
32+
*
33+
* @return Photo the photo with refreshed statistics
34+
*
35+
* @throws ConflictingPropertyException if a database conflict occurs during the transaction
36+
*/
37+
public function do(Photo $photo, User $user, int $rating): Photo
38+
{
39+
try {
40+
DB::transaction(function () use ($photo, $user, $rating): void {
41+
// Ensure statistics record exists atomically (Q001-07)
42+
$statistics = Statistics::firstOrCreate(
43+
['photo_id' => $photo->id],
44+
[
45+
'album_id' => null,
46+
'visit_count' => 0,
47+
'download_count' => 0,
48+
'favourite_count' => 0,
49+
'shared_count' => 0,
50+
'rating_sum' => 0,
51+
'rating_count' => 0,
52+
]
53+
);
54+
55+
if ($rating > 0) {
56+
// Find existing rating by this user for this photo
57+
$existing_rating = PhotoRating::where('photo_id', $photo->id)
58+
->where('user_id', $user->id)
59+
->first();
60+
61+
if ($existing_rating !== null) {
62+
// Update: adjust statistics delta
63+
$delta = $rating - $existing_rating->rating;
64+
$statistics->rating_sum += $delta;
65+
$existing_rating->rating = $rating;
66+
$existing_rating->save();
67+
} else {
68+
// Insert: create new rating and increment statistics
69+
PhotoRating::create([
70+
'photo_id' => $photo->id,
71+
'user_id' => $user->id,
72+
'rating' => $rating,
73+
]);
74+
$statistics->rating_sum += $rating;
75+
$statistics->rating_count++;
76+
}
77+
78+
$statistics->save();
79+
} else {
80+
// Rating == 0: remove rating (idempotent, Q001-06)
81+
$existing_rating = PhotoRating::where('photo_id', $photo->id)
82+
->where('user_id', $user->id)
83+
->first();
84+
85+
if ($existing_rating !== null) {
86+
$statistics->rating_sum -= $existing_rating->rating;
87+
$statistics->rating_count--;
88+
$statistics->save();
89+
$existing_rating->delete();
90+
}
91+
// If no existing rating, do nothing (idempotent)
92+
}
93+
});
94+
95+
// Reload photo with fresh statistics
96+
$photo->refresh();
97+
98+
return $photo;
99+
} catch (\Throwable $e) {
100+
throw new ConflictingPropertyException('Failed to update photo rating due to a conflict. Please try again.', $e);
101+
}
102+
}
103+
}

app/Actions/Photo/Timeline.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public function do(): Builder
5252
// @codeCoverageIgnoreEnd
5353

5454
return $this->photo_query_policy->applySearchabilityFilter(
55-
query: Photo::query()->with(['statistics', 'size_variants', 'statistics', 'palette', 'tags']),
55+
query: Photo::query()->with(['statistics', 'size_variants', 'statistics', 'palette', 'tags', 'rating']),
5656
origin: null,
5757
include_nsfw: !$this->config_manager->getValueAsBool('hide_nsfw_in_timeline')
5858
)->orderBy($order->value, OrderSortingType::DESC->value);

app/Actions/Search/PhotoSearch.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public function query(array $terms): Collection
5656
public function sqlQuery(array $terms, ?Album $album = null): Builder
5757
{
5858
$query = $this->photo_query_policy->applySearchabilityFilter(
59-
query: Photo::query()->with(['albums', 'statistics', 'size_variants', 'palette', 'tags']),
59+
query: Photo::query()->with(['albums', 'statistics', 'size_variants', 'palette', 'tags', 'rating']),
6060
origin: $album,
6161
include_nsfw: !$this->config_manager->getValueAsBool('hide_nsfw_in_search')
6262
);

app/Actions/Tag/GetTagWithPhotos.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public function do(Tag $tag): TagWithPhotosResource
4545
$user = Auth::user();
4646

4747
$base_query = Photo::query()
48-
->with(['size_variants', 'statistics', 'palette', 'tags'])
48+
->with(['size_variants', 'statistics', 'palette', 'tags', 'rating'])
4949
->when(
5050
$user->may_administrate === false,
5151
fn ($q) => $q->where('photos.owner_id', Auth::id())

app/Contracts/Http/Requests/RequestAttribute.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ class RequestAttribute
8383
public const FILE_ATTRIBUTE = 'file';
8484
public const SHALL_OVERRIDE_ATTRIBUTE = 'shall_override';
8585
public const IS_STARRED_ATTRIBUTE = 'is_starred';
86+
public const RATING_ATTRIBUTE = 'rating';
8687
public const DIRECTION_ATTRIBUTE = 'direction';
8788

8889
public const SINGLE_PATH_ATTRIBUTE = 'path';

0 commit comments

Comments
 (0)