From 1eb025ec5ff8472564c883a252a2fcef535e1de5 Mon Sep 17 00:00:00 2001 From: krek0 Date: Sun, 3 May 2026 23:50:48 +0200 Subject: [PATCH 01/10] Push latest tag on docker image on release --- .forgejo/workflows/docker.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.forgejo/workflows/docker.yml b/.forgejo/workflows/docker.yml index 90f3a7c..e5da6fc 100644 --- a/.forgejo/workflows/docker.yml +++ b/.forgejo/workflows/docker.yml @@ -22,11 +22,12 @@ jobs: username: ${{ secrets.REGISTRY_USER }} password: ${{ secrets.REGISTRY_TOKEN }} - - name: Build image - run: | - docker build -t git.sinfonie.org/sinfonie/photo26:${{ steps.meta.outputs.TAG }} . - - - name: Push image + - name: Build and push image run: | + docker build \ + -t git.sinfonie.org/sinfonie/photo26:${{ steps.meta.outputs.TAG }} \ + -t git.sinfonie.org/sinfonie/photo26:latest \ + . docker push git.sinfonie.org/sinfonie/photo26:${{ steps.meta.outputs.TAG }} + docker push git.sinfonie.org/sinfonie/photo26:latest From c15e9bf65444f2052d0ba43dc72112ad55e6785b Mon Sep 17 00:00:00 2001 From: krek0 Date: Thu, 7 May 2026 13:49:03 +0200 Subject: [PATCH 02/10] Fix uploaded photos saved as empty files after Pillow verify() --- photologue/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/photologue/views.py b/photologue/views.py index 52d4776..0cf9ab3 100644 --- a/photologue/views.py +++ b/photologue/views.py @@ -272,6 +272,7 @@ class GalleryUpload(PermissionRequiredMixin, FormView): try: opened = Image.open(photo_file) opened.verify() + photo_file.seek(0) except Exception: # Pillow doesn't recognize it as an image, skip it messages.error( From fdbf03800a60afb5ebdb4890d2c4347ca6da7598 Mon Sep 17 00:00:00 2001 From: krek0 Date: Thu, 7 May 2026 13:51:07 +0200 Subject: [PATCH 03/10] Fix file descriptor leak when FileResponse raises an exception --- photo21/views.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/photo21/views.py b/photo21/views.py index 434be5b..bedd2a1 100644 --- a/photo21/views.py +++ b/photo21/views.py @@ -23,9 +23,14 @@ class MediaAccess(View): raise Http404 if not os.path.isfile(file_path): raise Http404 - response = FileResponse(open(file_path, 'rb')) - response['Cache-Control'] = 'max-age=2678400' - return response + f = open(file_path, 'rb') + try: + response = FileResponse(f) + response['Cache-Control'] = 'max-age=2678400' + return response + except Exception: + f.close() + raise class IndexView(LoginRequiredMixin, ListView): From 1621e7c17b056173029e5b2ad457fdc849d3f328 Mon Sep 17 00:00:00 2001 From: krek0 Date: Thu, 7 May 2026 13:54:58 +0200 Subject: [PATCH 04/10] remove useless |saf tag --- photologue/templates/photologue/gallery_detail.html | 2 +- photologue/templates/photologue/photo_detail.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/photologue/templates/photologue/gallery_detail.html b/photologue/templates/photologue/gallery_detail.html index 8e704d8..5364548 100755 --- a/photologue/templates/photologue/gallery_detail.html +++ b/photologue/templates/photologue/gallery_detail.html @@ -52,7 +52,7 @@ SPDX-License-Identifier: GPL-3.0-or-later {% endfor %}

{% endif %} -{% if gallery.description %}

{{ gallery.description|safe|linebreaksbr }}

{% endif %} +{% if gallery.description %}

{{ gallery.description|linebreaksbr }}

