Sith/sas/api.py

159 lines
6.0 KiB
Python
Raw Permalink Normal View History

2024-10-21 08:30:35 +00:00
from typing import Annotated
from annotated_types import MinLen
from django.conf import settings
2024-07-31 09:56:38 +00:00
from django.db.models import F
from django.urls import reverse
2024-07-18 18:23:30 +00:00
from ninja import Query
2024-08-05 17:25:30 +00:00
from ninja_extra import ControllerBase, api_controller, paginate, route
from ninja_extra.exceptions import NotFound, PermissionDenied
2024-08-05 17:25:30 +00:00
from ninja_extra.pagination import PageNumberPaginationExtra
2024-07-18 18:23:30 +00:00
from ninja_extra.permissions import IsAuthenticated
2024-08-05 17:25:30 +00:00
from ninja_extra.schemas import PaginatedResponseSchema
from pydantic import NonNegativeInt
2024-07-18 18:23:30 +00:00
2024-10-21 08:30:35 +00:00
from core.api_permissions import CanAccessLookup, CanView, IsInGroup, IsRoot
from core.models import Notification, User
2024-10-21 08:30:35 +00:00
from sas.models import Album, PeoplePictureRelation, Picture
from sas.schemas import (
2024-10-21 08:30:35 +00:00
AlbumSchema,
IdentifiedUserSchema,
ModerationRequestSchema,
PictureFilterSchema,
PictureSchema,
)
IsSasAdmin = IsRoot | IsInGroup(settings.SITH_GROUP_SAS_ADMIN_ID)
2024-07-18 18:23:30 +00:00
2024-10-21 08:30:35 +00:00
@api_controller("/sas/album")
class AlbumController(ControllerBase):
@route.get(
"/search",
response=PaginatedResponseSchema[AlbumSchema],
permissions=[CanAccessLookup],
)
@paginate(PageNumberPaginationExtra, page_size=50)
def search_album(self, search: Annotated[str, MinLen(1)]):
return Album.objects.filter(name__icontains=search)
@api_controller("/sas/picture")
class PicturesController(ControllerBase):
2024-07-18 18:23:30 +00:00
@route.get(
"",
2024-08-05 17:25:30 +00:00
response=PaginatedResponseSchema[PictureSchema],
2024-07-18 18:23:30 +00:00
permissions=[IsAuthenticated],
url_name="pictures",
)
2024-08-05 17:25:30 +00:00
@paginate(PageNumberPaginationExtra, page_size=100)
2024-07-18 18:23:30 +00:00
def fetch_pictures(self, filters: Query[PictureFilterSchema]):
"""Find pictures viewable by the user corresponding to the given filters.
A user with an active subscription can see any picture, as long
as it has been moderated and not asked for removal.
An unsubscribed user can see the pictures he has been identified on
2024-07-26 12:25:26 +00:00
(only the moderated ones, too).
2024-07-18 18:23:30 +00:00
Notes:
Trying to fetch the pictures of another user with this route
while being unsubscribed will just result in an empty response.
2024-07-26 12:25:26 +00:00
Notes:
Unsubscribed users who are identified is not a rare case.
They can be UTT students, faluchards from other schools,
or even Richard Stallman (that ain't no joke,
cf. https://ae.utbm.fr/user/32663/pictures/)
2024-07-18 18:23:30 +00:00
"""
user: User = self.context.request.user
return (
filters.filter(Picture.objects.viewable_by(user))
2024-07-22 17:12:03 +00:00
.distinct()
2024-08-05 21:40:11 +00:00
.order_by("-parent__date", "date")
2024-09-03 18:15:37 +00:00
.select_related("owner")
2024-07-31 09:56:38 +00:00
.annotate(album=F("parent__name"))
2024-07-18 18:23:30 +00:00
)
@route.get(
"/{picture_id}/identified",
permissions=[IsAuthenticated, CanView],
response=list[IdentifiedUserSchema],
)
def fetch_identifications(self, picture_id: int):
"""Fetch the users that have been identified on the given picture."""
picture = self.get_object_or_exception(Picture, pk=picture_id)
return picture.people.select_related("user")
@route.put("/{picture_id}/identified", permissions=[IsAuthenticated, CanView])
def identify_users(self, picture_id: NonNegativeInt, users: set[NonNegativeInt]):
picture = self.get_object_or_exception(Picture, pk=picture_id)
db_users = list(User.objects.filter(id__in=users))
if len(users) != len(db_users):
raise NotFound
already_identified = set(
picture.people.filter(user_id__in=users).values_list("user_id", flat=True)
)
identified = [u for u in db_users if u.pk not in already_identified]
relations = [
PeoplePictureRelation(user=u, picture_id=picture_id) for u in identified
]
PeoplePictureRelation.objects.bulk_create(relations)
for u in identified:
Notification.objects.get_or_create(
user=u,
viewed=False,
type="NEW_PICTURES",
defaults={
"url": reverse("core:user_pictures", kwargs={"user_id": u.id})
},
)
@route.delete("/{picture_id}", permissions=[IsSasAdmin])
2024-09-03 18:15:37 +00:00
def delete_picture(self, picture_id: int):
self.get_object_or_exception(Picture, pk=picture_id).delete()
@route.patch(
"/{picture_id}/moderation",
permissions=[IsSasAdmin],
url_name="picture_moderate",
)
2024-09-03 18:15:37 +00:00
def moderate_picture(self, picture_id: int):
"""Mark a picture as moderated and remove its pending moderation requests."""
2024-09-03 18:15:37 +00:00
picture = self.get_object_or_exception(Picture, pk=picture_id)
picture.moderation_requests.all().delete()
2024-09-03 18:15:37 +00:00
picture.is_moderated = True
picture.moderator = self.context.request.user
picture.asked_for_removal = False
picture.save()
@route.get(
"/{picture_id}/moderation",
permissions=[IsSasAdmin],
response=list[ModerationRequestSchema],
url_name="picture_moderation_requests",
)
def fetch_moderation_requests(self, picture_id: int):
"""Fetch the moderation requests issued on this picture."""
picture = self.get_object_or_exception(Picture, pk=picture_id)
return picture.moderation_requests.select_related("author")
2024-07-26 12:25:26 +00:00
@api_controller("/sas/relation", tags="User identification on SAS pictures")
class UsersIdentifiedController(ControllerBase):
2024-07-26 12:25:26 +00:00
@route.delete("/{relation_id}", permissions=[IsAuthenticated])
def delete_relation(self, relation_id: NonNegativeInt):
2024-07-26 12:25:26 +00:00
"""Untag a user from a SAS picture.
Root and SAS admins can delete any picture identification.
All other users can delete their own identification.
"""
relation = self.get_object_or_exception(PeoplePictureRelation, pk=relation_id)
user: User = self.context.request.user
if (
relation.user_id != user.id
and not user.is_root
and not user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID)
):
raise PermissionDenied
relation.delete()