diff --git a/sas/api.py b/sas/api.py index 11355de5..d9e2ad2e 100644 --- a/sas/api.py +++ b/sas/api.py @@ -1,6 +1,3 @@ -from typing import Annotated - -from annotated_types import MinLen from django.conf import settings from django.db.models import F from django.urls import reverse @@ -16,6 +13,8 @@ from core.auth.api_permissions import CanAccessLookup, CanView, IsInGroup, IsRoo from core.models import Notification, User from sas.models import Album, PeoplePictureRelation, Picture from sas.schemas import ( + AlbumAutocompleteSchema, + AlbumFilterSchema, AlbumSchema, IdentifiedUserSchema, ModerationRequestSchema, @@ -31,11 +30,30 @@ class AlbumController(ControllerBase): @route.get( "/search", response=PaginatedResponseSchema[AlbumSchema], + permissions=[IsAuthenticated], + url_name="search-album", + ) + @paginate(PageNumberPaginationExtra, page_size=50) + def fetch_album(self, filters: Query[AlbumFilterSchema]): + """General-purpose album search.""" + return filters.filter(Album.objects.viewable_by(self.context.request.user)) + + @route.get( + "/autocomplete-search", + response=PaginatedResponseSchema[AlbumAutocompleteSchema], permissions=[CanAccessLookup], ) @paginate(PageNumberPaginationExtra, page_size=50) - def search_album(self, search: Annotated[str, MinLen(1)]): - return Album.objects.filter(name__icontains=search) + def autocomplete_album(self, filters: Query[AlbumFilterSchema]): + """Search route to use exclusively on autocomplete input fields. + + This route is separated from `GET /sas/album/search` because + getting the path of an album may need an absurd amount of db queries. + + If you don't need the path of the albums, + do NOT use this route. + """ + return filters.filter(Album.objects.viewable_by(self.context.request.user)) @api_controller("/sas/picture") diff --git a/sas/schemas.py b/sas/schemas.py index d606219b..76eb908a 100644 --- a/sas/schemas.py +++ b/sas/schemas.py @@ -1,6 +1,8 @@ from datetime import datetime from pathlib import Path +from typing import Annotated +from annotated_types import MinLen from django.urls import reverse from ninja import FilterSchema, ModelSchema, Schema from pydantic import Field, NonNegativeInt @@ -9,7 +11,37 @@ from core.schemas import SimpleUserSchema, UserProfileSchema from sas.models import Album, Picture, PictureModerationRequest +class AlbumFilterSchema(FilterSchema): + search: Annotated[str, MinLen(1)] | None = Field(None, q="name__icontains") + before_date: datetime | None = Field(None, q="event_date__lte") + after_date: datetime | None = Field(None, q="event_date__gte") + parent_id: int | None = Field(None, q="parent_id") + + class AlbumSchema(ModelSchema): + class Meta: + model = Album + fields = ["id", "name", "is_moderated"] + + thumbnail: str | None + sas_url: str + + @staticmethod + def resolve_thumbnail(obj: Album) -> str | None: + # Album thumbnails aren't stored in `Album.thumbnail` but in `Album.file` + # Don't ask me why. + if not obj.file: + return None + return obj.get_download_url() + + @staticmethod + def resolve_sas_url(obj: Album) -> str: + return obj.get_absolute_url() + + +class AlbumAutocompleteSchema(ModelSchema): + """Schema to use on album autocomplete input field.""" + class Meta: model = Album fields = ["id", "name"] diff --git a/sas/static/bundled/sas/components/ajax-select-index.ts b/sas/static/bundled/sas/components/ajax-select-index.ts index 5b811f52..e11d96c2 100644 --- a/sas/static/bundled/sas/components/ajax-select-index.ts +++ b/sas/static/bundled/sas/components/ajax-select-index.ts @@ -2,7 +2,7 @@ import { AjaxSelect } from "#core:core/components/ajax-select-base"; import { registerComponent } from "#core:utils/web-components"; import type { TomOption } from "tom-select/dist/types/types"; import type { escape_html } from "tom-select/dist/types/utils"; -import { type AlbumSchema, albumSearchAlbum } from "#openapi"; +import { type AlbumAutocompleteSchema, albumSearchAlbum } from "#openapi"; @registerComponent("album-ajax-select") export class AlbumAjaxSelect extends AjaxSelect { @@ -18,13 +18,13 @@ export class AlbumAjaxSelect extends AjaxSelect { return []; } - protected renderOption(item: AlbumSchema, sanitize: typeof escape_html) { + protected renderOption(item: AlbumAutocompleteSchema, sanitize: typeof escape_html) { return `