Sith/sas/models.py

275 lines
8.9 KiB
Python
Raw Normal View History

#
# 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"
#
#
2024-08-06 11:23:34 +00:00
from __future__ import annotations
2024-06-24 11:07:36 +00:00
from io import BytesIO
from typing import Self
2024-06-24 11:07:36 +00:00
from django.conf import settings
from django.core.cache import cache
2016-10-26 17:21:19 +00:00
from django.db import models
from django.db.models import Exists, OuterRef
from django.urls import reverse
from django.utils import timezone
2024-06-24 11:07:36 +00:00
from django.utils.translation import gettext_lazy as _
2016-11-20 22:53:41 +00:00
from PIL import Image
2016-10-26 17:21:19 +00:00
from core.models import SithFile, User
2024-06-24 11:07:36 +00:00
from core.utils import exif_auto_rotate, resize_image
2016-10-26 17:21:19 +00:00
2018-06-10 16:43:39 +00:00
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) -> Self:
"""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)
2024-08-06 11:23:34 +00:00
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)
2018-06-10 16:43:39 +00:00
2017-06-12 08:02:38 +00:00
class Picture(SasFile):
2016-10-26 17:21:19 +00:00
class Meta:
proxy = True
objects = SASPictureManager.from_queryset(PictureQuerySet)()
2016-11-20 22:53:41 +00:00
@property
def is_vertical(self):
2024-07-26 13:14:37 +00:00
with open(settings.MEDIA_ROOT / self.file.name, "rb") as f:
2016-11-29 10:35:31 +00:00
im = Image.open(BytesIO(f.read()))
(w, h) = im.size
return (w / h) < 1
2016-11-20 22:53:41 +00:00
2016-10-26 17:21:19 +00:00
def get_download_url(self):
2018-10-04 19:29:19 +00:00
return reverse("sas:download", kwargs={"picture_id": self.id})
2016-10-26 17:21:19 +00:00
2016-11-20 10:56:33 +00:00
def get_download_compressed_url(self):
2018-10-04 19:29:19 +00:00
return reverse("sas:download_compressed", kwargs={"picture_id": self.id})
2016-11-20 10:56:33 +00:00
def get_download_thumb_url(self):
2018-10-04 19:29:19 +00:00
return reverse("sas:download_thumb", kwargs={"picture_id": self.id})
2016-11-20 10:56:33 +00:00
2016-11-25 12:47:09 +00:00
def get_absolute_url(self):
2018-10-04 19:29:19 +00:00
return reverse("sas:picture", kwargs={"picture_id": self.id})
2016-11-25 12:47:09 +00:00
2024-06-27 12:57:40 +00:00
def generate_thumbnails(self, *, overwrite=False):
2016-11-20 23:02:40 +00:00
im = Image.open(BytesIO(self.file.read()))
try:
im = exif_auto_rotate(im)
2017-06-12 08:02:38 +00:00
except:
pass
2024-09-01 17:05:54 +00:00
# convert the compressed image and the thumbnail into webp
# The original image keeps its original type, because it's not
# meant to be shown on the website, but rather to keep the real image
# for less frequent cases (like downloading the pictures of an user)
extension = self.mime_type.split("/")[-1]
file = resize_image(im, max(im.size), extension)
thumb = resize_image(im, 200, "webp")
compressed = resize_image(im, 1200, "webp")
if overwrite:
self.file.delete()
self.thumbnail.delete()
self.compressed.delete()
2024-09-01 17:05:54 +00:00
new_extension_name = self.name.removesuffix(extension) + "webp"
2016-11-20 23:02:40 +00:00
self.file = file
self.file.name = self.name
self.thumbnail = thumb
2024-09-01 17:05:54 +00:00
self.thumbnail.name = new_extension_name
2016-11-20 23:02:40 +00:00
self.compressed = compressed
2024-09-01 17:05:54 +00:00
self.compressed.name = new_extension_name
2016-11-20 23:02:40 +00:00
self.save()
2016-11-20 12:39:04 +00:00
def rotate(self, degree):
2018-10-04 19:29:19 +00:00
for attr in ["file", "compressed", "thumbnail"]:
2016-11-30 08:19:09 +00:00
name = self.__getattribute__(attr).name
2024-07-26 13:14:37 +00:00
with open(settings.MEDIA_ROOT / name, "r+b") as file:
2016-11-30 08:19:09 +00:00
if file:
im = Image.open(BytesIO(file.read()))
file.seek(0)
im = im.rotate(degree, expand=True)
2018-10-04 19:29:19 +00:00
im.save(
fp=file,
format=self.mime_type.split("/")[-1].upper(),
quality=90,
optimize=True,
progressive=True,
)
2016-11-20 12:39:04 +00:00
def get_next(self):
if self.is_moderated:
pictures_qs = self.parent.children.filter(
is_moderated=True,
asked_for_removal=False,
is_folder=False,
id__gt=self.id,
2018-10-04 19:29:19 +00:00
)
else:
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:
pictures_qs = self.parent.children.filter(
is_moderated=True,
asked_for_removal=False,
is_folder=False,
id__lt=self.id,
2018-10-04 19:29:19 +00:00
)
else:
pictures_qs = Picture.objects.filter(id__lt=self.id, is_moderated=False)
return pictures_qs.order_by("-id").first()
2017-06-12 08:02:38 +00:00
class AlbumQuerySet(models.QuerySet):
def viewable_by(self, user: User) -> Self:
"""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()
2024-08-09 16:06:58 +00:00
if user.was_subscribed:
2024-08-09 22:43:15 +00:00
return self.filter(is_moderated=True)
2024-08-09 16:06:58 +00:00
# known bug : if all children of an album are also albums
# then this album is excluded, even if one of the sub-albums should be visible.
# The fs-like navigation is likely to be half-broken for non-subscribers,
# but that's ok, since non-subscribers are expected to see only the albums
# containing pictures on which they have been identified (hence, very few).
# Most, if not all, of their albums will be displayed on the
# `latest albums` section of the SAS.
# Moreover, they will still see all of their picture in their profile.
return self.filter(
Exists(Picture.objects.filter(parent_id=OuterRef("pk")).viewable_by(user))
)
class SASAlbumManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(is_in_sas=True, is_folder=True)
class Album(SasFile):
2016-10-26 17:21:19 +00:00
class Meta:
proxy = True
objects = SASAlbumManager.from_queryset(AlbumQuerySet)()
@property
def children_pictures(self):
return Picture.objects.filter(parent=self)
@property
def children_albums(self):
return Album.objects.filter(parent=self)
2016-10-26 17:21:19 +00:00
def get_absolute_url(self):
2018-10-04 19:29:19 +00:00
return reverse("sas:album", kwargs={"album_id": self.id})
2016-10-26 17:21:19 +00:00
2016-11-30 08:19:09 +00:00
def get_download_url(self):
2018-10-04 19:29:19 +00:00
return reverse("sas:album_preview", kwargs={"album_id": self.id})
2016-11-30 08:19:09 +00:00
2016-12-18 17:34:48 +00:00
def generate_thumbnail(self):
2018-10-04 19:29:19 +00:00
p = (
self.children_pictures.order_by("?").first()
or self.children_albums.exclude(file=None)
.exclude(file="")
.order_by("?")
.first()
)
2016-12-18 17:34:48 +00:00
if p and p.file:
2024-09-01 17:05:54 +00:00
image = resize_image(Image.open(BytesIO(p.file.read())), 200, "webp")
self.file = image
self.file.name = f"{self.name}/thumb.webp"
2016-12-18 17:34:48 +00:00
self.save()
2018-06-10 16:43:39 +00:00
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()
2017-06-12 08:02:38 +00:00
2018-06-10 16:43:39 +00:00
class PeoplePictureRelation(models.Model):
2024-07-12 07:34:16 +00:00
"""The PeoplePictureRelation class makes the connection between User and Picture."""
2018-10-04 19:29:19 +00:00
user = models.ForeignKey(
User,
verbose_name=_("user"),
related_name="pictures",
null=False,
blank=False,
on_delete=models.CASCADE,
2018-10-04 19:29:19 +00:00
)
picture = models.ForeignKey(
Picture,
verbose_name=_("picture"),
related_name="people",
null=False,
blank=False,
on_delete=models.CASCADE,
2018-10-04 19:29:19 +00:00
)
class Meta:
2018-10-04 19:29:19 +00:00
unique_together = ["user", "picture"]
2016-11-25 12:47:09 +00:00
def __str__(self):
return self.user.get_display_name() + " - " + str(self.picture)