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
« prev ^ index » next coverage.py v7.6.7, created at 2025-08-14 10:31 +0000
1import os
2from datetime import date
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
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
24COVER_FILENAME = "cover.jpg"
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 = []
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)
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
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])
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 )
86 context["has_reference_faces"] = self.request.member.reference_faces.filter(
87 marked_for_deletion_at__isnull=True
88 ).exists()
89 return context
92class _BaseAlbumView(TemplateView):
93 template_name = "photos/album.html"
95 def get_album(self, **kwargs):
96 raise NotImplementedError
98 def get_context_data(self, **kwargs):
99 context = super().get_context_data(**kwargs)
101 album = self.get_album(**kwargs)
103 context["album"] = album
104 photos = album.photo_set.select_properties("num_likes")
106 # Fix select_properties dropping the default ordering.
107 photos = photos.order_by("pk")
109 # Prefetch thumbnails for efficiency
110 fetch_thumbnails([p.file for p in photos])
112 context["photos"] = photos
113 return context
116class AlbumDetailView(LoginRequiredMixin, _BaseAlbumView):
117 """Render an album, if it is accessible by the user."""
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 )
125 if not is_album_accessible(self.request, album):
126 raise Http404("Sorry, you're not allowed to view this album")
128 return album
131class SharedAlbumView(_BaseAlbumView):
132 """Render a shared album if the correct token is provided."""
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 )
141 check_shared_album_token(album, token)
143 return album
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
155def _download(request, obj, filename):
156 """Download a photo.
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}"))
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")
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)
181class LikedPhotoView(LoginRequiredMixin, PagedView):
182 model = Photo
183 paginate_by = 16
184 template_name = "photos/liked-photos.html"
185 context_object_name = "photos"
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
200 def get_context_data(self, **kwargs):
201 context = super().get_context_data(**kwargs)
203 fetch_thumbnails([p.file for p in context["photos"]])
205 return context