paginate GET /api/sas/picture

This commit is contained in:
thomas girod 2024-08-05 19:25:30 +02:00
parent a056bd177f
commit 0b9ccf6a57
5 changed files with 58 additions and 32 deletions

View File

@ -23,7 +23,7 @@
<button <button
:disabled="in_progress" :disabled="in_progress"
class="btn btn-blue" class="btn btn-blue"
@click="download('{{ url("api:pictures") }}?users_identified={{ object.id }}')" @click="download_zip()"
> >
<i class="fa fa-download"></i> <i class="fa fa-download"></i>
{% trans %}Download all my pictures{% endtrans %} {% trans %}Download all my pictures{% endtrans %}
@ -86,13 +86,34 @@
Alpine.data("picture_download", () => ({ Alpine.data("picture_download", () => ({
in_progress: false, in_progress: false,
async download(url) { /**
* @return {Promise<Picture[]>}
*/
async get_pictures() {
{# The API forbids to get more than 199 items at once
from paginated routes.
In order to download all the user pictures, it may be needed
to performs multiple requests #}
const max_per_page = 1;
const url = "{{ url("api:pictures") }}"
+ "?users_identified={{ object.id }}"
+ `&page_size=${max_per_page}`;
let promises = [];
const nb_pages = Math.ceil({{ nb_pictures }} / max_per_page);
for (let i = 1; i <= nb_pages; i++) {
promises.push(
fetch(url + `&page=${i}`).then(res => res.json().then(json => json.results))
);
}
return (await Promise.all(promises)).flat()
},
async download_zip(){
this.in_progress = true; this.in_progress = true;
const bar = this.$refs.progress; const bar = this.$refs.progress;
bar.value = 0; bar.value = 0;
const pictures = await this.get_pictures();
/** @type Picture[] */
const pictures = await (await fetch(url)).json();
bar.max = pictures.length; bar.max = pictures.length;
const fileHandle = await window.showSaveFilePicker({ const fileHandle = await window.showSaveFilePicker({

View File

@ -319,6 +319,7 @@ class UserPicturesView(UserTabsMixin, CanViewMixin, DetailView):
.order_by("-parent__date", "-date") .order_by("-parent__date", "-date")
.annotate(album=F("parent__name")) .annotate(album=F("parent__name"))
) )
kwargs["nb_pictures"] = len(pictures)
kwargs["albums"] = { kwargs["albums"] = {
album: list(picts) album: list(picts)
for album, picts in itertools.groupby(pictures, lambda i: i.album) for album, picts in itertools.groupby(pictures, lambda i: i.album)

View File

@ -1,9 +1,11 @@
from django.conf import settings from django.conf import settings
from django.db.models import F from django.db.models import F
from ninja import Query from ninja import Query
from ninja_extra import ControllerBase, api_controller, route from ninja_extra import ControllerBase, api_controller, paginate, route
from ninja_extra.exceptions import PermissionDenied from ninja_extra.exceptions import PermissionDenied
from ninja_extra.pagination import PageNumberPaginationExtra
from ninja_extra.permissions import IsAuthenticated from ninja_extra.permissions import IsAuthenticated
from ninja_extra.schemas import PaginatedResponseSchema
from pydantic import NonNegativeInt from pydantic import NonNegativeInt
from core.models import User from core.models import User
@ -15,10 +17,11 @@ from sas.schemas import PictureFilterSchema, PictureSchema
class PicturesController(ControllerBase): class PicturesController(ControllerBase):
@route.get( @route.get(
"", "",
response=list[PictureSchema], response=PaginatedResponseSchema[PictureSchema],
permissions=[IsAuthenticated], permissions=[IsAuthenticated],
url_name="pictures", url_name="pictures",
) )
@paginate(PageNumberPaginationExtra, page_size=100)
def fetch_pictures(self, filters: Query[PictureFilterSchema]): def fetch_pictures(self, filters: Query[PictureFilterSchema]):
"""Find pictures viewable by the user corresponding to the given filters. """Find pictures viewable by the user corresponding to the given filters.
@ -42,7 +45,7 @@ class PicturesController(ControllerBase):
# User can view any moderated picture if he/she is subscribed. # User can view any moderated picture if he/she is subscribed.
# If not, he/she can view only the one he/she has been identified on # If not, he/she can view only the one he/she has been identified on
raise PermissionDenied raise PermissionDenied
pictures = list( return (
filters.filter( filters.filter(
Picture.objects.filter(is_moderated=True, asked_for_removal=False) Picture.objects.filter(is_moderated=True, asked_for_removal=False)
) )
@ -50,11 +53,6 @@ class PicturesController(ControllerBase):
.order_by("-date") .order_by("-date")
.annotate(album=F("parent__name")) .annotate(album=F("parent__name"))
) )
for picture in pictures:
picture.full_size_url = picture.get_download_url()
picture.compressed_url = picture.get_download_compressed_url()
picture.thumb_url = picture.get_download_thumb_url()
return pictures
@api_controller("/sas/relation", tags="User identification on SAS pictures") @api_controller("/sas/relation", tags="User identification on SAS pictures")

View File

@ -3,7 +3,6 @@ from datetime import datetime
from ninja import FilterSchema, ModelSchema, Schema from ninja import FilterSchema, ModelSchema, Schema
from pydantic import Field, NonNegativeInt from pydantic import Field, NonNegativeInt
from core.schemas import SimpleUserSchema
from sas.models import PeoplePictureRelation, Picture from sas.models import PeoplePictureRelation, Picture
@ -17,14 +16,25 @@ class PictureFilterSchema(FilterSchema):
class PictureSchema(ModelSchema): class PictureSchema(ModelSchema):
class Meta: class Meta:
model = Picture model = Picture
fields = ["id", "name", "date", "size"] fields = ["id", "name", "date", "size", "is_moderated"]
author: SimpleUserSchema = Field(validation_alias="owner")
full_size_url: str full_size_url: str
compressed_url: str compressed_url: str
thumb_url: str thumb_url: str
album: str album: str
@staticmethod
def resolve_full_size_url(obj: Picture) -> str:
return obj.get_download_url()
@staticmethod
def resolve_compressed_url(obj: Picture) -> str:
return obj.get_download_compressed_url()
@staticmethod
def resolve_thumb_url(obj: Picture) -> str:
return obj.get_download_thumb_url()
class PictureCreateRelationSchema(Schema): class PictureCreateRelationSchema(Schema):
user_id: NonNegativeInt user_id: NonNegativeInt

View File

@ -44,12 +44,8 @@ class TestPictureSearch(TestSas):
self.client.force_login(self.user_b) self.client.force_login(self.user_b)
res = self.client.get(reverse("api:pictures") + f"?album_id={self.album_a.id}") res = self.client.get(reverse("api:pictures") + f"?album_id={self.album_a.id}")
assert res.status_code == 200 assert res.status_code == 200
expected = list( expected = list(self.album_a.children_pictures.values_list("id", flat=True))
self.album_a.children_pictures.order_by("-date").values_list( assert [i["id"] for i in res.json()["results"]] == expected
"id", flat=True
)
)
assert [i["id"] for i in res.json()] == expected
def test_filter_by_user(self): def test_filter_by_user(self):
self.client.force_login(self.user_b) self.client.force_login(self.user_b)
@ -58,11 +54,11 @@ class TestPictureSearch(TestSas):
) )
assert res.status_code == 200 assert res.status_code == 200
expected = list( expected = list(
self.user_a.pictures.order_by("-picture__date").values_list( self.user_a.pictures.order_by(
"picture_id", flat=True "-picture__parent__date", "picture__date"
) ).values_list("picture_id", flat=True)
) )
assert [i["id"] for i in res.json()] == expected assert [i["id"] for i in res.json()["results"]] == expected
def test_filter_by_multiple_user(self): def test_filter_by_multiple_user(self):
self.client.force_login(self.user_b) self.client.force_login(self.user_b)
@ -73,10 +69,10 @@ class TestPictureSearch(TestSas):
assert res.status_code == 200 assert res.status_code == 200
expected = list( expected = list(
self.user_a.pictures.union(self.user_b.pictures.all()) self.user_a.pictures.union(self.user_b.pictures.all())
.order_by("-picture__date") .order_by("-picture__parent__date", "picture__date")
.values_list("picture_id", flat=True) .values_list("picture_id", flat=True)
) )
assert [i["id"] for i in res.json()] == expected assert [i["id"] for i in res.json()["results"]] == expected
def test_not_subscribed_user(self): def test_not_subscribed_user(self):
"""Test that a user that is not subscribed can only its own pictures.""" """Test that a user that is not subscribed can only its own pictures."""
@ -86,11 +82,11 @@ class TestPictureSearch(TestSas):
) )
assert res.status_code == 200 assert res.status_code == 200
expected = list( expected = list(
self.user_a.pictures.order_by("-picture__date").values_list( self.user_a.pictures.order_by(
"picture_id", flat=True "-picture__parent__date", "picture__date"
) ).values_list("picture_id", flat=True)
) )
assert [i["id"] for i in res.json()] == expected assert [i["id"] for i in res.json()["results"]] == expected
# trying to access the pictures of someone else # trying to access the pictures of someone else
res = self.client.get( res = self.client.get(