{% endif %}
diff --git a/photologue/templates/photologue/photo_detail.html b/photologue/templates/photologue/photo_detail.html index a936847..0c40697 100644 --- a/photologue/templates/photologue/photo_detail.html +++ b/photologue/templates/photologue/photo_detail.html @@ -15,7 +15,7 @@ SPDX-License-Identifier: GPL-3.0-or-later

{% trans "Published" %} {{ object.date_added }}

-{% if object.caption %}

{{ object.caption|safe }}

{% endif %} +{% if object.caption %}

{{ object.caption }}

{% endif %} {{ object.title }} From 753f0889e2094ec133732fd84e4b263d2a9c8919 Mon Sep 17 00:00:00 2001 From: krek0 Date: Thu, 7 May 2026 14:04:23 +0200 Subject: [PATCH 05/10] avoid uselsse exif() calls --- photologue/models.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/photologue/models.py b/photologue/models.py index 99b937c..e304b5c 100644 --- a/photologue/models.py +++ b/photologue/models.py @@ -408,14 +408,15 @@ class ImageModel(models.Model): # Save the original format im_format = im.format # Rotate if found & necessary + exif = self.exif() if ( - "Image Orientation" in self.exif() - and self.exif().get("Image Orientation").values[0] + "Image Orientation" in exif + and exif.get("Image Orientation").values[0] in IMAGE_EXIF_ORIENTATION_MAP ): im = im.transpose( IMAGE_EXIF_ORIENTATION_MAP[ - self.exif().get("Image Orientation").values[0] + exif.get("Image Orientation").values[0] ] ) # Resize/crop image From b916a00c353780f373257ac7cea3d361e87eea9c Mon Sep 17 00:00:00 2001 From: krek0 Date: Thu, 7 May 2026 14:06:18 +0200 Subject: [PATCH 06/10] Fix gallery card image height inconsistency --- photologue/templates/photologue/includes/gallery_sample.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/photologue/templates/photologue/includes/gallery_sample.html b/photologue/templates/photologue/includes/gallery_sample.html index 7210415..f91dd7f 100644 --- a/photologue/templates/photologue/includes/gallery_sample.html +++ b/photologue/templates/photologue/includes/gallery_sample.html @@ -7,7 +7,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% for photo in gallery.sample %} - {{ photo.title }} + {{ photo.title }} {% endfor %}

{{ gallery.title }}

