api to fetch albums

This commit is contained in:
imperosol 2025-02-20 15:33:05 +01:00
parent feeff5970d
commit cfefd9bd5b
5 changed files with 74 additions and 11 deletions

View File

@ -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")

View File

@ -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"]

View File

@ -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 `<div class="select-item">
<span class="select-item-text">${sanitize(item.path)}</span>
</div>`;
}
protected renderItem(item: AlbumSchema, sanitize: typeof escape_html) {
protected renderItem(item: AlbumAutocompleteSchema, sanitize: typeof escape_html) {
return `<span>${sanitize(item.path)}</span>`;
}
}

View File

@ -228,3 +228,16 @@ class TestPictureModeration(TestSas):
assert res.status_code == 200
assert len(res.json()) == 1
assert res.json()[0]["author"]["id"] == self.user_a.id
class TestAlbumSearch(TestSas):
def test_num_queries(self):
"""Check the number of queries is stable"""
self.client.force_login(subscriber_user.make())
cache.clear()
with self.assertNumQueries(7):
# - 2 for authentication
# - 3 to check permissions
# - 1 for pagination
# - 1 for the actual results
self.client.get(reverse("api:search-album"))

View File

@ -2,7 +2,7 @@ from pydantic import TypeAdapter
from core.views.widgets.select import AutoCompleteSelect, AutoCompleteSelectMultiple
from sas.models import Album
from sas.schemas import AlbumSchema
from sas.schemas import AlbumAutocompleteSchema
_js = ["bundled/sas/components/ajax-select-index.ts"]
@ -10,7 +10,7 @@ _js = ["bundled/sas/components/ajax-select-index.ts"]
class AutoCompleteSelectAlbum(AutoCompleteSelect):
component_name = "album-ajax-select"
model = Album
adapter = TypeAdapter(list[AlbumSchema])
adapter = TypeAdapter(list[AlbumAutocompleteSchema])
js = _js
@ -18,6 +18,6 @@ class AutoCompleteSelectAlbum(AutoCompleteSelect):
class AutoCompleteSelectMultipleAlbum(AutoCompleteSelectMultiple):
component_name = "album-ajax-select"
model = Album
adapter = TypeAdapter(list[AlbumSchema])
adapter = TypeAdapter(list[AlbumAutocompleteSchema])
js = _js