From d821dfa180f526ec7826cac0b81f4c20fbfec1b8 Mon Sep 17 00:00:00 2001 From: imperosol Date: Wed, 26 Mar 2025 15:06:41 +0100 Subject: [PATCH] add api endpoint to upload a sas picture --- core/utils.py | 14 +++++++ .../commands/generate_galaxy_test_data.py | 9 +---- sas/api.py | 40 ++++++++++++++++++- sas/tests/test_api.py | 27 ++++++++++++- 4 files changed, 79 insertions(+), 11 deletions(-) diff --git a/core/utils.py b/core/utils.py index 2f56cebf..d9e8f180 100644 --- a/core/utils.py +++ b/core/utils.py @@ -17,6 +17,7 @@ from datetime import date, timedelta # Image utils from io import BytesIO +from typing import Final import PIL from django.conf import settings @@ -26,6 +27,19 @@ from django.utils.timezone import localdate from PIL import ExifTags from PIL.Image import Image, Resampling +RED_PIXEL_PNG: Final[bytes] = ( + b"\x89\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52" + b"\x00\x00\x00\x01\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90\x77\x53" + b"\xde\x00\x00\x00\x0c\x49\x44\x41\x54\x08\xd7\x63\xf8\xcf\xc0\x00" + b"\x00\x03\x01\x01\x00\x18\xdd\x8d\xb0\x00\x00\x00\x00\x49\x45\x4e" + b"\x44\xae\x42\x60\x82" +) +"""A single red pixel, in PNG format. + +Can be used in tests and in dev, when there is a need +to generate a dummy image that is considered valid nonetheless +""" + def get_start_of_semester(today: date | None = None) -> date: """Return the date of the start of the semester of the given date. diff --git a/galaxy/management/commands/generate_galaxy_test_data.py b/galaxy/management/commands/generate_galaxy_test_data.py index d0dea4a5..563b35e6 100644 --- a/galaxy/management/commands/generate_galaxy_test_data.py +++ b/galaxy/management/commands/generate_galaxy_test_data.py @@ -32,17 +32,10 @@ from django.utils import timezone from club.models import Club, Membership from core.models import Group, Page, SithFile, User +from core.utils import RED_PIXEL_PNG from sas.models import Album, PeoplePictureRelation, Picture from subscription.models import Subscription -RED_PIXEL_PNG: Final[bytes] = ( - b"\x89\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52" - b"\x00\x00\x00\x01\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90\x77\x53" - b"\xde\x00\x00\x00\x0c\x49\x44\x41\x54\x08\xd7\x63\xf8\xcf\xc0\x00" - b"\x00\x03\x01\x01\x00\x18\xdd\x8d\xb0\x00\x00\x00\x00\x49\x45\x4e" - b"\x44\xae\x42\x60\x82" -) - USER_PACK_SIZE: Final[int] = 1000 diff --git a/sas/api.py b/sas/api.py index d9e2ad2e..2d6d64ef 100644 --- a/sas/api.py +++ b/sas/api.py @@ -1,7 +1,9 @@ from django.conf import settings +from django.core.exceptions import ValidationError from django.db.models import F from django.urls import reverse -from ninja import Query +from ninja import Body, Query, UploadedFile +from ninja.errors import HttpError from ninja_extra import ControllerBase, api_controller, paginate, route from ninja_extra.exceptions import NotFound, PermissionDenied from ninja_extra.pagination import PageNumberPaginationExtra @@ -9,7 +11,13 @@ from ninja_extra.permissions import IsAuthenticated from ninja_extra.schemas import PaginatedResponseSchema from pydantic import NonNegativeInt -from core.auth.api_permissions import CanAccessLookup, CanView, IsInGroup, IsRoot +from core.auth.api_permissions import ( + CanAccessLookup, + CanEdit, + CanView, + IsInGroup, + IsRoot, +) from core.models import Notification, User from sas.models import Album, PeoplePictureRelation, Picture from sas.schemas import ( @@ -92,6 +100,34 @@ class PicturesController(ControllerBase): .annotate(album=F("parent__name")) ) + @route.post( + "", + permissions=[CanEdit], + response={200: None, 409: dict[str, list[str]]}, + url_name="upload_picture", + ) + def upload_picture(self, album_id: Body[int], picture: UploadedFile): + album = self.get_object_or_exception(Album, pk=album_id) + user = self.context.request.user + self_moderate = user.has_perm("sas.moderate_sasfile") + new = Picture( + parent=album, + name=picture.name, + file=picture, + owner=user, + is_moderated=self_moderate, + is_folder=False, + mime_type=picture.content_type, + ) + if self_moderate: + new.moderator = user + try: + new.generate_thumbnails() + new.full_clean() + new.save() + except ValidationError as e: + raise HttpError(status_code=409, message=str(e)) from e + @route.get( "/{picture_id}/identified", permissions=[IsAuthenticated, CanView], diff --git a/sas/tests/test_api.py b/sas/tests/test_api.py index 25014e86..2570a3c6 100644 --- a/sas/tests/test_api.py +++ b/sas/tests/test_api.py @@ -1,13 +1,16 @@ +import pytest from django.conf import settings from django.core.cache import cache +from django.core.files.uploadedfile import SimpleUploadedFile from django.db import transaction -from django.test import TestCase +from django.test import Client, TestCase from django.urls import reverse from model_bakery import baker from model_bakery.recipe import Recipe from core.baker_recipes import old_subscriber_user, subscriber_user from core.models import Group, SithFile, User +from core.utils import RED_PIXEL_PNG from sas.baker_recipes import picture_recipe from sas.models import Album, PeoplePictureRelation, Picture, PictureModerationRequest @@ -241,3 +244,25 @@ class TestAlbumSearch(TestSas): # - 1 for pagination # - 1 for the actual results self.client.get(reverse("api:search-album")) + + +@pytest.mark.django_db +def test_upload_picture(client: Client): + sas = SithFile.objects.get(pk=settings.SITH_SAS_ROOT_DIR_ID) + album = baker.make(Album, is_in_sas=True, parent=sas, name="test album") + user = baker.make(User, is_superuser=True) + client.force_login(user) + img = SimpleUploadedFile( + name="img.png", content=RED_PIXEL_PNG, content_type="image/png" + ) + res = client.post( + reverse("api:upload_picture"), {"album_id": album.id, "picture": img} + ) + assert res.status_code == 200 + picture = Picture.objects.filter(parent_id=album.id).first() + assert picture is not None + assert picture.name == "img.png" + assert picture.owner == user + assert picture.file.name == "SAS/test album/img.png" + assert picture.compressed.name == ".compressed/SAS/test album/img.webp" + assert picture.thumbnail.name == ".thumbnails/SAS/test album/img.webp"