From ed28d4a9c42c163811f34627a0f4e2669f412b62 Mon Sep 17 00:00:00 2001 From: krek0 Date: Thu, 7 May 2026 14:08:13 +0200 Subject: [PATCH 07/10] Add allauth_oauth app to sync user fields on OAuth login --- allauth_oauth/apps.py | 12 ++++++++++++ allauth_oauth/signals.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 allauth_oauth/apps.py create mode 100644 allauth_oauth/signals.py diff --git a/allauth_oauth/apps.py b/allauth_oauth/apps.py new file mode 100644 index 0000000..69b727a --- /dev/null +++ b/allauth_oauth/apps.py @@ -0,0 +1,12 @@ +# This file is part of photo21 +# Copyright (C) 2022 Amicale des élèves de l'ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from django.apps import AppConfig + + +class AllauthOAuthConfig(AppConfig): + name = "allauth_oauth" + + def ready(self): + import allauth_oauth.signals # noqa: F401 diff --git a/allauth_oauth/signals.py b/allauth_oauth/signals.py new file mode 100644 index 0000000..d7f1cf6 --- /dev/null +++ b/allauth_oauth/signals.py @@ -0,0 +1,29 @@ +# This file is part of photo21 +# Copyright (C) 2022 Amicale des élèves de l'ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from allauth.socialaccount.signals import pre_social_login +from django.dispatch import receiver + + +@receiver(pre_social_login) +def sync_user_fields(sender, request, sociallogin, **kwargs): + if not sociallogin.is_existing: + return + + user = sociallogin.user + data = sociallogin.account.extra_data + changed = False + + email = data.get("email") + if email and user.email != email: + user.email = email + changed = True + + username = data.get("username") + if username and user.username != username: + user.username = username + changed = True + + if changed: + user.save() From 72e93441023ed81c11af113112fc5d024e6d5274 Mon Sep 17 00:00:00 2001 From: krek0 Date: Mon, 11 May 2026 14:24:35 +0200 Subject: [PATCH 08/10] Fix gallery photo deletion gap and uncensor UI glitche. --- photologue/locale/fr/LC_MESSAGES/django.po | 4 ++++ photologue/static/gallery_justified.js | 1 + .../static/lightgallery/plugins/admin/lg-admin.js | 9 ++++++++- .../templates/photologue/gallery_detail.html | 14 +++++++++++++- 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/photologue/locale/fr/LC_MESSAGES/django.po b/photologue/locale/fr/LC_MESSAGES/django.po index b60677f..2ac7b65 100644 --- a/photologue/locale/fr/LC_MESSAGES/django.po +++ b/photologue/locale/fr/LC_MESSAGES/django.po @@ -399,6 +399,10 @@ msgstr "Aucune galerie trouvée." msgid "to" msgstr "au" +#: photologue/templates/photologue/gallery_detail.html:49 +msgid "censored photos" +msgstr "photos censurées" + #: photologue/templates/photologue/gallery_detail.html:53 msgid "Public link:" msgstr "" diff --git a/photologue/static/gallery_justified.js b/photologue/static/gallery_justified.js index 80dcc55..e162011 100644 --- a/photologue/static/gallery_justified.js +++ b/photologue/static/gallery_justified.js @@ -70,6 +70,7 @@ }); } + window.applyJustifiedLayout = applyLayout; document.addEventListener('DOMContentLoaded', applyLayout); window.addEventListener('resize', applyLayout); })(); diff --git a/photologue/static/lightgallery/plugins/admin/lg-admin.js b/photologue/static/lightgallery/plugins/admin/lg-admin.js index aa0214d..0c44cc4 100644 --- a/photologue/static/lightgallery/plugins/admin/lg-admin.js +++ b/photologue/static/lightgallery/plugins/admin/lg-admin.js @@ -76,9 +76,15 @@ class lgAdmin { if (el) { el.dataset.isPublic = 'true'; const img = el.querySelector('img'); - if (img) img.classList.remove('border-danger', 'border-5'); + if (img) img.classList.remove('border-danger', 'border-5', 'photo-private'); } document.getElementById("lg-restore").style.display = 'none'; + const countSpan = document.getElementById('private-count'); + if (countSpan) { + const newCount = parseInt(countSpan.textContent, 10) - 1; + if (newCount <= 0) document.getElementById('private-count-line').remove(); + else countSpan.textContent = newCount; + } }); } } @@ -101,6 +107,7 @@ class lgAdmin { this.core.LGel.off('lgAfterSlide.adminRemove'); const thumb = document.querySelector(`[data-slide-name='${photoId}']`); if (thumb) thumb.remove(); + if (typeof window.applyJustifiedLayout === 'function') window.applyJustifiedLayout(); const lgId = this.core.lgId; const deletedItem = document.getElementById(`lg-item-${lgId}-${currentIndex}`); if (deletedItem) deletedItem.remove(); diff --git a/photologue/templates/photologue/gallery_detail.html b/photologue/templates/photologue/gallery_detail.html index 5364548..402e0c1 100755 --- a/photologue/templates/photologue/gallery_detail.html +++ b/photologue/templates/photologue/gallery_detail.html @@ -44,7 +44,19 @@ SPDX-License-Identifier: GPL-3.0-or-later {% endif %} {% if gallery.date_start %}

{{ gallery.date_start }}{% if gallery.date_end and gallery.date_end != gallery.date_start %} {% trans "to" %} {{ gallery.date_end }}{% endif %}

{% endif %} -{% if request.user.is_staff and gallery.photo_private_count %}

{{ gallery.photo_private_count }} photos censurées

{% endif %} +{% if request.user.is_staff and gallery.photo_private_count %}

{{ gallery.photo_private_count }} {% trans "censored photos" %}

