# # Copyright 2023 © AE UTBM # ae@utbm.fr / ae.info@utbm.fr # # This file is part of the website of the UTBM Student Association (AE UTBM), # https://ae.utbm.fr. # # You can find the source code of the website at https://github.com/ae-utbm/sith3 # # LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3) # SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE # OR WITHIN THE LOCAL FILE "LICENSE" # # 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.urls import reverse from django.utils import timezone from django.utils.translation import gettext_lazy as _ from PIL import Image from core.models import SithFile, User from core.utils import exif_auto_rotate, resize_image 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 PictureQuerySet(self.model, using=self._db).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 Meta: proxy = True objects = SASPictureManager.from_queryset(PictureQuerySet)() @property def is_vertical(self): with open(settings.MEDIA_ROOT / self.file.name, "rb") as f: im = Image.open(BytesIO(f.read())) (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: User) -> bool: if user.is_anonymous: return False cache_key = f"sas:pictures_viewable_by_{user.id}_in_{self.parent_id}" viewable: list[int] | None = cache.get(cache_key) if viewable is None: viewable = list( Picture.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 get_download_url(self): return reverse("sas:download", kwargs={"picture_id": self.id}) def get_download_compressed_url(self): return reverse("sas:download_compressed", kwargs={"picture_id": self.id}) def get_download_thumb_url(self): return reverse("sas:download_thumb", kwargs={"picture_id": self.id}) def get_absolute_url(self): return reverse("sas:picture", kwargs={"picture_id": self.id}) def generate_thumbnails(self, *, overwrite=False): im = Image.open(BytesIO(self.file.read())) try: im = exif_auto_rotate(im) except: pass file = resize_image(im, max(im.size), self.mime_type.split("/")[-1]) thumb = resize_image(im, 200, self.mime_type.split("/")[-1]) compressed = resize_image(im, 1200, self.mime_type.split("/")[-1]) if overwrite: self.file.delete() self.thumbnail.delete() self.compressed.delete() self.file = file self.file.name = self.name self.thumbnail = thumb self.thumbnail.name = self.name self.compressed = compressed self.compressed.name = self.name self.save() def rotate(self, degree): for attr in ["file", "compressed", "thumbnail"]: name = self.__getattribute__(attr).name with open(settings.MEDIA_ROOT / name, "r+b") as file: if file: im = Image.open(BytesIO(file.read())) file.seek(0) im = im.rotate(degree, expand=True) im.save( fp=file, format=self.mime_type.split("/")[-1].upper(), quality=90, optimize=True, progressive=True, ) def get_next(self): if self.is_moderated: return ( 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() ) def get_previous(self): if self.is_moderated: return ( 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() ) class Album(SithFile): class Meta: proxy = True objects = SASAlbumManager() @property def children_pictures(self): return Picture.objects.filter(parent=self) @property 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}) def get_download_url(self): return reverse("sas:album_preview", kwargs={"album_id": self.id}) def generate_thumbnail(self): p = ( self.children_pictures.order_by("?").first() or self.children_albums.exclude(file=None) .exclude(file="") .order_by("?") .first() ) if p and p.file: im = Image.open(BytesIO(p.file.read())) self.file = resize_image(im, 200, "jpeg") self.file.name = self.name + "/thumb.jpg" self.save() def sas_notification_callback(notif): count = Picture.objects.filter(is_moderated=False).count() if count: notif.viewed = False else: notif.viewed = True notif.param = "%s" % count notif.date = timezone.now() class PeoplePictureRelation(models.Model): """The PeoplePictureRelation class makes the connection between User and Picture.""" user = models.ForeignKey( User, verbose_name=_("user"), related_name="pictures", null=False, blank=False, on_delete=models.CASCADE, ) picture = models.ForeignKey( Picture, verbose_name=_("picture"), related_name="people", null=False, blank=False, on_delete=models.CASCADE, ) class Meta: unique_together = ["user", "picture"] def __str__(self): return self.user.get_display_name() + " - " + str(self.picture)