mirror of
				https://github.com/ae-utbm/sith.git
				synced 2025-10-31 00:53:08 +00:00 
			
		
		
		
	| @@ -982,7 +982,7 @@ class SithFile(models.Model): | ||||
|             return True | ||||
|         if self.is_in_sas and user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID): | ||||
|             return True | ||||
|         return user.id == self.owner.id | ||||
|         return user.id == self.owner_id | ||||
|  | ||||
|     def can_be_viewed_by(self, user): | ||||
|         if hasattr(self, "profile_of"): | ||||
|   | ||||
| @@ -65,3 +65,21 @@ function display_notif() { | ||||
| function getCSRFToken() { | ||||
|     return $("[name=csrfmiddlewaretoken]").val(); | ||||
| } | ||||
|  | ||||
|  | ||||
| const initialUrlParams = new URLSearchParams(window.location.search); | ||||
|  | ||||
| function update_query_string(key, value) { | ||||
|     const url = new URL(window.location.href); | ||||
|     if (!value) { | ||||
|         // If the value is null, undefined or empty => delete it | ||||
|         url.searchParams.delete(key) | ||||
|     } else if (Array.isArray(value)) { | ||||
|  | ||||
|         url.searchParams.delete(key) | ||||
|         value.forEach((v) => url.searchParams.append(key, v)) | ||||
|     } else { | ||||
|         url.searchParams.set(key, value); | ||||
|     } | ||||
|     history.pushState(null, document.title, url.toString()); | ||||
| } | ||||
|   | ||||
| @@ -3,6 +3,7 @@ | ||||
| .pagination { | ||||
|   text-align: center; | ||||
|   gap: 10px; | ||||
|   margin: 30px; | ||||
|  | ||||
|   button { | ||||
|     background-color: $secondary-neutral-light-color; | ||||
|   | ||||
| @@ -93,6 +93,32 @@ a:not(.button) { | ||||
|   } | ||||
| } | ||||
|  | ||||
| [aria-busy] { | ||||
|   --loading-size: 50px; | ||||
|   --loading-stroke: 5px; | ||||
|   --loading-duration: 1s; | ||||
|   position: relative; | ||||
| } | ||||
|  | ||||
| [aria-busy]:after { | ||||
|   content: ''; | ||||
|   position: absolute; | ||||
|   top: 50%; | ||||
|   left: 50%; | ||||
|   width: var(--loading-size); | ||||
|   height: var(--loading-size); | ||||
|   margin-top: calc(var(--loading-size) / 2 * -1); | ||||
|   margin-left: calc(var(--loading-size) / 2 * -1); | ||||
|   border: var(--loading-stroke) solid rgba(0, 0, 0, .15); | ||||
|   border-radius: 50%; | ||||
|   border-top-color: rgba(0, 0, 0, 0.5); | ||||
|   animation: rotate calc(var(--loading-duration)) linear infinite; | ||||
| } | ||||
|  | ||||
| @keyframes rotate { | ||||
|   100% { transform: rotate(360deg); } | ||||
| } | ||||
|  | ||||
| .ib { | ||||
|   display: inline-block; | ||||
|   padding: 1px; | ||||
|   | ||||
| @@ -102,20 +102,10 @@ main { | ||||
|   border-radius: 10px; | ||||
| } | ||||
|  | ||||
| .paginator { | ||||
|   display: flex; | ||||
|   justify-content: center; | ||||
|   gap: 10px; | ||||
|   width: -moz-fit-content; | ||||
|   width: fit-content; | ||||
|   background-color: rgba(0,0,0,.1); | ||||
|   border-radius: 10px; | ||||
|   padding: 10px; | ||||
|   margin: 10px 0 10px auto; | ||||
| } | ||||
|  | ||||
| .photos, | ||||
| .albums { | ||||
|   margin: 20px; | ||||
|   min-height: 50px;  // To contain the aria-busy loading wheel, even if empty | ||||
|   box-sizing: border-box; | ||||
|   display: flex; | ||||
|   flex-direction: row; | ||||
| @@ -161,17 +151,13 @@ main { | ||||
|     > .album { | ||||
|       box-sizing: border-box; | ||||
|       background-color: #333333; | ||||
|       background-size: cover; | ||||
|       background-size: contain; | ||||
|       background-repeat: no-repeat; | ||||
|       background-position: center center; | ||||
|  | ||||
|       width: calc(16 / 9 * 128px); | ||||
|       height: 128px; | ||||
|  | ||||
|       &.vertical { | ||||
|         background-size: contain; | ||||
|       } | ||||
|  | ||||
|       margin: 0; | ||||
|       padding: 0; | ||||
|       box-shadow: none; | ||||
|   | ||||
| @@ -23,7 +23,7 @@ | ||||
|         <button | ||||
|           :disabled="in_progress" | ||||
|           class="btn btn-blue" | ||||
|           @click="download('{{ url("api:pictures") }}?users_identified={{ object.id }}')" | ||||
|           @click="download_zip()" | ||||
|         > | ||||
|           <i class="fa fa-download"></i> | ||||
|           {% trans %}Download all my pictures{% endtrans %} | ||||
| @@ -86,13 +86,34 @@ | ||||
|         Alpine.data("picture_download", () => ({ | ||||
|           in_progress: false, | ||||
|  | ||||
|           async download(url) { | ||||
|           /** | ||||
|            * @return {Promise<Picture[]>} | ||||
|            */ | ||||
|           async get_pictures() { | ||||
|             {# The API forbids to get more than 199 items at once | ||||
|              from paginated routes. | ||||
|              In order to download all the user pictures, it may be needed | ||||
|              to performs multiple requests #} | ||||
|             const max_per_page = 1; | ||||
|             const url = "{{ url("api:pictures") }}" | ||||
|             + "?users_identified={{ object.id }}" | ||||
|             + `&page_size=${max_per_page}`; | ||||
|             let promises = []; | ||||
|             const nb_pages = Math.ceil({{ nb_pictures }} / max_per_page); | ||||
|             for (let i = 1; i <= nb_pages; i++) { | ||||
|               promises.push( | ||||
|                 fetch(url + `&page=${i}`).then(res => res.json().then(json => json.results)) | ||||
|               ); | ||||
|             } | ||||
|             return (await Promise.all(promises)).flat() | ||||
|           }, | ||||
|  | ||||
|  | ||||
|           async download_zip(){ | ||||
|             this.in_progress = true; | ||||
|             const bar = this.$refs.progress; | ||||
|             bar.value = 0; | ||||
|  | ||||
|             /** @type Picture[] */ | ||||
|             const pictures = await (await fetch(url)).json(); | ||||
|             const pictures = await this.get_pictures(); | ||||
|             bar.max = pictures.length; | ||||
|  | ||||
|             const fileHandle = await window.showSaveFilePicker({ | ||||
|   | ||||
| @@ -96,10 +96,7 @@ def get_semester_code(d: Optional[date] = None) -> str: | ||||
|  | ||||
|  | ||||
| def scale_dimension(width, height, long_edge): | ||||
|     if width > height: | ||||
|         ratio = long_edge * 1.0 / width | ||||
|     else: | ||||
|         ratio = long_edge * 1.0 / height | ||||
|     ratio = long_edge / max(width, height) | ||||
|     return int(width * ratio), int(height * ratio) | ||||
|  | ||||
|  | ||||
| @@ -107,8 +104,8 @@ def resize_image(im, edge, img_format): | ||||
|     (w, h) = im.size | ||||
|     (width, height) = scale_dimension(w, h, long_edge=edge) | ||||
|     content = BytesIO() | ||||
|     # use the lanczos filter for antialiasing | ||||
|     im = im.resize((width, height), Resampling.LANCZOS) | ||||
|     # use the lanczos filter for antialiasing and discard the alpha channel | ||||
|     im = im.resize((width, height), Resampling.LANCZOS).convert("RGB") | ||||
|     try: | ||||
|         im.save( | ||||
|             fp=content, | ||||
|   | ||||
| @@ -319,6 +319,7 @@ class UserPicturesView(UserTabsMixin, CanViewMixin, DetailView): | ||||
|             .order_by("-parent__date", "-date") | ||||
|             .annotate(album=F("parent__name")) | ||||
|         ) | ||||
|         kwargs["nb_pictures"] = len(pictures) | ||||
|         kwargs["albums"] = { | ||||
|             album: list(picts) | ||||
|             for album, picts in itertools.groupby(pictures, lambda i: i.album) | ||||
|   | ||||
| @@ -96,7 +96,7 @@ | ||||
|           {% endif %} | ||||
|         </tr> | ||||
|       </thead> | ||||
|       <tbody id="dynamic_view_content"> | ||||
|       <tbody id="dynamic_view_content" :aria-busy="loading"> | ||||
|         <template x-for="uv in uvs.results" :key="uv.id"> | ||||
|           <tr @click="window.location.href = `/pedagogy/uv/${uv.id}`" class="clickable"> | ||||
|             <td><a :href="`/pedagogy/uv/${uv.id}`" x-text="uv.code"></a></td> | ||||
| @@ -126,22 +126,6 @@ | ||||
|     </nav> | ||||
|   </div> | ||||
|   <script> | ||||
|     const initialUrlParams = new URLSearchParams(window.location.search); | ||||
|  | ||||
|     function update_query_string(key, value) { | ||||
|       const url = new URL(window.location.href); | ||||
|       if (!value) { | ||||
|             {# If the value is null, undefined or empty => delete it #} | ||||
|         url.searchParams.delete(key) | ||||
|       } else if (Array.isArray(value)) { | ||||
|         url.searchParams.delete(key) | ||||
|         value.forEach((v) => url.searchParams.append(key, v)) | ||||
|       } else { | ||||
|         url.searchParams.set(key, value); | ||||
|       } | ||||
|       history.pushState(null, document.title, url.toString()); | ||||
|     } | ||||
|  | ||||
|     {# | ||||
|     How does this work : | ||||
|  | ||||
| @@ -156,6 +140,7 @@ | ||||
|     document.addEventListener("alpine:init", () => { | ||||
|       Alpine.data("uv_search", () => ({ | ||||
|         uvs: [], | ||||
|         loading: false, | ||||
|         page: parseInt(initialUrlParams.get("page")) || page_default, | ||||
|         page_size: parseInt(initialUrlParams.get("page_size")) || page_size_default, | ||||
|         search: initialUrlParams.get("search") || "", | ||||
| @@ -187,8 +172,10 @@ | ||||
|         }, | ||||
|  | ||||
|         async fetch_data() { | ||||
|           this.loading = true; | ||||
|           const url = "{{ url("api:fetch_uvs") }}" + window.location.search; | ||||
|           this.uvs = await (await fetch(url)).json(); | ||||
|           this.loading = false; | ||||
|         }, | ||||
|  | ||||
|         max_page() { | ||||
|   | ||||
							
								
								
									
										24
									
								
								sas/api.py
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								sas/api.py
									
									
									
									
									
								
							| @@ -1,9 +1,11 @@ | ||||
| from django.conf import settings | ||||
| from django.db.models import F | ||||
| from ninja import Query | ||||
| from ninja_extra import ControllerBase, api_controller, route | ||||
| from ninja_extra import ControllerBase, api_controller, paginate, route | ||||
| from ninja_extra.exceptions import PermissionDenied | ||||
| from ninja_extra.pagination import PageNumberPaginationExtra | ||||
| from ninja_extra.permissions import IsAuthenticated | ||||
| from ninja_extra.schemas import PaginatedResponseSchema | ||||
| from pydantic import NonNegativeInt | ||||
|  | ||||
| from core.models import User | ||||
| @@ -15,10 +17,11 @@ from sas.schemas import PictureFilterSchema, PictureSchema | ||||
| class PicturesController(ControllerBase): | ||||
|     @route.get( | ||||
|         "", | ||||
|         response=list[PictureSchema], | ||||
|         response=PaginatedResponseSchema[PictureSchema], | ||||
|         permissions=[IsAuthenticated], | ||||
|         url_name="pictures", | ||||
|     ) | ||||
|     @paginate(PageNumberPaginationExtra, page_size=100) | ||||
|     def fetch_pictures(self, filters: Query[PictureFilterSchema]): | ||||
|         """Find pictures viewable by the user corresponding to the given filters. | ||||
|  | ||||
| @@ -38,23 +41,12 @@ class PicturesController(ControllerBase): | ||||
|             cf. https://ae.utbm.fr/user/32663/pictures/) | ||||
|         """ | ||||
|         user: User = self.context.request.user | ||||
|         if not user.is_subscribed and filters.users_identified != {user.id}: | ||||
|             # User can view any moderated picture if he/she is subscribed. | ||||
|             # If not, he/she can view only the one he/she has been identified on | ||||
|             raise PermissionDenied | ||||
|         pictures = list( | ||||
|             filters.filter( | ||||
|                 Picture.objects.filter(is_moderated=True, asked_for_removal=False) | ||||
|             ) | ||||
|         return ( | ||||
|             filters.filter(Picture.objects.viewable_by(user)) | ||||
|             .distinct() | ||||
|             .order_by("-date") | ||||
|             .order_by("-parent__date", "date") | ||||
|             .annotate(album=F("parent__name")) | ||||
|         ) | ||||
|         for picture in pictures: | ||||
|             picture.full_size_url = picture.get_download_url() | ||||
|             picture.compressed_url = picture.get_download_compressed_url() | ||||
|             picture.thumb_url = picture.get_download_thumb_url() | ||||
|         return pictures | ||||
|  | ||||
|  | ||||
| @api_controller("/sas/relation", tags="User identification on SAS pictures") | ||||
|   | ||||
							
								
								
									
										135
									
								
								sas/models.py
									
									
									
									
									
								
							
							
						
						
									
										135
									
								
								sas/models.py
									
									
									
									
									
								
							| @@ -13,11 +13,14 @@ | ||||
| # | ||||
| # | ||||
|  | ||||
| from __future__ import annotations | ||||
|  | ||||
| from io import BytesIO | ||||
|  | ||||
| from django.conf import settings | ||||
| from django.core.cache import cache | ||||
| from django.db import models | ||||
| from django.db.models import Exists, OuterRef | ||||
| from django.urls import reverse | ||||
| from django.utils import timezone | ||||
| from django.utils.translation import gettext_lazy as _ | ||||
| @@ -27,21 +30,60 @@ from core.models import SithFile, User | ||||
| from core.utils import exif_auto_rotate, resize_image | ||||
|  | ||||
|  | ||||
| class SasFile(SithFile): | ||||
|     """Proxy model for any file in the SAS. | ||||
|  | ||||
|     May be used to have logic that should be shared by both | ||||
|     [Picture][sas.models.Picture] and [Album][sas.models.Album]. | ||||
|     """ | ||||
|  | ||||
|     class Meta: | ||||
|         proxy = True | ||||
|  | ||||
|     def can_be_viewed_by(self, user): | ||||
|         if user.is_anonymous: | ||||
|             return False | ||||
|         cache_key = ( | ||||
|             f"sas:{self._meta.model_name}_viewable_by_{user.id}_in_{self.parent_id}" | ||||
|         ) | ||||
|         viewable: list[int] | None = cache.get(cache_key) | ||||
|         if viewable is None: | ||||
|             viewable = list( | ||||
|                 self.__class__.objects.filter(parent_id=self.parent_id) | ||||
|                 .viewable_by(user) | ||||
|                 .values_list("pk", flat=True) | ||||
|             ) | ||||
|             cache.set(cache_key, viewable, timeout=10) | ||||
|         return self.id in viewable | ||||
|  | ||||
|     def can_be_edited_by(self, user): | ||||
|         return user.is_root or user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID) | ||||
|  | ||||
|  | ||||
| class PictureQuerySet(models.QuerySet): | ||||
|     def viewable_by(self, user: User) -> PictureQuerySet: | ||||
|         """Filter the pictures that this user can view. | ||||
|  | ||||
|         Warnings: | ||||
|             Calling this queryset method may add several additional requests. | ||||
|         """ | ||||
|         if user.is_root or user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID): | ||||
|             return self.all() | ||||
|         if user.was_subscribed: | ||||
|             return self.filter(is_moderated=True) | ||||
|         return self.filter(people__user_id=user.id, is_moderated=True) | ||||
|  | ||||
|  | ||||
| class SASPictureManager(models.Manager): | ||||
|     def get_queryset(self): | ||||
|         return super().get_queryset().filter(is_in_sas=True, is_folder=False) | ||||
|  | ||||
|  | ||||
| class SASAlbumManager(models.Manager): | ||||
|     def get_queryset(self): | ||||
|         return super().get_queryset().filter(is_in_sas=True, is_folder=True) | ||||
|  | ||||
|  | ||||
| class Picture(SithFile): | ||||
| class Picture(SasFile): | ||||
|     class Meta: | ||||
|         proxy = True | ||||
|  | ||||
|     objects = SASPictureManager() | ||||
|     objects = SASPictureManager.from_queryset(PictureQuerySet)() | ||||
|  | ||||
|     @property | ||||
|     def is_vertical(self): | ||||
| @@ -50,29 +92,6 @@ class Picture(SithFile): | ||||
|             (w, h) = im.size | ||||
|             return (w / h) < 1 | ||||
|  | ||||
|     def can_be_edited_by(self, user): | ||||
|         perm = cache.get("%d_can_edit_pictures" % (user.id), None) | ||||
|         if perm is None: | ||||
|             perm = user.is_root or user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID) | ||||
|  | ||||
|         cache.set("%d_can_edit_pictures" % (user.id), perm, timeout=4) | ||||
|         return perm | ||||
|  | ||||
|     def can_be_viewed_by(self, user): | ||||
|         # SAS pictures are visible to old subscribers | ||||
|         # Result is cached 4s for this user | ||||
|         if user.is_anonymous: | ||||
|             return False | ||||
|  | ||||
|         perm = cache.get("%d_can_view_pictures" % (user.id), False) | ||||
|         if not perm: | ||||
|             perm = user.was_subscribed | ||||
|  | ||||
|         cache.set("%d_can_view_pictures" % (user.id), perm, timeout=4) | ||||
|         return (perm and self.is_moderated and self.is_in_sas) or self.can_be_edited_by( | ||||
|             user | ||||
|         ) | ||||
|  | ||||
|     def get_download_url(self): | ||||
|         return reverse("sas:download", kwargs={"picture_id": self.id}) | ||||
|  | ||||
| @@ -124,48 +143,53 @@ class Picture(SithFile): | ||||
|  | ||||
|     def get_next(self): | ||||
|         if self.is_moderated: | ||||
|             return ( | ||||
|                 self.parent.children.filter( | ||||
|             pictures_qs = self.parent.children.filter( | ||||
|                 is_moderated=True, | ||||
|                 asked_for_removal=False, | ||||
|                 is_folder=False, | ||||
|                 id__gt=self.id, | ||||
|             ) | ||||
|                 .order_by("id") | ||||
|                 .first() | ||||
|             ) | ||||
|         else: | ||||
|             return ( | ||||
|                 Picture.objects.filter(id__gt=self.id, is_moderated=False) | ||||
|                 .order_by("id") | ||||
|                 .first() | ||||
|             ) | ||||
|             pictures_qs = Picture.objects.filter(id__gt=self.id, is_moderated=False) | ||||
|         return pictures_qs.order_by("id").first() | ||||
|  | ||||
|     def get_previous(self): | ||||
|         if self.is_moderated: | ||||
|             return ( | ||||
|                 self.parent.children.filter( | ||||
|             pictures_qs = self.parent.children.filter( | ||||
|                 is_moderated=True, | ||||
|                 asked_for_removal=False, | ||||
|                 is_folder=False, | ||||
|                 id__lt=self.id, | ||||
|             ) | ||||
|                 .order_by("id") | ||||
|                 .last() | ||||
|             ) | ||||
|         else: | ||||
|             return ( | ||||
|                 Picture.objects.filter(id__lt=self.id, is_moderated=False) | ||||
|                 .order_by("-id") | ||||
|                 .first() | ||||
|             pictures_qs = Picture.objects.filter(id__lt=self.id, is_moderated=False) | ||||
|         return pictures_qs.order_by("-id").first() | ||||
|  | ||||
|  | ||||
| class AlbumQuerySet(models.QuerySet): | ||||
|     def viewable_by(self, user: User) -> PictureQuerySet: | ||||
|         """Filter the albums that this user can view. | ||||
|  | ||||
|         Warnings: | ||||
|             Calling this queryset method may add several additional requests. | ||||
|         """ | ||||
|         if user.is_root or user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID): | ||||
|             return self.all() | ||||
|         return self.filter( | ||||
|             Exists(Picture.objects.filter(parent_id=OuterRef("pk")).viewable_by(user)) | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class Album(SithFile): | ||||
| class SASAlbumManager(models.Manager): | ||||
|     def get_queryset(self): | ||||
|         return super().get_queryset().filter(is_in_sas=True, is_folder=True) | ||||
|  | ||||
|  | ||||
| class Album(SasFile): | ||||
|     class Meta: | ||||
|         proxy = True | ||||
|  | ||||
|     objects = SASAlbumManager() | ||||
|     objects = SASAlbumManager.from_queryset(AlbumQuerySet)() | ||||
|  | ||||
|     @property | ||||
|     def children_pictures(self): | ||||
| @@ -175,15 +199,6 @@ class Album(SithFile): | ||||
|     def children_albums(self): | ||||
|         return Album.objects.filter(parent=self) | ||||
|  | ||||
|     def can_be_edited_by(self, user): | ||||
|         return user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID) | ||||
|  | ||||
|     def can_be_viewed_by(self, user): | ||||
|         # file = SithFile.objects.filter(id=self.id).first() | ||||
|         return self.can_be_edited_by(user) or ( | ||||
|             self.is_in_sas and self.is_moderated and user.was_subscribed | ||||
|         )  # or user.can_view(file) | ||||
|  | ||||
|     def get_absolute_url(self): | ||||
|         return reverse("sas:album", kwargs={"album_id": self.id}) | ||||
|  | ||||
|   | ||||
| @@ -3,7 +3,6 @@ from datetime import datetime | ||||
| from ninja import FilterSchema, ModelSchema, Schema | ||||
| from pydantic import Field, NonNegativeInt | ||||
|  | ||||
| from core.schemas import SimpleUserSchema | ||||
| from sas.models import PeoplePictureRelation, Picture | ||||
|  | ||||
|  | ||||
| @@ -17,14 +16,25 @@ class PictureFilterSchema(FilterSchema): | ||||
| class PictureSchema(ModelSchema): | ||||
|     class Meta: | ||||
|         model = Picture | ||||
|         fields = ["id", "name", "date", "size"] | ||||
|         fields = ["id", "name", "date", "size", "is_moderated"] | ||||
|  | ||||
|     author: SimpleUserSchema = Field(validation_alias="owner") | ||||
|     full_size_url: str | ||||
|     compressed_url: str | ||||
|     thumb_url: str | ||||
|     album: str | ||||
|  | ||||
|     @staticmethod | ||||
|     def resolve_full_size_url(obj: Picture) -> str: | ||||
|         return obj.get_download_url() | ||||
|  | ||||
|     @staticmethod | ||||
|     def resolve_compressed_url(obj: Picture) -> str: | ||||
|         return obj.get_download_compressed_url() | ||||
|  | ||||
|     @staticmethod | ||||
|     def resolve_thumb_url(obj: Picture) -> str: | ||||
|         return obj.get_download_thumb_url() | ||||
|  | ||||
|  | ||||
| class PictureCreateRelationSchema(Schema): | ||||
|     user_id: NonNegativeInt | ||||
|   | ||||
| @@ -1,20 +1,15 @@ | ||||
| {% extends "core/base.jinja" %} | ||||
| {% from "core/macros.jinja" import paginate %} | ||||
|  | ||||
| {%- block additional_css -%} | ||||
|   <link rel="stylesheet" href="{{ scss('sas/album.scss') }}"> | ||||
|   <link rel="stylesheet" href="{{ scss('core/pagination.scss') }}"> | ||||
| {%- endblock -%} | ||||
|  | ||||
| {% block title %} | ||||
|   {% trans %}SAS{% endtrans %} | ||||
| {% endblock %} | ||||
|  | ||||
| {% macro print_path(file) %} | ||||
|   {% if file and file.parent %} | ||||
|     {{ print_path(file.parent) }} | ||||
|     <a href="{{ url('sas:album', album_id=file.id) }}">{{ file.get_display_name() }}</a> / | ||||
|   {% endif %} | ||||
| {% endmacro %} | ||||
| {% from "sas/macros.jinja" import display_album, print_path %} | ||||
|  | ||||
|  | ||||
| {% block content %} | ||||
| @@ -22,10 +17,10 @@ | ||||
|     <a href="{{ url('sas:main') }}">SAS</a> / {{ print_path(album.parent) }} {{ album.get_display_name() }} | ||||
|   </code> | ||||
|  | ||||
|   {% set edit_mode = user.can_edit(album) %} | ||||
|   {% set is_sas_admin = user.can_edit(album) %} | ||||
|   {% set start = timezone.now() %} | ||||
|  | ||||
|   {% if edit_mode %} | ||||
|   {% if is_sas_admin %} | ||||
|     <form action="" method="post" enctype="multipart/form-data"> | ||||
|       {% csrf_token %} | ||||
|  | ||||
| @@ -53,73 +48,63 @@ | ||||
|       {% endif %} | ||||
|   {% endif %} | ||||
|  | ||||
|   {% if album.children_albums.count() > 0 %} | ||||
|   {% if children_albums|length > 0 %} | ||||
|     <h4>{% trans %}Albums{% endtrans %}</h4> | ||||
|     <div class="albums"> | ||||
|       {% for a in album.children_albums.order_by('-date') %} | ||||
|         {% if a.can_be_viewed_by(user) %} | ||||
|           <a href="{{ url('sas:album', album_id=a.id) }}"> | ||||
|             <div | ||||
|               class="album{% if not a.is_moderated %} not_moderated{% endif %}" | ||||
|               style="background-image: url('{% if a.file %}{{ a.get_download_url() }}{% else %}{{ static('core/img/sas.jpg') }}{% endif %}');" | ||||
|             > | ||||
|               {% if not a.is_moderated %} | ||||
|                 <div class="overlay"> </div> | ||||
|                 <div class="text">{% trans %}To be moderated{% endtrans %}</div> | ||||
|               {% else %} | ||||
|                 <div class="text">{{ a.name }}</div> | ||||
|               {% endif %} | ||||
|             </div> | ||||
|             {% if edit_mode %} | ||||
|               <input type="checkbox" name="file_list" value="{{ a.id }}"> | ||||
|             {% endif %} | ||||
|           </a> | ||||
|         {% endif %} | ||||
|       {% for a in children_albums %} | ||||
|         {{ display_album(a, is_sas_admin) }} | ||||
|       {% endfor %} | ||||
|     </div> | ||||
|  | ||||
|     <br> | ||||
|   {% endif %} | ||||
|  | ||||
|   <div x-data="pictures"> | ||||
|     <h4>{% trans %}Pictures{% endtrans %}</h4> | ||||
|   {% if pictures | length != 0 %} | ||||
|     <div class="photos"> | ||||
|       {% for p in pictures %} | ||||
|         {% if p.can_be_viewed_by(user) %} | ||||
|           <a href="{{ url('sas:picture', picture_id=p.id) }}#pict"> | ||||
|             <div | ||||
|               class="photo {% if p.is_vertical %}vertical{% endif %}" | ||||
|               style="background-image: url('{{ p.get_download_thumb_url() }}')" | ||||
|             > | ||||
|               {% if not p.is_moderated %} | ||||
|     <div class="photos" :aria-busy="loading"> | ||||
|       <template x-for="picture in pictures.results"> | ||||
|         <a :href="`/sas/picture/${picture.id}#pict`"> | ||||
|           <div class="photo" :style="`background-image: url(${picture.thumb_url})`"> | ||||
|             <template x-if="!picture.is_moderated"> | ||||
|               <div class="overlay"> </div> | ||||
|               <div class="text">{% trans %}To be moderated{% endtrans %}</div> | ||||
|               {% else %} | ||||
|             </template> | ||||
|             <template x-if="picture.is_moderated"> | ||||
|               <div class="text"> </div> | ||||
|               {% endif %} | ||||
|             </template> | ||||
|           </div> | ||||
|             {% if edit_mode %} | ||||
|               <input type="checkbox" name="file_list" value="{{ p.id }}"> | ||||
|           {% if is_sas_admin %} | ||||
|             <input type="checkbox" name="file_list" :value="picture.id"> | ||||
|           {% endif %} | ||||
|         </a> | ||||
|         {% endif %} | ||||
|       {% endfor %} | ||||
|       </template> | ||||
|     </div> | ||||
|   {% else %} | ||||
|     {% trans %}This album does not contain any photos.{% endtrans %} | ||||
|   {% endif %} | ||||
|  | ||||
|   {% if pictures.has_previous() or pictures.has_next() %} | ||||
|     <div class="paginator"> | ||||
|       {{ paginate(pictures, paginator) }} | ||||
|     <nav class="pagination" x-show="nb_pages() > 1"> | ||||
|       {# Adding the prevent here is important, because otherwise, | ||||
|       clicking on the pagination buttons could submit the picture management form | ||||
|       and reload the page #} | ||||
|       <button | ||||
|         @click.prevent="page--" | ||||
|         :disabled="page <= 1" | ||||
|         @keyup.right.window="page = Math.min(nb_pages(), page + 1)" | ||||
|       > | ||||
|         <i class="fa fa-caret-left"></i> | ||||
|       </button> | ||||
|       <template x-for="i in nb_pages()"> | ||||
|         <button x-text="i" @click.prevent="page = i" :class="{active: page === i}"></button> | ||||
|       </template> | ||||
|       <button | ||||
|         @click.prevent="page++" | ||||
|         :disabled="page >= nb_pages()" | ||||
|         @keyup.left.window="page = Math.max(1, page - 1)" | ||||
|       > | ||||
|         <i class="fa fa-caret-right"></i> | ||||
|       </button> | ||||
|     </nav> | ||||
|   </div> | ||||
|   {% endif %} | ||||
|  | ||||
|   {% if edit_mode %} | ||||
|   {% if is_sas_admin %} | ||||
|     </form> | ||||
|   {% endif %} | ||||
|  | ||||
|   {% if user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID) %} | ||||
|     <form class="add-files" id="upload_form" action="" method="post" enctype="multipart/form-data"> | ||||
|       {% csrf_token %} | ||||
|       <div class="inputs"> | ||||
| @@ -140,6 +125,36 @@ | ||||
| {% block script %} | ||||
|   {{ super() }} | ||||
|   <script> | ||||
|     document.addEventListener("alpine:init", () => { | ||||
|       Alpine.data("pictures", () => ({ | ||||
|         pictures: {}, | ||||
|         page: parseInt(initialUrlParams.get("page")) || 1, | ||||
|         loading: false, | ||||
|  | ||||
|         async init() { | ||||
|           await this.fetch_pictures(); | ||||
|           this.$watch("page", () => { | ||||
|             update_query_string("page", this.page === 1 ? null : this.page); | ||||
|             this.fetch_pictures() | ||||
|           }); | ||||
|         }, | ||||
|  | ||||
|         async fetch_pictures() { | ||||
|           this.loading=true; | ||||
|           const url = "{{ url("api:pictures") }}" | ||||
|           +"?album_id={{ album.id }}" | ||||
|           +`&page=${this.page}` | ||||
|           +"&page_size={{ settings.SITH_SAS_IMAGES_PER_PAGE }}"; | ||||
|           this.pictures = await (await fetch(url)).json(); | ||||
|           this.loading=false; | ||||
|         }, | ||||
|  | ||||
|         nb_pages() { | ||||
|           return Math.ceil(this.pictures.count / {{ settings.SITH_SAS_IMAGES_PER_PAGE }}); | ||||
|         } | ||||
|       })) | ||||
|     }) | ||||
|  | ||||
|     $("form#upload_form").submit(function (event) { | ||||
|       let formData = new FormData($(this)[0]); | ||||
|  | ||||
|   | ||||
							
								
								
									
										32
									
								
								sas/templates/sas/macros.jinja
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								sas/templates/sas/macros.jinja
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| {% macro display_album(a, edit_mode) %} | ||||
|   <a href="{{ url('sas:album', album_id=a.id) }}"> | ||||
|     {% if a.file %} | ||||
|       {% set img = a.get_download_url() %} | ||||
|     {% elif a.children.filter(is_folder=False, is_moderated=True).exists() %} | ||||
|       {% set img = a.children.filter(is_folder=False).first().as_picture.get_download_thumb_url() %} | ||||
|     {% else %} | ||||
|       {% set img = static('core/img/sas.jpg') %} | ||||
|     {% endif %} | ||||
|     <div | ||||
|       class="album{% if not a.is_moderated %} not_moderated{% endif %}" | ||||
|       style="background-image: url('{{ img }}');" | ||||
|     > | ||||
|       {% if not a.is_moderated %} | ||||
|         <div class="overlay"> </div> | ||||
|         <div class="text">{% trans %}To be moderated{% endtrans %}</div> | ||||
|       {% else %} | ||||
|         <div class="text">{{ a.name }}</div> | ||||
|       {% endif %} | ||||
|     </div> | ||||
|     {% if edit_mode %} | ||||
|       <input type="checkbox" name="file_list" value="{{ a.id }}"> | ||||
|     {% endif %} | ||||
|   </a> | ||||
| {% endmacro %} | ||||
|  | ||||
| {% macro print_path(file) %} | ||||
|   {% if file and file.parent %} | ||||
|     {{ print_path(file.parent) }} | ||||
|     <a href="{{ url('sas:album', album_id=file.id) }}">{{ file.get_display_name() }}</a> / | ||||
|   {% endif %} | ||||
| {% endmacro %} | ||||
| @@ -8,31 +8,9 @@ | ||||
|   {% trans %}SAS{% endtrans %} | ||||
| {% endblock %} | ||||
|  | ||||
| {% set edit_mode = user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID) %} | ||||
| {% set is_sas_admin = user.is_root or user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID) %} | ||||
|  | ||||
| {% macro display_album(a, checkbox) %} | ||||
|   <a href="{{ url('sas:album', album_id=a.id) }}"> | ||||
|     {% if a.file %} | ||||
|       {% set img = a.get_download_url() %} | ||||
|     {% elif a.children.filter(is_folder=False, is_moderated=True).exists() %} | ||||
|       {% set img = a.children.filter(is_folder=False).first().as_picture.get_download_thumb_url() %} | ||||
|     {% else %} | ||||
|       {% set img = static('core/img/sas.jpg') %} | ||||
|     {% endif %} | ||||
|  | ||||
|     <div | ||||
|       class="album" | ||||
|       style="background-image: url('{{ img }}');" | ||||
|     > | ||||
|       <div class="text"> | ||||
|         {{ a.name }} | ||||
|       </div> | ||||
|     </div> | ||||
|         {# {% if edit_mode and checkbox %} | ||||
|             <input type="checkbox" name="file_list" value="{{ a.id }}"> | ||||
|         {% endif %} #} | ||||
|   </a> | ||||
| {% endmacro %} | ||||
| {% from "sas/macros.jinja" import display_album %} | ||||
|  | ||||
| {% block content %} | ||||
|   <main> | ||||
| @@ -46,22 +24,18 @@ | ||||
|  | ||||
|       <div class="albums"> | ||||
|         {% for a in latest %} | ||||
|           {{ display_album(a) }} | ||||
|           {{ display_album(a, edit_mode=False) }} | ||||
|         {% endfor %} | ||||
|       </div> | ||||
|  | ||||
|       <br> | ||||
|  | ||||
|       {% if edit_mode %} | ||||
|       {% if is_sas_admin %} | ||||
|         <form action="" method="post" enctype="multipart/form-data"> | ||||
|           {% csrf_token %} | ||||
|  | ||||
|           <div class="navbar"> | ||||
|             <h4>{% trans %}All categories{% endtrans %}</h4> | ||||
|  | ||||
|                         {# <div class="toolbar"> | ||||
|                             <input name="delete" type="submit" value="{% trans %}Delete{% endtrans %}"> | ||||
|                         </div> #} | ||||
|           </div> | ||||
|  | ||||
|           {% if clipboard %} | ||||
| @@ -81,11 +55,11 @@ | ||||
|  | ||||
|       <div class="albums"> | ||||
|         {% for a in categories %} | ||||
|           {{ display_album(a, true) }} | ||||
|           {{ display_album(a, edit_mode=False) }} | ||||
|         {% endfor %} | ||||
|       </div> | ||||
|  | ||||
|       {% if edit_mode %} | ||||
|       {% if is_sas_admin %} | ||||
|         </form> | ||||
|  | ||||
|         <br> | ||||
|   | ||||
| @@ -4,32 +4,11 @@ | ||||
|   <link rel="stylesheet" href="{{ scss('sas/picture.scss') }}"> | ||||
| {%- endblock -%} | ||||
|  | ||||
| {% block head %} | ||||
|   {{ super() }} | ||||
|  | ||||
|   {% if picture.get_previous() %} | ||||
|     <link | ||||
|       rel="preload" | ||||
|       as="image" | ||||
|       href="{{ url("sas:download_compressed", picture_id=picture.get_previous().id) }}" | ||||
|     > | ||||
|   {% endif %} | ||||
|   {% if picture.get_next() %} | ||||
|     <link rel="preload" as="image" href="{{ url("sas:download_compressed", picture_id=picture.get_next().id) }}"> | ||||
|   {% endif %} | ||||
|  | ||||
| {% endblock %} | ||||
|  | ||||
| {% block title %} | ||||
|   {% trans %}SAS{% endtrans %} | ||||
| {% endblock %} | ||||
|  | ||||
| {% macro print_path(file) %} | ||||
|   {% if file and file.parent %} | ||||
|     {{ print_path(file.parent) }} | ||||
|     <a href="{{ url('sas:album', album_id=file.id) }}">{{ file.get_display_name() }}</a> / | ||||
|   {% endif %} | ||||
| {% endmacro %} | ||||
| {% from "sas/macros.jinja" import print_path %} | ||||
|  | ||||
| {% block content %} | ||||
|   <code> | ||||
| @@ -124,16 +103,16 @@ | ||||
|     <div class="subsection"> | ||||
|       <div class="navigation"> | ||||
|         <div id="prev"> | ||||
|           {% if picture.get_previous() %} | ||||
|             <a href="{{ url( 'sas:picture', picture_id=picture.get_previous().id) }}#pict"> | ||||
|               <div style="background-image: url('{{ picture.get_previous().as_picture.get_download_thumb_url() }}');"></div> | ||||
|           {% if previous_pict %} | ||||
|             <a href="{{ url( 'sas:picture', picture_id=previous_pict.id) }}#pict"> | ||||
|               <div style="background-image: url('{{ previous_pict.get_download_thumb_url() }}');"></div> | ||||
|             </a> | ||||
|           {% endif %} | ||||
|         </div> | ||||
|         <div id="next"> | ||||
|           {% if picture.get_next() %} | ||||
|             <a href="{{ url( 'sas:picture', picture_id=picture.get_next().id) }}#pict"> | ||||
|               <div style="background-image: url('{{ picture.get_next().as_picture.get_download_thumb_url() }}');"></div> | ||||
|           {% if next_pict %} | ||||
|             <a href="{{ url( 'sas:picture', picture_id=next_pict.id) }}#pict"> | ||||
|               <div style="background-image: url('{{ next_pict.get_download_thumb_url() }}');"></div> | ||||
|             </a> | ||||
|           {% endif %} | ||||
|         </div> | ||||
| @@ -141,11 +120,13 @@ | ||||
|  | ||||
|       <div class="tags"> | ||||
|         <h5>{% trans %}People{% endtrans %}</h5> | ||||
|         {% if user.was_subscribed %} | ||||
|           <form action="" method="post" enctype="multipart/form-data"> | ||||
|             {% csrf_token %} | ||||
|             {{ form.as_p() }} | ||||
|             <input type="submit" value="{% trans %}Go{% endtrans %}" /> | ||||
|           </form> | ||||
|         {% endif %} | ||||
|         <ul x-data="user_identification"> | ||||
|           <template x-for="item in items" :key="item.id"> | ||||
|             <li> | ||||
|   | ||||
| @@ -44,12 +44,8 @@ class TestPictureSearch(TestSas): | ||||
|         self.client.force_login(self.user_b) | ||||
|         res = self.client.get(reverse("api:pictures") + f"?album_id={self.album_a.id}") | ||||
|         assert res.status_code == 200 | ||||
|         expected = list( | ||||
|             self.album_a.children_pictures.order_by("-date").values_list( | ||||
|                 "id", flat=True | ||||
|             ) | ||||
|         ) | ||||
|         assert [i["id"] for i in res.json()] == expected | ||||
|         expected = list(self.album_a.children_pictures.values_list("id", flat=True)) | ||||
|         assert [i["id"] for i in res.json()["results"]] == expected | ||||
|  | ||||
|     def test_filter_by_user(self): | ||||
|         self.client.force_login(self.user_b) | ||||
| @@ -58,11 +54,11 @@ class TestPictureSearch(TestSas): | ||||
|         ) | ||||
|         assert res.status_code == 200 | ||||
|         expected = list( | ||||
|             self.user_a.pictures.order_by("-picture__date").values_list( | ||||
|                 "picture_id", flat=True | ||||
|             self.user_a.pictures.order_by( | ||||
|                 "-picture__parent__date", "picture__date" | ||||
|             ).values_list("picture_id", flat=True) | ||||
|         ) | ||||
|         ) | ||||
|         assert [i["id"] for i in res.json()] == expected | ||||
|         assert [i["id"] for i in res.json()["results"]] == expected | ||||
|  | ||||
|     def test_filter_by_multiple_user(self): | ||||
|         self.client.force_login(self.user_b) | ||||
| @@ -73,38 +69,53 @@ class TestPictureSearch(TestSas): | ||||
|         assert res.status_code == 200 | ||||
|         expected = list( | ||||
|             self.user_a.pictures.union(self.user_b.pictures.all()) | ||||
|             .order_by("-picture__date") | ||||
|             .order_by("-picture__parent__date", "picture__date") | ||||
|             .values_list("picture_id", flat=True) | ||||
|         ) | ||||
|         assert [i["id"] for i in res.json()] == expected | ||||
|         assert [i["id"] for i in res.json()["results"]] == expected | ||||
|  | ||||
|     def test_not_subscribed_user(self): | ||||
|         """Test that a user that is not subscribed can only its own pictures.""" | ||||
|         """Test that a user that never subscribed can only its own pictures.""" | ||||
|         self.user_a.subscriptions.all().delete() | ||||
|         self.client.force_login(self.user_a) | ||||
|         res = self.client.get( | ||||
|             reverse("api:pictures") + f"?users_identified={self.user_a.id}" | ||||
|         ) | ||||
|         assert res.status_code == 200 | ||||
|         expected = list( | ||||
|             self.user_a.pictures.order_by("-picture__date").values_list( | ||||
|                 "picture_id", flat=True | ||||
|             self.user_a.pictures.order_by( | ||||
|                 "-picture__parent__date", "picture__date" | ||||
|             ).values_list("picture_id", flat=True) | ||||
|         ) | ||||
|         ) | ||||
|         assert [i["id"] for i in res.json()] == expected | ||||
|         assert [i["id"] for i in res.json()["results"]] == expected | ||||
|  | ||||
|         # trying to access the pictures of someone else | ||||
|         res = self.client.get( | ||||
|             reverse("api:pictures") + f"?users_identified={self.user_b.id}" | ||||
|         ) | ||||
|         assert res.status_code == 403 | ||||
|  | ||||
|         # trying to access the pictures of someone else shouldn't success, | ||||
|         # even if mixed with owned pictures | ||||
|         # trying to access the pictures of someone else mixed with owned pictures | ||||
|         # should return only owned pictures | ||||
|         res = self.client.get( | ||||
|             reverse("api:pictures") | ||||
|             + f"?users_identified={self.user_a.id}&users_identified={self.user_b.id}" | ||||
|         ) | ||||
|         assert res.status_code == 403 | ||||
|         assert res.status_code == 200 | ||||
|         assert [i["id"] for i in res.json()["results"]] == expected | ||||
|  | ||||
|         # trying to fetch everything should be the same | ||||
|         # as fetching its own pictures for a non-subscriber | ||||
|         res = self.client.get(reverse("api:pictures")) | ||||
|         assert res.status_code == 200 | ||||
|         assert [i["id"] for i in res.json()["results"]] == expected | ||||
|  | ||||
|         # trying to access the pictures of someone else should return only | ||||
|         # the ones where the non-subscribed user is identified too | ||||
|         res = self.client.get( | ||||
|             reverse("api:pictures") + f"?users_identified={self.user_b.id}" | ||||
|         ) | ||||
|         assert res.status_code == 200 | ||||
|         expected = list( | ||||
|             self.user_b.pictures.intersection(self.user_a.pictures.all()) | ||||
|             .order_by("-picture__parent__date", "picture__date") | ||||
|             .values_list("picture_id", flat=True) | ||||
|         ) | ||||
|         assert [i["id"] for i in res.json()["results"]] == expected | ||||
|  | ||||
|  | ||||
| class TestPictureRelation(TestSas): | ||||
|   | ||||
							
								
								
									
										48
									
								
								sas/tests/test_model.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								sas/tests/test_model.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | ||||
| from django.test import TestCase | ||||
| from model_bakery import baker, seq | ||||
|  | ||||
| from core.baker_recipes import old_subscriber_user, subscriber_user | ||||
| from core.models import User | ||||
| from sas.models import Picture | ||||
|  | ||||
|  | ||||
| class TestPictureQuerySet(TestCase): | ||||
|     @classmethod | ||||
|     def setUpTestData(cls): | ||||
|         Picture.objects.all().delete() | ||||
|         cls.pictures = baker.make( | ||||
|             Picture, | ||||
|             is_moderated=True, | ||||
|             is_in_sas=True, | ||||
|             is_folder=False, | ||||
|             name=seq(""), | ||||
|             _quantity=10, | ||||
|             _bulk_create=True, | ||||
|         ) | ||||
|         Picture.objects.filter(pk=cls.pictures[0].id).update(is_moderated=False) | ||||
|  | ||||
|     def test_root(self): | ||||
|         root = baker.make(User, is_superuser=True) | ||||
|         pictures = list(Picture.objects.viewable_by(root)) | ||||
|         self.assertCountEqual(pictures, self.pictures) | ||||
|  | ||||
|     def test_subscriber(self): | ||||
|         subscriber = subscriber_user.make() | ||||
|         old_subcriber = old_subscriber_user.make() | ||||
|         for user in (subscriber, old_subcriber): | ||||
|             pictures = list(Picture.objects.viewable_by(user)) | ||||
|             self.assertCountEqual(pictures, self.pictures[1:]) | ||||
|  | ||||
|     def test_not_subscribed_identified(self): | ||||
|         user = baker.make( | ||||
|             # This is the guy who asked the feature of making pictures | ||||
|             # available for tagged users, even if not subscribed | ||||
|             User, | ||||
|             first_name="Pierrick", | ||||
|             last_name="Dheilly", | ||||
|             nick_name="Sahmer", | ||||
|         ) | ||||
|         user.pictures.create(picture=self.pictures[0]) | ||||
|         user.pictures.create(picture=self.pictures[1]) | ||||
|         pictures = list(Picture.objects.viewable_by(user)) | ||||
|         assert pictures == [self.pictures[1]] | ||||
							
								
								
									
										39
									
								
								sas/views.py
									
									
									
									
									
								
							
							
						
						
									
										39
									
								
								sas/views.py
									
									
									
									
									
								
							| @@ -18,7 +18,6 @@ from ajax_select.fields import AutoCompleteSelectMultipleField | ||||
| from django import forms | ||||
| from django.conf import settings | ||||
| from django.core.exceptions import PermissionDenied | ||||
| from django.core.paginator import InvalidPage, Paginator | ||||
| from django.http import Http404, HttpResponse | ||||
| from django.shortcuts import get_object_or_404, redirect | ||||
| from django.urls import reverse, reverse_lazy | ||||
| @@ -120,10 +119,11 @@ class SASMainView(FormView): | ||||
|  | ||||
|     def get_context_data(self, **kwargs): | ||||
|         kwargs = super().get_context_data(**kwargs) | ||||
|         kwargs["categories"] = Album.objects.filter( | ||||
|             parent__id=settings.SITH_SAS_ROOT_DIR_ID | ||||
|         ).order_by("id") | ||||
|         kwargs["latest"] = Album.objects.filter(is_moderated=True).order_by("-id")[:5] | ||||
|         albums_qs = Album.objects.viewable_by(self.request.user) | ||||
|         kwargs["categories"] = list( | ||||
|             albums_qs.filter(parent_id=settings.SITH_SAS_ROOT_DIR_ID).order_by("id") | ||||
|         ) | ||||
|         kwargs["latest"] = list(albums_qs.order_by("-id")[:5]) | ||||
|         return kwargs | ||||
|  | ||||
|  | ||||
| @@ -181,7 +181,14 @@ class PictureView(CanViewMixin, DetailView, FormMixin): | ||||
|  | ||||
|     def get_context_data(self, **kwargs): | ||||
|         kwargs = super().get_context_data(**kwargs) | ||||
|         pictures_qs = Picture.objects.viewable_by(self.request.user) | ||||
|         kwargs["form"] = self.form | ||||
|         kwargs["next_pict"] = ( | ||||
|             pictures_qs.filter(id__gt=self.object.id).order_by("id").first() | ||||
|         ) | ||||
|         kwargs["previous_pict"] = ( | ||||
|             pictures_qs.filter(id__lt=self.object.id).order_by("-id").first() | ||||
|         ) | ||||
|         return kwargs | ||||
|  | ||||
|     def get_success_url(self): | ||||
| @@ -222,8 +229,9 @@ class AlbumUploadView(CanViewMixin, DetailView, FormMixin): | ||||
|                     parent=parent, | ||||
|                     owner=request.user, | ||||
|                     files=files, | ||||
|                     automodere=request.user.is_in_group( | ||||
|                         pk=settings.SITH_GROUP_SAS_ADMIN_ID | ||||
|                     automodere=( | ||||
|                         request.user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID) | ||||
|                         or request.user.is_root | ||||
|                     ), | ||||
|                 ) | ||||
|                 if self.form.is_valid(): | ||||
| @@ -236,7 +244,6 @@ class AlbumView(CanViewMixin, DetailView, FormMixin): | ||||
|     form_class = SASForm | ||||
|     pk_url_kwarg = "album_id" | ||||
|     template_name = "sas/album.jinja" | ||||
|     paginate_by = settings.SITH_SAS_IMAGES_PER_PAGE | ||||
|  | ||||
|     def dispatch(self, request, *args, **kwargs): | ||||
|         try: | ||||
| @@ -283,17 +290,15 @@ class AlbumView(CanViewMixin, DetailView, FormMixin): | ||||
|  | ||||
|     def get_context_data(self, **kwargs): | ||||
|         kwargs = super().get_context_data(**kwargs) | ||||
|         kwargs["paginator"] = Paginator( | ||||
|             self.object.children_pictures.order_by("id"), self.paginate_by | ||||
|         ) | ||||
|         try: | ||||
|             kwargs["pictures"] = kwargs["paginator"].page(self.asked_page) | ||||
|         except InvalidPage as e: | ||||
|             raise Http404 from e | ||||
|         kwargs["form"] = self.form | ||||
|         kwargs["clipboard"] = SithFile.objects.filter( | ||||
|             id__in=self.request.session["clipboard"] | ||||
|         ) | ||||
|         kwargs["children_albums"] = list( | ||||
|             Album.objects.viewable_by(self.request.user) | ||||
|             .filter(parent_id=self.object.id) | ||||
|             .order_by("-date") | ||||
|         ) | ||||
|         return kwargs | ||||
|  | ||||
|  | ||||
| @@ -326,9 +331,7 @@ class ModerationView(TemplateView): | ||||
|         kwargs["albums_to_moderate"] = Album.objects.filter( | ||||
|             is_moderated=False, is_in_sas=True, is_folder=True | ||||
|         ).order_by("id") | ||||
|         kwargs["pictures"] = Picture.objects.filter( | ||||
|             is_moderated=False, is_in_sas=True, is_folder=False | ||||
|         ) | ||||
|         kwargs["pictures"] = Picture.objects.filter(is_moderated=False) | ||||
|         kwargs["albums"] = Album.objects.filter( | ||||
|             id__in=kwargs["pictures"].values("parent").distinct("parent") | ||||
|         ) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user