{% endif %} +{% if request.user.is_staff %} + {% if public_url %} + + {% else %} +
{% csrf_token %}
+ {% endif %} +{% endif %} {% if gallery.tags.all %}

Tags : {% for tag in gallery.tags.all %} From 1cdd1dce2687e2aef4dc859397fd76e6791d31db Mon Sep 17 00:00:00 2001 From: krek0 Date: Mon, 11 May 2026 15:49:46 +0200 Subject: [PATCH 09/10] Add configurable per-gallery public access --- photo21/templates/base.html | 2 + photo21/views.py | 22 ++++++- photologue/admin.py | 3 +- photologue/locale/fr/LC_MESSAGES/django.po | 20 +++---- ...lter_photo_options_gallery_public_token.py | 22 +++++++ photologue/models.py | 5 ++ .../lightgallery/plugins/admin/lg-admin.js | 1 - .../templates/photologue/gallery_detail.html | 30 +++++----- photologue/urls.py | 2 + photologue/views.py | 57 ++++++++++++++----- 10 files changed, 119 insertions(+), 45 deletions(-) create mode 100644 photologue/migrations/0009_alter_photo_options_gallery_public_token.py diff --git a/photo21/templates/base.html b/photo21/templates/base.html index 91f63cb..ccd17c2 100644 --- a/photo21/templates/base.html +++ b/photo21/templates/base.html @@ -37,6 +37,7 @@ SPDX-License-Identifier: GPL-3.0-or-later

