Coverage for website/photos/api/v2/views.py: 39.60%
89 statements
« prev ^ index » next coverage.py v7.6.7, created at 2025-08-14 10:31 +0000
« prev ^ index » next coverage.py v7.6.7, created at 2025-08-14 10:31 +0000
1from django.db.models import Count, Prefetch, Q
3from oauth2_provider.contrib.rest_framework import IsAuthenticatedOrTokenHasScope
4from rest_framework import filters, status
5from rest_framework.exceptions import PermissionDenied
6from rest_framework.generics import ListAPIView, RetrieveAPIView
7from rest_framework.response import Response
8from rest_framework.views import APIView
10from photos import services
11from photos.api.v2.serializers.album import (
12 AlbumListSerializer,
13 AlbumSerializer,
14 PhotoListSerializer,
15)
16from photos.models import Album, Like, Photo
17from utils.media.services import fetch_thumbnails
20class AlbumListView(ListAPIView):
21 """Returns an overview of all albums."""
23 serializer_class = AlbumListSerializer
25 def get_serializer(self, *args, **kwargs):
26 if len(args) > 0:
27 albums = args[0]
28 fetch_thumbnails([album.cover.file for album in albums if album.cover])
29 return super().get_serializer(*args, **kwargs)
31 queryset = Album.objects.filter(hidden=False, is_processing=False).select_related(
32 "_cover"
33 )
35 permission_classes = [
36 IsAuthenticatedOrTokenHasScope,
37 ]
38 required_scopes = ["photos:read"]
39 filter_backends = (filters.SearchFilter,)
40 search_fields = ("title", "date", "slug")
43class AlbumDetailView(RetrieveAPIView):
44 """Returns the details of an album."""
46 serializer_class = AlbumSerializer
47 permission_classes = [
48 IsAuthenticatedOrTokenHasScope,
49 ]
50 required_scopes = ["photos:read"]
51 lookup_field = "slug"
53 def retrieve(self, request, *args, **kwargs):
54 if not services.is_album_accessible(request, self.get_object()):
55 raise PermissionDenied
56 return super().retrieve(request, *args, **kwargs)
58 def get_object(self):
59 object = super().get_object()
60 fetch_thumbnails([photo.file for photo in object.photo_set.all()])
61 return object
63 def get_queryset(self):
64 photos = Photo.objects.select_properties("num_likes")
65 if self.request.member:
66 photos = photos.annotate(
67 member_likes=Count("likes", filter=Q(likes__member=self.request.member))
68 )
70 # Fix select_properties dropping the default ordering.
71 photos = photos.order_by("pk")
73 return Album.objects.filter(hidden=False, is_processing=False).prefetch_related(
74 Prefetch("photo_set", queryset=photos)
75 )
78class LikedPhotosListView(ListAPIView):
79 """Returns the details the liked album."""
81 serializer_class = PhotoListSerializer
82 permission_classes = [
83 IsAuthenticatedOrTokenHasScope,
84 ]
85 required_scopes = ["photos:read"]
87 def get(self, request, *args, **kwargs):
88 if not self.request.member:
89 return Response(
90 data={
91 "detail": "You need to be a member in order to view your liked photos."
92 },
93 status=status.HTTP_403_FORBIDDEN,
94 )
95 return self.list(request, *args, **kwargs)
97 def get_serializer(self, *args, **kwargs):
98 if len(args) > 0:
99 photos = args[0]
100 fetch_thumbnails([photo.file for photo in photos])
101 return super().get_serializer(*args, **kwargs)
103 def get_queryset(self):
104 return (
105 Photo.objects.filter(
106 likes__member=self.request.member,
107 album__hidden=False,
108 album__is_processing=False,
109 )
110 .annotate(
111 member_likes=Count("likes", filter=Q(likes__member=self.request.member))
112 )
113 .select_properties("num_likes")
114 # Fix select_properties dropping the default ordering.
115 .order_by("pk")
116 )
119class PhotoLikeView(APIView):
120 permission_classes = [IsAuthenticatedOrTokenHasScope]
121 required_scopes = ["photos:read"]
123 def get(self, request, **kwargs):
124 photo_id = kwargs.get("pk")
125 try:
126 photo = Photo.objects.filter(
127 album__hidden=False, album__is_processing=False
128 ).get(pk=photo_id)
129 except Photo.DoesNotExist:
130 return Response(status=status.HTTP_404_NOT_FOUND)
132 return Response(
133 {
134 "liked": photo.likes.filter(member=request.member).exists(),
135 "num_likes": photo.num_likes,
136 },
137 status=status.HTTP_200_OK,
138 )
140 def post(self, request, **kwargs):
141 photo_id = kwargs.get("pk")
142 try:
143 photo = Photo.objects.filter(
144 album__hidden=False, album__is_processing=False
145 ).get(pk=photo_id)
146 except Photo.DoesNotExist:
147 return Response(status=status.HTTP_404_NOT_FOUND)
149 _, created = Like.objects.get_or_create(photo=photo, member=request.member)
151 if created:
152 return Response(
153 {
154 "liked": photo.likes.filter(member=request.member).exists(),
155 "num_likes": photo.num_likes,
156 },
157 status=status.HTTP_201_CREATED,
158 )
159 return Response(
160 {
161 "liked": photo.likes.filter(member=request.member).exists(),
162 "num_likes": photo.num_likes,
163 },
164 status=status.HTTP_200_OK,
165 )
167 def delete(self, request, **kwargs):
168 photo_id = kwargs.get("pk")
169 try:
170 photo = Photo.objects.filter(
171 album__hidden=False, album__is_processing=False
172 ).get(pk=photo_id)
173 except Photo.DoesNotExist:
174 return Response(status=status.HTTP_404_NOT_FOUND)
176 try:
177 like = Like.objects.filter(
178 photo__album__hidden=False, photo__album__is_processing=False
179 ).get(member=request.member, photo__pk=photo_id)
180 except Like.DoesNotExist:
181 return Response(
182 {
183 "liked": False,
184 "num_likes": photo.num_likes,
185 },
186 status=status.HTTP_204_NO_CONTENT,
187 )
189 like.delete()
191 return Response(
192 {
193 "liked": False,
194 "num_likes": photo.num_likes,
195 },
196 status=status.HTTP_202_ACCEPTED,
197 )