diff --git a/app/Actions/Metrics/GetMetrics.php b/app/Actions/Metrics/GetMetrics.php index 92618f7404c..4d913485090 100644 --- a/app/Actions/Metrics/GetMetrics.php +++ b/app/Actions/Metrics/GetMetrics.php @@ -21,22 +21,26 @@ public function get(): Collection /** @var User $user */ $user = Auth::user() ?? throw new UnauthorizedException(); + // If this query becomes too slow, then we probably will need to refactor it for something that does not use + // relationship magic but instead extracts the values directly from the database. return LiveMetrics::query()->with(['photo', 'photo.size_variants', 'album', 'album_impl', 'album.thumb']) ->join('photos', 'photos.id', '=', 'live_metrics.photo_id', 'left') ->join('base_albums', 'base_albums.id', '=', 'live_metrics.album_id', 'left') ->join('albums', 'albums.id', '=', 'live_metrics.album_id', 'left') + // Unnecessary but safer as that avoids some issues in the front-end if the migration failed for some reasons... + ->whereNotNull('live_metrics.album_id') + // Owner check (if not admin) ->when(!$user->may_administrate, fn ($q_owner) => $q_owner ->where(fn ($q) => $q - ->where('base_albums.owner_id', $user->id) - ->orWhere('photos.owner_id', $user->id)) + ->where('base_albums.owner_id', '=', $user->id) + ->orWhere('photos.owner_id', '=', $user->id)) ) // Do not fetch the visit for photos (too noisy) ->where(fn ($q) => $q->where('live_metrics.action', '!=', 'visit') - ->orWhere(fn ($q1) => $q1->where('live_metrics.action', 'visit') - ->whereNotNull('live_metrics.album_id'))) + ->orWhere(fn ($q1) => $q1->whereNull('live_metrics.photo_id'))) // Do not fetch the tag albums too. // Maybe refactor if we decide to unify tag albums and normal albums... @@ -45,8 +49,6 @@ public function get(): Collection ->select([ 'live_metrics.*', - // // // 'photos.title as photo_title', - // // // 'base_albums.title as album_title', ]) ->orderBy('live_metrics.created_at', 'desc') ->get(); diff --git a/app/Contracts/Http/Requests/HasFromId.php b/app/Contracts/Http/Requests/HasFromId.php new file mode 100644 index 00000000000..01c38131ac3 --- /dev/null +++ b/app/Contracts/Http/Requests/HasFromId.php @@ -0,0 +1,17 @@ + $this->visitor_id, + 'action' => $this->metricAction(), + 'album_id' => $this->id, + 'created_at' => now(), + ]; + } +} diff --git a/app/Events/Metrics/BaseMetricsEvent.php b/app/Events/Metrics/BaseMetricsEvent.php index 161b49a0169..fd044dd1228 100644 --- a/app/Events/Metrics/BaseMetricsEvent.php +++ b/app/Events/Metrics/BaseMetricsEvent.php @@ -11,6 +11,7 @@ use App\Enum\MetricsAction; use Illuminate\Broadcasting\InteractsWithSockets; use Illuminate\Foundation\Events\Dispatchable; +use Illuminate\Support\Carbon; abstract class BaseMetricsEvent { @@ -40,4 +41,11 @@ abstract public function key(): string; * @codeCoverageIgnore, abstract method can't be covered */ abstract public function metricAction(): MetricsAction; + + /** + * Convert the event to an array for insertion into the database. + * + * @return array + */ + abstract public function toArray(): array; } diff --git a/app/Events/Metrics/BasePhotoMetricsEvent.php b/app/Events/Metrics/BasePhotoMetricsEvent.php new file mode 100644 index 00000000000..565d9cedd12 --- /dev/null +++ b/app/Events/Metrics/BasePhotoMetricsEvent.php @@ -0,0 +1,54 @@ +album_id = $album_id; + } + + /** + * Return the type of key : photo_id or album_id. + * + * @return string + * + * @codeCoverageIgnore, abstract method can't be covered + */ + final public function key(): string + { + return 'photo_id'; + } + + /** + * Convert the event to an array for insertion into the database. + * + * @return array{visitor_id:string,action:MetricsAction,album_id:string,photo_id:string,created_at:Carbon} + */ + final public function toArray(): array + { + return [ + 'visitor_id' => $this->visitor_id, + 'action' => $this->metricAction(), + 'album_id' => $this->album_id, + 'photo_id' => $this->id, + 'created_at' => now(), + ]; + } +} diff --git a/app/Events/Metrics/PhotoDownload.php b/app/Events/Metrics/PhotoDownload.php index b967a07d953..31d83f3a94a 100644 --- a/app/Events/Metrics/PhotoDownload.php +++ b/app/Events/Metrics/PhotoDownload.php @@ -13,13 +13,8 @@ /** * This event is fired when one or multiple photos are downloaded. */ -final class PhotoDownload extends BaseMetricsEvent +final class PhotoDownload extends BasePhotoMetricsEvent { - public function key(): string - { - return 'photo_id'; - } - public function metricAction(): MetricsAction { return MetricsAction::DOWNLOAD; diff --git a/app/Events/Metrics/PhotoFavourite.php b/app/Events/Metrics/PhotoFavourite.php index 176aa179b4b..809f83da920 100644 --- a/app/Events/Metrics/PhotoFavourite.php +++ b/app/Events/Metrics/PhotoFavourite.php @@ -13,13 +13,8 @@ /** * This event is fired when a photo is visited. */ -class PhotoFavourite extends BaseMetricsEvent +class PhotoFavourite extends BasePhotoMetricsEvent { - public function key(): string - { - return 'photo_id'; - } - public function metricAction(): MetricsAction { return MetricsAction::FAVOURITE; diff --git a/app/Events/Metrics/PhotoShared.php b/app/Events/Metrics/PhotoShared.php index 2d80a42a68f..1f349531ece 100644 --- a/app/Events/Metrics/PhotoShared.php +++ b/app/Events/Metrics/PhotoShared.php @@ -13,13 +13,8 @@ /** * This event is fired when a direct link to a photo is used. */ -class PhotoShared extends BaseMetricsEvent +class PhotoShared extends BasePhotoMetricsEvent { - public function key(): string - { - return 'photo_id'; - } - public function metricAction(): MetricsAction { return MetricsAction::SHARED; diff --git a/app/Events/Metrics/PhotoVisit.php b/app/Events/Metrics/PhotoVisit.php index 726705a344e..278da77517a 100644 --- a/app/Events/Metrics/PhotoVisit.php +++ b/app/Events/Metrics/PhotoVisit.php @@ -13,13 +13,8 @@ /** * This event is fired when a photo is visited. */ -final class PhotoVisit extends BaseMetricsEvent +final class PhotoVisit extends BasePhotoMetricsEvent { - public function key(): string - { - return 'photo_id'; - } - public function metricAction(): MetricsAction { return MetricsAction::VISIT; diff --git a/app/Http/Controllers/Gallery/AlbumController.php b/app/Http/Controllers/Gallery/AlbumController.php index 9e7d385e55f..a201a198947 100644 --- a/app/Http/Controllers/Gallery/AlbumController.php +++ b/app/Http/Controllers/Gallery/AlbumController.php @@ -320,7 +320,7 @@ public function getArchive(ZipRequest $request): StreamedResponse // We dispatch one event per photo. foreach ($request->photos() as $photo) { - PhotoDownload::dispatchIf($should_measure, $this->visitorId(), $photo->id); + PhotoDownload::dispatchIf($should_measure && $request->from_id() !== null, $this->visitorId(), $photo->id, $request->from_id()); } return PhotoBaseArchive::resolve()->do($request->photos(), $request->sizeVariant()); diff --git a/app/Http/Controllers/MetricsController.php b/app/Http/Controllers/MetricsController.php index 4a3d3b62ac0..8569fe7b408 100644 --- a/app/Http/Controllers/MetricsController.php +++ b/app/Http/Controllers/MetricsController.php @@ -45,7 +45,7 @@ public function get(MetricsRequest $request, GetMetrics $get_metrics, CleanupMet */ public function photo(PhotoMetricsRequest $request): void { - PhotoVisit::dispatchIf(self::shouldMeasure(), $request->visitorId(), $request->photoIds()[0]); + PhotoVisit::dispatchIf(self::shouldMeasure() && $request->from_id() !== null, $request->visitorId(), $request->photoIds()[0], $request->from_id()); return; } @@ -59,7 +59,7 @@ public function photo(PhotoMetricsRequest $request): void */ public function favourite(PhotoMetricsRequest $request): void { - PhotoFavourite::dispatchIf(self::shouldMeasure(), $request->visitorId(), $request->photoIds()[0]); + PhotoFavourite::dispatchIf(self::shouldMeasure() && $request->from_id() !== null, $request->visitorId(), $request->photoIds()[0], $request->from_id()); return; } diff --git a/app/Http/Controllers/VueController.php b/app/Http/Controllers/VueController.php index dd97755adec..b1bad8e1661 100644 --- a/app/Http/Controllers/VueController.php +++ b/app/Http/Controllers/VueController.php @@ -67,7 +67,7 @@ public function gallery(?string $album_id = null, ?string $photo_id = null): Vie } if ($photo !== null) { - PhotoShared::dispatchIf(MetricsController::shouldMeasure(), $this->visitorId(), $photo->id); + PhotoShared::dispatchIf(MetricsController::shouldMeasure() && $album_id !== null, $this->visitorId(), $photo->id, $album_id); } elseif ($album !== null) { AlbumShared::dispatchIf(MetricsController::shouldMeasure(), $this->visitorId(), $album->get_id()); } diff --git a/app/Http/Requests/Album/ZipRequest.php b/app/Http/Requests/Album/ZipRequest.php index 358b1ebb52d..540631226f5 100644 --- a/app/Http/Requests/Album/ZipRequest.php +++ b/app/Http/Requests/Album/ZipRequest.php @@ -9,6 +9,7 @@ namespace App\Http\Requests\Album; use App\Contracts\Http\Requests\HasAlbums; +use App\Contracts\Http\Requests\HasFromId; use App\Contracts\Http\Requests\HasPhotos; use App\Contracts\Http\Requests\HasSizeVariant; use App\Contracts\Http\Requests\RequestAttribute; @@ -16,6 +17,7 @@ use App\Enum\DownloadVariantType; use App\Http\Requests\BaseApiRequest; use App\Http\Requests\Traits\HasAlbumsTrait; +use App\Http\Requests\Traits\HasFromIdTrait; use App\Http\Requests\Traits\HasPhotosTrait; use App\Http\Requests\Traits\HasSizeVariantTrait; use App\Models\Photo; @@ -23,17 +25,19 @@ use App\Policies\PhotoPolicy; use App\Rules\AlbumIDListRule; use App\Rules\RandomIDListRule; +use App\Rules\RandomIDRule; use Illuminate\Support\Facades\Gate; use Illuminate\Validation\Rules\Enum; /** * @implements HasAlbums */ -class ZipRequest extends BaseApiRequest implements HasAlbums, HasPhotos, HasSizeVariant +class ZipRequest extends BaseApiRequest implements HasAlbums, HasPhotos, HasSizeVariant, HasFromId { /** @use HasAlbumsTrait */ use HasAlbumsTrait; use HasPhotosTrait; + use HasFromIdTrait; use HasSizeVariantTrait; /** @@ -67,6 +71,7 @@ public function rules(): array RequestAttribute::ALBUM_IDS_ATTRIBUTE => ['sometimes', new AlbumIDListRule()], RequestAttribute::PHOTO_IDS_ATTRIBUTE => ['sometimes', new RandomIDListRule()], RequestAttribute::SIZE_VARIANT_ATTRIBUTE => ['required_if_accepted:photos_ids', new Enum(DownloadVariantType::class)], + RequestAttribute::FROM_ID_ATTRIBUTE => ['required_if_accepted:photos_ids', new RandomIDRule(true)], ]; } @@ -85,6 +90,8 @@ protected function processValidatedValues(array $values, array $files): void $photo_ids = $values[RequestAttribute::PHOTO_IDS_ATTRIBUTE] ?? null; $photo_ids = $photo_ids === null ? [] : explode(',', $photo_ids); $this->processPhotos($photo_ids); + + $this->from_id = $values[RequestAttribute::FROM_ID_ATTRIBUTE] ?? null; } /** diff --git a/app/Http/Requests/Metrics/PhotoMetricsRequest.php b/app/Http/Requests/Metrics/PhotoMetricsRequest.php index 23d1c9f2954..ae314fffbca 100644 --- a/app/Http/Requests/Metrics/PhotoMetricsRequest.php +++ b/app/Http/Requests/Metrics/PhotoMetricsRequest.php @@ -8,17 +8,20 @@ namespace App\Http\Requests\Metrics; +use App\Contracts\Http\Requests\HasFromId; use App\Contracts\Http\Requests\HasPhotoIds; use App\Contracts\Http\Requests\HasVisitorId; use App\Contracts\Http\Requests\RequestAttribute; use App\Http\Requests\BaseApiRequest; +use App\Http\Requests\Traits\HasFromIdTrait; use App\Http\Requests\Traits\HasPhotoIdsTrait; use App\Http\Requests\Traits\HasVisitorIdTrait; use App\Rules\RandomIDRule; -class PhotoMetricsRequest extends BaseApiRequest implements HasPhotoIds, HasVisitorId +class PhotoMetricsRequest extends BaseApiRequest implements HasPhotoIds, HasVisitorId, HasFromId { use HasPhotoIdsTrait; + use HasFromIdTrait; use HasVisitorIdTrait; // No need to authorize this request as it is only used for metrics purposes @@ -33,6 +36,7 @@ public function authorize(): bool public function rules(): array { return [ + RequestAttribute::FROM_ID_ATTRIBUTE => ['required', new RandomIDRule(true)], RequestAttribute::PHOTO_IDS_ATTRIBUTE => 'required|array|min:1', RequestAttribute::PHOTO_IDS_ATTRIBUTE . '.*' => ['required', new RandomIDRule(false)], ]; @@ -44,5 +48,6 @@ public function rules(): array protected function processValidatedValues(array $values, array $files): void { $this->photo_ids = $values[RequestAttribute::PHOTO_IDS_ATTRIBUTE]; + $this->from_id = $values[RequestAttribute::FROM_ID_ATTRIBUTE] ?? null; } } diff --git a/app/Http/Requests/Traits/HasFromIdTrait.php b/app/Http/Requests/Traits/HasFromIdTrait.php new file mode 100644 index 00000000000..9459e550c47 --- /dev/null +++ b/app/Http/Requests/Traits/HasFromIdTrait.php @@ -0,0 +1,22 @@ +from_id; + } +} \ No newline at end of file diff --git a/app/Http/Resources/Models/LiveMetricsResource.php b/app/Http/Resources/Models/LiveMetricsResource.php index cd5a73aaa70..f692550f693 100644 --- a/app/Http/Resources/Models/LiveMetricsResource.php +++ b/app/Http/Resources/Models/LiveMetricsResource.php @@ -33,14 +33,14 @@ public function __construct( public static function fromModel(LiveMetrics $a): LiveMetricsResource { $title = $a->photo_id !== null ? $a->photo->title : $a->album_impl->title; - $url = $a->photo_id !== null ? $a->photo->size_variants?->getSmall()?->url ?? $a->photo->size_variants?->getThumb()?->url : $a->album?->thumb?->thumbUrl; + $url = $a->photo_id !== null ? $a->photo->size_variants?->getThumb()?->url : $a->album?->thumb?->thumbUrl; return new self( created_at: $a->created_at->toIso8601String(), // toIso8601String() is used to ensure the date has the correct timezone visitor_id: $a->visitor_id, action: $a->action, photo_id: $a->photo_id, - album_id: $a->album_id ?? $a->photo->album_id, + album_id: $a->album_id, title: $title, url: $url, ); diff --git a/app/Listeners/MetricsListener.php b/app/Listeners/MetricsListener.php index 228fba697e8..499320e1bdd 100644 --- a/app/Listeners/MetricsListener.php +++ b/app/Listeners/MetricsListener.php @@ -31,15 +31,7 @@ public function handle(BaseMetricsEvent $event): void if (Configs::getValueAsBool('live_metrics_enabled') === true) { // Add event to the live metrics table - DB::table('live_metrics') - ->insert([ - [ - 'visitor_id' => $event->visitor_id, - 'action' => $event->metricAction(), - $event->key() => $event->id, - 'created_at' => now(), - ], - ]); + DB::table('live_metrics')->insert([$event->toArray()]); } } } diff --git a/database/migrations/2025_05_28_194147_fix_livemetrics_album_id.php b/database/migrations/2025_05_28_194147_fix_livemetrics_album_id.php new file mode 100644 index 00000000000..482ce1b85f0 --- /dev/null +++ b/database/migrations/2025_05_28_194147_fix_livemetrics_album_id.php @@ -0,0 +1,28 @@ +whereNotNull('photo_id')->update(['album_id' => null]); + } +}; diff --git a/resources/js/components/gallery/albumModule/AlbumPanel.vue b/resources/js/components/gallery/albumModule/AlbumPanel.vue index 21d03a1deb3..4b496646630 100644 --- a/resources/js/components/gallery/albumModule/AlbumPanel.vue +++ b/resources/js/components/gallery/albumModule/AlbumPanel.vue @@ -188,7 +188,7 @@ const { albumClick, } = useSelection(photos, children, togglableStore); -const { photoRoute } = usePhotoRoute(router); +const { photoRoute, getParentId } = usePhotoRoute(router); function photoClick(idx: number, e: MouseEvent) { router.push(photoRoute(photos.value[idx].id)); @@ -238,7 +238,7 @@ const photoCallbacks = { toggleMove: toggleMove, toggleDelete: toggleDelete, toggleDownload: () => { - PhotoService.download(selectedPhotosIds.value); + PhotoService.download(selectedPhotosIds.value, getParentId()); }, }; diff --git a/resources/js/components/gallery/albumModule/thumbs/PhotoThumb.vue b/resources/js/components/gallery/albumModule/thumbs/PhotoThumb.vue index 1931a22f014..f06e8aedba1 100644 --- a/resources/js/components/gallery/albumModule/thumbs/PhotoThumb.vue +++ b/resources/js/components/gallery/albumModule/thumbs/PhotoThumb.vue @@ -87,6 +87,8 @@ import { storeToRefs } from "pinia"; import { useImageHelpers } from "@/utils/Helpers"; import { useFavouriteStore } from "@/stores/FavouriteState"; import ThumbFavourite from "./ThumbFavourite.vue"; +import { useRouter } from "vue-router"; +import { usePhotoRoute } from "@/composables/photo/photoRoute"; const { getNoImageIcon, getPlayIcon } = useImageHelpers(); @@ -101,6 +103,8 @@ const props = defineProps<{ photo: App.Http.Resources.Models.PhotoResource; }>(); +const router = useRouter(); +const { getParentId } = usePhotoRoute(router); const auth = useAuthStore(); const { user } = storeToRefs(auth); const favourites = useFavouriteStore(); @@ -112,7 +116,7 @@ const srcNoImage = ref(getNoImageIcon()); const isImageLoaded = ref(false); function toggleFavourite() { - favourites.toggle(props.photo); + favourites.toggle(props.photo, getParentId()); } function onImageLoad() { diff --git a/resources/js/services/metrics-service.ts b/resources/js/services/metrics-service.ts index 922a648c335..3e94e4a051f 100644 --- a/resources/js/services/metrics-service.ts +++ b/resources/js/services/metrics-service.ts @@ -6,17 +6,27 @@ const MetricsService = { return axios.get(`${Constants.getApiUrl()}Metrics`, { data: {} }); }, - photo(photo_id: string): Promise | null> { + photo(photo_id: string, album_id: string | undefined): Promise | null> { if (!photo_id) { // TODO: figure out why this sometimes happens... // Do not send a request if no photo_id is provided, otherwise it breaks in front-end. return Promise.resolve(null); } - return axios.post(`${Constants.getApiUrl()}Metrics::photo`, { photo_ids: [photo_id] }); + // This is the case if we are in global search mode. + if (album_id === undefined) { + return Promise.resolve(null); + } + + return axios.post(`${Constants.getApiUrl()}Metrics::photo`, { photo_ids: [photo_id], from_id: album_id }); }, - favourite(photo_id: string): Promise> { - return axios.post(`${Constants.getApiUrl()}Metrics::favourite`, { photo_ids: [photo_id] }); + favourite(photo_id: string, album_id: string | undefined): Promise | null> { + // This is the case if we are in global search mode. + if (album_id === undefined) { + return Promise.resolve(null); + } + + return axios.post(`${Constants.getApiUrl()}Metrics::favourite`, { photo_ids: [photo_id], from_id: album_id }); }, }; diff --git a/resources/js/services/photo-service.ts b/resources/js/services/photo-service.ts index 540fbf5c323..edbdc30ed5d 100644 --- a/resources/js/services/photo-service.ts +++ b/resources/js/services/photo-service.ts @@ -68,8 +68,8 @@ const PhotoService = { return axios.post(`${Constants.getApiUrl()}Album::header`, { header_id: photo_id, album_id: album_id, is_compact: is_compact }); }, - download(photo_ids: string[], download_type: App.Enum.DownloadVariantType = "ORIGINAL"): void { - location.href = `${Constants.getApiUrl()}Zip?photo_ids=${photo_ids.join(",")}&variant=${download_type}`; + download(photo_ids: string[], from_id: string | undefined, download_type: App.Enum.DownloadVariantType = "ORIGINAL"): void { + location.href = `${Constants.getApiUrl()}Zip?photo_ids=${photo_ids.join(",")}&variant=${download_type}&from_id=${from_id ?? null}`; }, }; diff --git a/resources/js/stores/FavouriteState.ts b/resources/js/stores/FavouriteState.ts index 2974eb9df9a..3d09905d487 100644 --- a/resources/js/stores/FavouriteState.ts +++ b/resources/js/stores/FavouriteState.ts @@ -35,7 +35,7 @@ export const useFavouriteStore = defineStore("favourite-store", { } this.photos = this.photos.filter((p: App.Http.Resources.Models.PhotoResource) => p.id !== photoId); }, - toggle(photo: App.Http.Resources.Models.PhotoResource) { + toggle(photo: App.Http.Resources.Models.PhotoResource, albumId: string | undefined) { if (!this.photos) { this.photos = []; } @@ -44,7 +44,7 @@ export const useFavouriteStore = defineStore("favourite-store", { this.removePhoto(photo.id); } else { this.addPhoto(photo); - MetricsService.favourite(photo.id); + MetricsService.favourite(photo.id, albumId); } }, }, diff --git a/resources/js/views/gallery-panels/Album.vue b/resources/js/views/gallery-panels/Album.vue index d12cdd4c151..816472da808 100644 --- a/resources/js/views/gallery-panels/Album.vue +++ b/resources/js/views/gallery-panels/Album.vue @@ -171,6 +171,7 @@ import { useHasNextPreviousPhoto } from "@/composables/photo/hasNextPreviousPhot import { getNextPreviousPhoto } from "@/composables/photo/getNextPreviousPhoto"; import { usePhotoRefresher } from "@/composables/photo/hasRefresher"; import MetricsService from "@/services/metrics-service"; +import { usePhotoRoute } from "@/composables/photo/photoRoute"; const route = useRoute(); const router = useRouter(); @@ -216,6 +217,7 @@ const { isPasswordProtected, isLoading, user, modelAlbum, album, photo, transiti useAlbumRefresher(albumId, photoId, auth, is_login_open); const { refreshPhoto } = usePhotoRefresher(photo, photos, photoId); +const { getParentId } = usePhotoRoute(router); const children = computed(() => modelAlbum.value?.albums ?? []); @@ -413,7 +415,7 @@ onUnmounted(() => { const debouncedPhotoMetrics = useDebounceFn(() => { if (photoId.value !== undefined) { - MetricsService.photo(photoId.value); + MetricsService.photo(photoId.value, getParentId()); return; } }, 100); diff --git a/resources/js/views/gallery-panels/Search.vue b/resources/js/views/gallery-panels/Search.vue index 20e2b44dea8..65239ed2f47 100644 --- a/resources/js/views/gallery-panels/Search.vue +++ b/resources/js/views/gallery-panels/Search.vue @@ -165,6 +165,7 @@ import LoadingProgress from "@/components/loading/LoadingProgress.vue"; import { shouldIgnoreKeystroke } from "@/utils/keybindings-utils"; import { useHasNextPreviousPhoto } from "@/composables/photo/hasNextPreviousPhoto"; import MetricsService from "@/services/metrics-service"; +import { usePhotoRoute } from "@/composables/photo/photoRoute"; const route = useRoute(); const router = useRouter(); @@ -212,6 +213,7 @@ const { refresh, } = useSearch(albumId, search_term, search_page); +const { getParentId } = usePhotoRoute(router); const { refreshPhoto } = usePhotoRefresher(photo, photos, photoId); const { albumsForSelection, photosForSelection, noData, configForMenu, title } = useSearchComputed(config, album, albums, photos, lycheeStore); @@ -423,7 +425,7 @@ onMounted(() => { const debouncedPhotoMetrics = useDebounceFn(() => { if (photoId.value !== undefined) { - MetricsService.photo(photoId.value); + MetricsService.photo(photoId.value, getParentId()); return; } }, 100); diff --git a/tests/Feature_v2/Album/DownloadTest.php b/tests/Feature_v2/Album/DownloadTest.php index 3c114362913..d758e6c7571 100644 --- a/tests/Feature_v2/Album/DownloadTest.php +++ b/tests/Feature_v2/Album/DownloadTest.php @@ -68,7 +68,9 @@ public function testSinglePhotoDownload(): void $photoArchiveResponse = $this->download( photo_ids: [$photo['id']], - kind: DownloadVariantType::ORIGINAL); + from_id: $this->album5->id, + kind: DownloadVariantType::ORIGINAL + ); // Stream the response in a temporary file $memoryBlob = new InMemoryBuffer(); @@ -105,7 +107,9 @@ public function testMultiplePhotoDownload(): void $photoArchiveResponse = $this->download( photo_ids: [$response->json('resource.photos.0.id'), $response->json('resource.photos.1.id')], - kind: DownloadVariantType::ORIGINAL); + from_id: $this->album5->id, + kind: DownloadVariantType::ORIGINAL + ); $zipArchive = AssertableZipArchive::createFromResponse($photoArchiveResponse); $zipArchive->assertContainsFilesExactly([ @@ -129,6 +133,7 @@ public function testGoogleMotionPhotoDownload(): void $photoArchiveResponse = $this->download( photo_ids: [$photo['id']], + from_id: $this->album5->id, kind: DownloadVariantType::LIVEPHOTOVIDEO ); @@ -158,7 +163,8 @@ public function testAmbiguousPhotoDownload(): void 'Photo', filename: TestConstants::SAMPLE_FILE_MONGOLIA_IMAGE, album_id: $this->album5->id, - file_name: TestConstants::PHOTO_MONGOLIA_TITLE . '.jpeg'); + file_name: TestConstants::PHOTO_MONGOLIA_TITLE . '.jpeg' + ); $this->assertCreated($response); $response = $this->getJsonWithData('Album', ['album_id' => $this->album5->id]); @@ -179,7 +185,8 @@ public function testAmbiguousPhotoDownload(): void 'Photo', filename: TestConstants::SAMPLE_FILE_NIGHT_IMAGE, album_id: $this->album5->id, - file_name: TestConstants::PHOTO_NIGHT_TITLE . '.jpg'); + file_name: TestConstants::PHOTO_NIGHT_TITLE . '.jpg' + ); $this->assertCreated($response); $response = $this->getJsonWithData('Album', ['album_id' => $this->album5->id]); @@ -190,7 +197,11 @@ public function testAmbiguousPhotoDownload(): void $photoID2a = $response->json('resource.photos.1.id'); $photoID2b = $response->json('resource.photos.2.id'); - $photoArchiveResponse = $this->download([$photoID1, $photoID2a, $photoID2b], kind: DownloadVariantType::ORIGINAL); + $photoArchiveResponse = $this->download( + photo_ids: [$photoID1, $photoID2a, $photoID2b], + from_id: $this->album5->id, + kind: DownloadVariantType::ORIGINAL + ); $zipArchive = AssertableZipArchive::createFromResponse($photoArchiveResponse); $zipArchive->assertContainsFilesExactly([ @@ -206,7 +217,8 @@ public function testPhotoDownloadWithMultiByteFilename(): void 'Photo', filename: TestConstants::SAMPLE_FILE_SUNSET_IMAGE, album_id: $this->album5->id, - file_name: 'fin de journée.jpg'); + file_name: 'fin de journée.jpg' + ); $this->assertCreated($response); $response = $this->getJsonWithData('Album', ['album_id' => $this->album5->id]); @@ -215,7 +227,11 @@ public function testPhotoDownloadWithMultiByteFilename(): void $id = $response->json('resource.photos.0.id'); - $download = $this->download([$id], kind: DownloadVariantType::ORIGINAL); + $download = $this->download( + photo_ids: [$id], + from_id: $this->album5->id, + kind: DownloadVariantType::ORIGINAL + ); $download->assertHeader('Content-Type', TestConstants::MIME_TYPE_IMG_JPEG); $download->assertHeader('Content-Length', filesize(base_path(TestConstants::SAMPLE_FILE_SUNSET_IMAGE))); $download->assertHeader('Content-Disposition', HeaderUtils::makeDisposition( @@ -239,7 +255,8 @@ public function testMultiplePhotoAlbumDownloadWithMultiByteFilename(): void 'Photo', filename: TestConstants::SAMPLE_FILE_SUNSET_IMAGE, album_id: $this->album5->id, - file_name: 'fin de journée.jpg'); + file_name: 'fin de journée.jpg' + ); $this->assertCreated($response); $album6 = Album::factory()->children_of($this->album5)->owned_by($this->admin)->create(); @@ -248,7 +265,8 @@ public function testMultiplePhotoAlbumDownloadWithMultiByteFilename(): void 'Photo', filename: TestConstants::SAMPLE_FILE_MONGOLIA_IMAGE, album_id: $album6->id, - file_name: TestConstants::PHOTO_MONGOLIA_TITLE . '.jpeg'); + file_name: TestConstants::PHOTO_MONGOLIA_TITLE . '.jpeg' + ); $this->assertCreated($response); $response = $this->getJsonWithData('Album', ['album_id' => $this->album5->id]); @@ -336,4 +354,4 @@ public function testMultiplePhotoAlbumDownloadWithMultiByteFilename(): void // Configs::set(TestConstants::CONFIG_DOWNLOADABLE, $areAlbumsDownloadable); // } // } -} \ No newline at end of file +} diff --git a/tests/Feature_v2/Base/BaseApiTest.php b/tests/Feature_v2/Base/BaseApiTest.php index 454b04586cf..7dabdd72898 100644 --- a/tests/Feature_v2/Base/BaseApiTest.php +++ b/tests/Feature_v2/Base/BaseApiTest.php @@ -178,6 +178,7 @@ public function deleteJson($uri, array $data = [], array $headers = [], $options public function download( array $photo_ids = [], array $album_ids = [], + string $from_id = '', DownloadVariantType $kind = DownloadVariantType::ORIGINAL, $expectedStatusCode = 200, ): TestResponse { @@ -189,6 +190,9 @@ public function download( if ($album_ids !== []) { $params['album_ids'] = implode(',', $album_ids); } + if ($from_id !== '') { + $params['from_id'] = $from_id; + } $response = $this->getWithParameters(self::API_PREFIX . 'Zip', $params, [ 'Accept' => '*/*', diff --git a/tests/Feature_v2/Metrics/EventsFiredTest.php b/tests/Feature_v2/Metrics/EventsFiredTest.php index 5bf9a5efffe..6543fa0b3b9 100644 --- a/tests/Feature_v2/Metrics/EventsFiredTest.php +++ b/tests/Feature_v2/Metrics/EventsFiredTest.php @@ -53,11 +53,11 @@ public function testVisitSharedAlbum(): void public function testVisitSharedPhoto(): void { - $response = $this->postJson('Metrics::photo', ['photo_ids' => [$this->photo4->id]]); + $response = $this->postJson('Metrics::photo', ['photo_ids' => [$this->photo4->id], 'from_id' => $this->album4->id]); $this->assertNoContent($response); $this->assertEquals(1, Statistics::where('photo_id', $this->photo4->id)->firstOrFail()->visit_count); - $response = $this->postJson('Metrics::favourite', ['photo_ids' => [$this->photo4->id]]); + $response = $this->postJson('Metrics::favourite', ['photo_ids' => [$this->photo4->id], 'from_id' => $this->album4->id]); $this->assertNoContent($response); $this->assertEquals(1, Statistics::where('photo_id', $this->photo4->id)->firstOrFail()->favourite_count); diff --git a/tests/Feature_v2/Metrics/MetricsGetTest.php b/tests/Feature_v2/Metrics/MetricsGetTest.php index a50b78756f5..1809f1db747 100644 --- a/tests/Feature_v2/Metrics/MetricsGetTest.php +++ b/tests/Feature_v2/Metrics/MetricsGetTest.php @@ -73,9 +73,9 @@ public function testWithData(): void $this->assertOk($response); // 1: - viewed album4 $response = $this->get('gallery/' . $this->album4->id); $this->assertOk($response); // 2: - shared album4 - $response = $this->postJson('Metrics::photo', ['photo_ids' => [$this->photo4->id]]); + $response = $this->postJson('Metrics::photo', ['photo_ids' => [$this->photo4->id], 'from_id' => $this->album4->id]); $this->assertNoContent($response); // 3: viewed photo4 - $response = $this->postJson('Metrics::favourite', ['photo_ids' => [$this->photo4->id]]); + $response = $this->postJson('Metrics::favourite', ['photo_ids' => [$this->photo4->id], 'from_id' => $this->album4->id]); $this->assertNoContent($response); // 4: favourite photo4 $response = $this->get('gallery/' . $this->album4->id . '/' . $this->photo4->id); $this->assertOk($response); // 5: shared photo4 diff --git a/tests/Unit/Http/Requests/Album/ZipRequestTest.php b/tests/Unit/Http/Requests/Album/ZipRequestTest.php index 7d76fe1c13a..e475eae7af1 100644 --- a/tests/Unit/Http/Requests/Album/ZipRequestTest.php +++ b/tests/Unit/Http/Requests/Album/ZipRequestTest.php @@ -18,6 +18,7 @@ use App\Policies\AlbumPolicy; use App\Rules\AlbumIDListRule; use App\Rules\RandomIDListRule; +use App\Rules\RandomIDRule; use Illuminate\Support\Facades\Gate; use Illuminate\Validation\Rules\Enum; use Tests\Unit\Http\Requests\Base\BaseRequestTest; @@ -62,6 +63,7 @@ public function testRules(): void RequestAttribute::ALBUM_IDS_ATTRIBUTE => ['sometimes', new AlbumIDListRule()], RequestAttribute::PHOTO_IDS_ATTRIBUTE => ['sometimes', new RandomIDListRule()], RequestAttribute::SIZE_VARIANT_ATTRIBUTE => ['required_if_accepted:photos_ids', new Enum(DownloadVariantType::class)], + RequestAttribute::FROM_ID_ATTRIBUTE => ['required_if_accepted:photos_ids', new RandomIDRule(true)], ]; $this->assertCount(count($expectedRuleMap), $rules); // only validating the first 7 rules & the GRANTS_UPLOAD_ATTRIBUTE is tested afterwards