{% for photo in photos %} - + {{ photo.title }}{% if photo.date_taken %} - {{ photo.date_taken|date }} {{ photo.date_taken|time }}{% endif %}{% if photo.owner.get_full_name %} - {{ photo.owner.get_full_name }}{% else %} - {{ photo.owner.username }}{% endif %}{% if photo.license %} - {{ photo.license }}{% endif %}{% if not photo.is_public %} - !PRIVATE!{% endif %} {% endfor %} diff --git a/photologue/urls.py b/photologue/urls.py index 53379e4..0acc12d 100644 --- a/photologue/urls.py +++ b/photologue/urls.py @@ -9,6 +9,7 @@ from .views import ( GalleryArchiveIndexView, GalleryDetailView, GalleryDownload, + GalleryPublicToggleView, GalleryUpload, GalleryYearArchiveView, PhotoDeleteView, @@ -44,4 +45,5 @@ urlpatterns = [ path("photo//report/", PhotoReportView.as_view(), name="pl-photo-report"), path("photo//uncensor/", PhotoUncensorView.as_view(), name="pl-photo-uncensor"), path("upload/", GalleryUpload.as_view(), name="pl-gallery-upload"), + path("gallery//toggle-public/", GalleryPublicToggleView.as_view(), name="pl-gallery-toggle-public"), ] diff --git a/photologue/views.py b/photologue/views.py index 0cf9ab3..6af85fd 100644 --- a/photologue/views.py +++ b/photologue/views.py @@ -4,23 +4,23 @@ import os import zipfile -from io import BytesIO from pathlib import Path from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin from django.core.mail import mail_admins +from django.conf import settings from django.db import transaction from django.http import HttpResponse from django.http import JsonResponse -from django.shortcuts import redirect -from django.urls import reverse_lazy +from django.shortcuts import get_object_or_404, redirect +from django.urls import reverse, reverse_lazy from django.utils.text import slugify +from django.views import View from django.views.generic.dates import ArchiveIndexView, YearArchiveView from django.views.generic.detail import DetailView from django.views.generic.edit import DeleteView, FormView -from django.conf import settings from PIL import Image from django.contrib.auth import get_user_model @@ -93,10 +93,18 @@ class PhotoDeleteView(LoginRequiredMixin, DeleteView): return reverse_lazy("photologue:pl-gallery", args=[slug]) -class PhotoReportView(LoginRequiredMixin, DetailView): +class PhotoReportView(DetailView): model = Photo template_name = "photologue/photo_confirm_report.html" + def dispatch(self, request, *args, **kwargs): + if not request.user.is_authenticated: + photo = self.get_object() + if not photo.galleries.filter(is_public=True).exists(): + from django.contrib.auth.views import redirect_to_login + return redirect_to_login(request.get_full_path()) + return super().dispatch(request, *args, **kwargs) + def post(self, request, *args, **kwargs): """ Make photo private on POST. @@ -115,9 +123,10 @@ class PhotoReportView(LoginRequiredMixin, DetailView): url = request.build_absolute_uri(url) # Send mail to managers + reporter = request.user.username if request.user.is_authenticated else "Anonymous (public link)" mail_admins( subject=f"Abuse report for photo id {photo.pk}", - message=f"{self.request.user.username} reported an abuse for `{photo.title}`: {url}#lg=1&slide={photo.pk}", + message=f"{reporter} reported an abuse for `{photo.title}`: {url}#lg=1&slide={photo.pk}", ) # Redirect to gallery @@ -150,13 +159,21 @@ class TagDetail(LoginRequiredMixin, DetailView): return context -class GalleryDetailView(LoginRequiredMixin, DetailView): +class GalleryDetailView(DetailView): """ Gallery detail view to filter on photo owner """ model = Gallery + def dispatch(self, request, *args, **kwargs): + if not request.user.is_authenticated: + self.object = self.get_object() + if not self.object.is_public: + from django.contrib.auth.views import redirect_to_login + return redirect_to_login(request.get_full_path()) + return super().dispatch(request, *args, **kwargs) + def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -192,10 +209,18 @@ class GalleryDetailView(LoginRequiredMixin, DetailView): return context -class GalleryDownload(LoginRequiredMixin, DetailView): +class GalleryDownload(DetailView): ### IN FUTURE, PUT IT as Django Task model = Gallery + def dispatch(self, request, *args, **kwargs): + if not request.user.is_authenticated: + self.object = self.get_object() + if not self.object.is_public: + from django.contrib.auth.views import redirect_to_login + return redirect_to_login(request.get_full_path()) + return super().dispatch(request, *args, **kwargs) + def get(self, request, *args, **kwargs): """ Download a zip file of the gallery on GET request. @@ -218,13 +243,17 @@ class GalleryDownload(LoginRequiredMixin, DetailView): return redirect( (settings.MEDIA_URL + str(gallery_zip)).replace("\\", "/") ) # windows fix - # Return zip file - # response = HttpResponse( - # byte_data.getvalue(), content_type="application/x-zip-compressed" - # ) - # response["Content-Disposition"] = f"attachment; filename={gallery.slug}.zip" - # return response + +class GalleryPublicToggleView(LoginRequiredMixin, View): + def post(self, request, slug): + if not request.user.is_staff: + from django.core.exceptions import PermissionDenied + raise PermissionDenied + gallery = get_object_or_404(Gallery, slug=slug) + gallery.is_public = not gallery.is_public + gallery.save() + return redirect(reverse("photologue:pl-gallery", args=[slug])) class GalleryUpload(PermissionRequiredMixin, FormView): From a634cc88bd3e8be767158e06fa57e6353ede155e Mon Sep 17 00:00:00 2001 From: krek0 Date: Mon, 11 May 2026 17:49:11 +0200 Subject: [PATCH 10/10] Fix Select2 tag autocomplete failing with multiple gunicorn workers. --- photo21/settings.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/photo21/settings.py b/photo21/settings.py index 953a8c2..422f2c7 100644 --- a/photo21/settings.py +++ b/photo21/settings.py @@ -166,9 +166,15 @@ CACHES = { "default": { "BACKEND": "django.core.cache.backends.locmem.LocMemCache", "LOCATION": "Master", - } + }, + "select2": { + "BACKEND": "django.core.cache.backends.filebased.FileBasedCache", + "LOCATION": "/tmp/django_select2_cache", + }, } +SELECT2_CACHE_BACKEND = "select2" + # Password validation # https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators