Automatically resize album thumbnail

This commit is contained in:
imperosol
2026-04-23 23:52:20 +02:00
parent f9c5297473
commit d621911849
4 changed files with 55 additions and 28 deletions
+40 -5
View File
@@ -1,16 +1,23 @@
from typing import Any
import copy
from pathlib import Path
from typing import TYPE_CHECKING, Any
from django import forms
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
from PIL import Image
from core.models import User
from core.utils import resize_image
from core.views import MultipleImageField
from core.views.forms import SelectDate
from core.views.widgets.ajax_select import AutoCompleteSelectMultipleGroup
from sas.models import Album, Picture, PictureModerationRequest
from sas.widgets.ajax_select import AutoCompleteSelectAlbum
if TYPE_CHECKING:
from django.db.models.fields.files import FieldFile
class AlbumCreateForm(forms.ModelForm):
class Meta:
@@ -49,17 +56,45 @@ class AlbumEditForm(forms.ModelForm):
class Meta:
model = Album
fields = ["name", "date", "file", "parent", "edit_groups"]
widgets = {
"edit_groups": AutoCompleteSelectMultipleGroup,
}
widgets = {"edit_groups": AutoCompleteSelectMultipleGroup, "date": SelectDate}
name = forms.CharField(max_length=Album.NAME_MAX_LENGTH, label=_("file name"))
date = forms.DateField(label=_("Date"), widget=SelectDate, required=True)
recursive = forms.BooleanField(label=_("Apply rights recursively"), required=False)
parent = forms.ModelChoiceField(
Album.objects.all(), required=True, widget=AutoCompleteSelectAlbum
)
def clean_file(self):
# if a file was given in the form, resize it
f: FieldFile = self.cleaned_data["file"]
if self.errors or not f or "file" not in self.changed_data:
return f
f.file = resize_image(Image.open(f.file), 200, "WEBP")
return f
def save(self, commit=True): # noqa: FBT002
initial_file = copy.copy(self.initial["file"])
if not self.cleaned_data["file"]:
# if no file is in the form, it can mean either :
# - there was a file initially, but the deletion box was checked
# - there was no file initially, and there still isn't
# in both cases, we procedurally generate the thumbnail
self.instance.generate_thumbnail()
elif "file" in self.changed_data:
self.instance.file.name = str(Path(self.instance.name) / "thumb.webp")
res = super().save(commit=commit)
if initial_file and (
not self.instance.file or initial_file.path != self.instance.file.path
):
# The initial file must be removed from storage
# AFTER the new one has been dealt with,
# in order to be sure that django will generate a different filename.
# Otherwise, the client cache wouldn't be properly busted.
# Even if there was no file, this operation cannot fail, because django
# will just silently return in that case.
initial_file.delete(save=False)
return res
class PictureModerationRequestForm(forms.ModelForm):
"""Form to create a PictureModerationRequest.
+6 -14
View File
@@ -22,6 +22,7 @@ from typing import ClassVar, Self
from django.conf import settings
from django.core.cache import cache
from django.core.files.base import ContentFile
from django.db import models
from django.db.models import Exists, OuterRef, Q
from django.urls import reverse
@@ -110,7 +111,7 @@ class Picture(SasFile):
def get_absolute_url(self):
return reverse("sas:picture", kwargs={"picture_id": self.id})
def generate_thumbnails(self, *, overwrite=False):
def generate_thumbnails(self):
im = Image.open(BytesIO(self.file.read()))
with contextlib.suppress(Exception):
im = exif_auto_rotate(im)
@@ -126,10 +127,6 @@ class Picture(SasFile):
file = resize_image(im, max(im.size), extension, optimize=False)
thumb = resize_image(im, 200, "webp")
compressed = resize_image(im, 1200, "webp")
if overwrite:
self.file.delete()
self.thumbnail.delete()
self.compressed.delete()
new_extension_name = str(Path(self.name).with_suffix(".webp"))
self.file = file
self.file.name = self.name
@@ -245,17 +242,12 @@ class Album(SasFile):
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()
p = self.children_pictures.order_by("?").first()
if p and p.thumbnail:
image = ContentFile(
name=str(Path(self.name) / "thumb.webp"), content=p.thumbnail.read()
)
if p and p.file:
image = resize_image(Image.open(BytesIO(p.file.read())), 200, "webp")
self.file = image
self.file.name = f"{self.name}/thumb.webp"
self.save()
+4 -4
View File
@@ -2,19 +2,19 @@
<a href="{{ url('sas:album', album_id=a.id) }}">
{% if a.file %}
{% set img = a.get_download_url() %}
{% set src = a.name %}
{% set alt = a.name %}
{% elif a.children.filter(is_folder=False, is_moderated=True).exists() %}
{% set picture = a.children.filter(is_folder=False).first().as_picture %}
{% set img = picture.get_download_thumb_url() %}
{% set src = picture.name %}
{% set alt = picture.name %}
{% else %}
{% set img = static('core/img/sas.jpg') %}
{% set src = "sas.jpg" %}
{% set alt = "sas.jpg" %}
{% endif %}
<div
class="album{% if not a.is_moderated %} not_moderated{% endif %}"
>
<img src="{{ img }}" alt="{{ src }}" loading="lazy" />
<img src="{{ img }}" alt="{{ alt }}" loading="lazy" />
{% if not a.is_moderated %}
<div class="overlay">&nbsp;</div>
<div class="text">{% trans %}To be moderated{% endtrans %}</div>
+3 -3
View File
@@ -16,6 +16,7 @@ from typing import Any
from django.conf import settings
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.core.exceptions import PermissionDenied
from django.db.models import Count, OuterRef, Subquery
from django.http import Http404, HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect
@@ -152,9 +153,8 @@ class AlbumView(CanViewMixin, UseFragmentsMixin, DetailView):
def post(self, request, *args, **kwargs):
self.object = self.get_object()
if not self.object.file:
self.object.generate_thumbnail()
if request.user.can_edit(self.object): # Handle the copy-paste functions
if not request.user.can_edit(self.object):
raise PermissionDenied
FileView.handle_clipboard(request, self.object)
return HttpResponseRedirect(self.request.path)