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

1from django.db.models import Count, Prefetch, Q 

2 

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 

9 

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 

18 

19 

20class AlbumListView(ListAPIView): 

21 """Returns an overview of all albums.""" 

22 

23 serializer_class = AlbumListSerializer 

24 

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) 

30 

31 queryset = Album.objects.filter(hidden=False, is_processing=False).select_related( 

32 "_cover" 

33 ) 

34 

35 permission_classes = [ 

36 IsAuthenticatedOrTokenHasScope, 

37 ] 

38 required_scopes = ["photos:read"] 

39 filter_backends = (filters.SearchFilter,) 

40 search_fields = ("title", "date", "slug") 

41 

42 

43class AlbumDetailView(RetrieveAPIView): 

44 """Returns the details of an album.""" 

45 

46 serializer_class = AlbumSerializer 

47 permission_classes = [ 

48 IsAuthenticatedOrTokenHasScope, 

49 ] 

50 required_scopes = ["photos:read"] 

51 lookup_field = "slug" 

52 

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) 

57 

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 

62 

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 ) 

69 

70 # Fix select_properties dropping the default ordering. 

71 photos = photos.order_by("pk") 

72 

73 return Album.objects.filter(hidden=False, is_processing=False).prefetch_related( 

74 Prefetch("photo_set", queryset=photos) 

75 ) 

76 

77 

78class LikedPhotosListView(ListAPIView): 

79 """Returns the details the liked album.""" 

80 

81 serializer_class = PhotoListSerializer 

82 permission_classes = [ 

83 IsAuthenticatedOrTokenHasScope, 

84 ] 

85 required_scopes = ["photos:read"] 

86 

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) 

96 

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) 

102 

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 ) 

117 

118 

119class PhotoLikeView(APIView): 

120 permission_classes = [IsAuthenticatedOrTokenHasScope] 

121 required_scopes = ["photos:read"] 

122 

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) 

131 

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 ) 

139 

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) 

148 

149 _, created = Like.objects.get_or_create(photo=photo, member=request.member) 

150 

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 ) 

166 

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) 

175 

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 ) 

188 

189 like.delete() 

190 

191 return Response( 

192 { 

193 "liked": False, 

194 "num_likes": photo.num_likes, 

195 }, 

196 status=status.HTTP_202_ACCEPTED, 

197 )