Coverage for website/photos/views.py: 90.08%

109 statements  

« prev     ^ index     » next       coverage.py v7.6.7, created at 2025-08-14 10:31 +0000

1import os 

2from datetime import date 

3 

4from django.db.models import QuerySet 

5from django.contrib.auth.decorators import login_required 

6from django.contrib.auth.mixins import LoginRequiredMixin 

7from django.http import Http404 

8from django.http.request import HttpRequest as HttpRequest 

9from django.shortcuts import get_object_or_404, redirect 

10from django.views.generic import TemplateView 

11 

12from facedetection.models import ReferenceFace 

13from photos.models import Album, Photo 

14from photos.services import ( 

15 check_shared_album_token, 

16 get_annotated_accessible_albums, 

17 is_album_accessible, 

18) 

19from thaliawebsite.views import PagedView 

20from utils.media.services import fetch_thumbnails, get_media_url 

21from utils.snippets import datetime_to_lectureyear 

22from django.db.models import Q 

23 

24COVER_FILENAME = "cover.jpg" 

25 

26 

27class IndexView(LoginRequiredMixin, PagedView): 

28 model = Album 

29 paginate_by = 16 

30 template_name = "photos/index.html" 

31 context_object_name = "albums" 

32 keywords = None 

33 query_filter = "" 

34 year_range = [] 

35 

36 def setup(self, request, *args, **kwargs): 

37 super().setup(request, *args, **kwargs) 

38 current_lectureyear = datetime_to_lectureyear(date.today()) 

39 self.year_range = list( 

40 reversed(range(current_lectureyear - 5, current_lectureyear + 1)) 

41 ) 

42 self.keywords = request.GET.get("keywords", "").split() or None 

43 self.query_filter = kwargs.get("year", None) 

44 

45 def get_queryset(self) -> QuerySet: 

46 albums = Album.objects.filter(hidden=False, is_processing=False).select_related( 

47 "_cover" 

48 ) 

49 # We split on greater than the 7th month of the year (July), 

50 # to make sure the introduction week photos (from August) are the first on each year page. 

51 if self.query_filter == "older": 51 ↛ 52line 51 didn't jump to line 52 because the condition on line 51 was never true

52 albums = albums.filter( 

53 Q(date__year=self.year_range[-1], date__month__gt=7) 

54 | Q(date__year__lt=self.year_range[-1]) 

55 ) 

56 elif self.query_filter: 56 ↛ 57line 56 didn't jump to line 57 because the condition on line 56 was never true

57 albums = albums.filter( 

58 Q(date__year=self.query_filter, date__month__gt=7) 

59 | Q(date__year=int(self.query_filter) + 1, date__month__lt=8) 

60 ) 

61 if self.keywords: 

62 for key in self.keywords: 

63 albums = albums.filter(title__icontains=key) 

64 albums = get_annotated_accessible_albums(self.request, albums) 

65 albums = albums.order_by("-date") 

66 return albums 

67 

68 def get_context_data(self, **kwargs): 

69 context = super().get_context_data(**kwargs) 

70 context.update( 

71 { 

72 "filter": self.query_filter, 

73 "year_range": self.year_range, 

74 "keywords": self.keywords, 

75 } 

76 ) 

77 fetch_thumbnails([x.cover.file for x in context["object_list"] if x.cover]) 

78 

79 context["has_rejected_reference_faces"] = ( 

80 self.request.member.reference_faces.filter( 

81 status=ReferenceFace.Status.REJECTED, 

82 marked_for_deletion_at__isnull=True, 

83 ).exists() 

84 ) 

85 

86 context["has_reference_faces"] = self.request.member.reference_faces.filter( 

87 marked_for_deletion_at__isnull=True 

88 ).exists() 

89 return context 

90 

91 

92class _BaseAlbumView(TemplateView): 

93 template_name = "photos/album.html" 

94 

95 def get_album(self, **kwargs): 

96 raise NotImplementedError 

97 

98 def get_context_data(self, **kwargs): 

99 context = super().get_context_data(**kwargs) 

100 

101 album = self.get_album(**kwargs) 

102 

103 context["album"] = album 

104 photos = album.photo_set.select_properties("num_likes") 

105 

106 # Fix select_properties dropping the default ordering. 

107 photos = photos.order_by("pk") 

108 

109 # Prefetch thumbnails for efficiency 

110 fetch_thumbnails([p.file for p in photos]) 

111 

112 context["photos"] = photos 

113 return context 

114 

115 

116class AlbumDetailView(LoginRequiredMixin, _BaseAlbumView): 

117 """Render an album, if it is accessible by the user.""" 

118 

119 def get_album(self, **kwargs): 

120 slug = kwargs.get("slug") 

121 album = get_object_or_404( 

122 Album.objects.filter(hidden=False, is_processing=False), slug=slug 

123 ) 

124 

125 if not is_album_accessible(self.request, album): 

126 raise Http404("Sorry, you're not allowed to view this album") 

127 

128 return album 

129 

130 

131class SharedAlbumView(_BaseAlbumView): 

132 """Render a shared album if the correct token is provided.""" 

133 

134 def get_album(self, **kwargs): 

135 slug = kwargs.get("slug") 

136 token = kwargs.get("token") 

137 album = get_object_or_404( 

138 Album.objects.filter(hidden=False, is_processing=False), slug=slug 

139 ) 

140 

141 check_shared_album_token(album, token) 

142 

143 return album 

144 

145 

146def _photo_path(obj, filename): 

147 """Return the path to a Photo.""" 

148 photoname = os.path.basename(filename) 

149 albumpath = os.path.join(obj.photosdir, obj.dirname) 

150 photopath = os.path.join(albumpath, photoname) 

151 get_object_or_404(Photo.objects.filter(album=obj, file=photopath)) 

152 return photopath 

153 

154 

155def _download(request, obj, filename): 

156 """Download a photo. 

157 

158 This function provides a layer of indirection for shared albums. 

159 """ 

160 photopath = _photo_path(obj, filename) 

161 photo = get_object_or_404(Photo.objects.filter(album=obj, file=photopath)) 

162 return redirect(get_media_url(photo.file, attachment=f"{obj.slug}-{filename}")) 

163 

164 

165@login_required 

166def download(request, slug, filename): 

167 """Download a photo if the album of the photo is accessible by the user.""" 

168 obj = get_object_or_404(Album, slug=slug) 

169 if is_album_accessible(request, obj): 169 ↛ 171line 169 didn't jump to line 171 because the condition on line 169 was always true

170 return _download(request, obj, filename) 

171 raise Http404("Sorry, you're not allowed to view this album") 

172 

173 

174def shared_download(request, slug, token, filename): 

175 """Download a photo from a shared album if the album token is provided.""" 

176 obj = get_object_or_404(Album, slug=slug) 

177 check_shared_album_token(obj, token) 

178 return _download(request, obj, filename) 

179 

180 

181class LikedPhotoView(LoginRequiredMixin, PagedView): 

182 model = Photo 

183 paginate_by = 16 

184 template_name = "photos/liked-photos.html" 

185 context_object_name = "photos" 

186 

187 def get_queryset(self): 

188 photos = ( 

189 Photo.objects.filter( 

190 likes__member=self.request.member, 

191 album__hidden=False, 

192 album__is_processing=False, 

193 ) 

194 .select_related("album") 

195 .select_properties("num_likes") 

196 .order_by("-album__date") 

197 ) 

198 return photos 

199 

200 def get_context_data(self, **kwargs): 

201 context = super().get_context_data(**kwargs) 

202 

203 fetch_thumbnails([p.file for p in context["photos"]]) 

204 

205 return context