From f5ccb40e1604a93c52e1b710a4cd6b25fbcf7c85 Mon Sep 17 00:00:00 2001 From: krek0 Date: Wed, 22 Apr 2026 08:31:07 +0200 Subject: [PATCH] Add can_resolve_censorship permission and uncensor feature - Add custom permission to Photo model - Add PhotoUncensorView POST endpoint to restore private photos - Show private photos to users with can_resolve_censorship - Add restore button in lightgallery toolbar for censored photos --- ...5_add_can_resolve_censorship_permission.py | 17 ++++++++++ photologue/models.py | 3 ++ .../lightgallery/plugins/admin/lg-admin.js | 32 +++++++++++++++++++ .../templates/photologue/gallery_detail.html | 3 +- photologue/urls.py | 2 ++ photologue/views.py | 16 +++++++++- 6 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 photologue/migrations/0005_add_can_resolve_censorship_permission.py diff --git a/photologue/migrations/0005_add_can_resolve_censorship_permission.py b/photologue/migrations/0005_add_can_resolve_censorship_permission.py new file mode 100644 index 0000000..4393519 --- /dev/null +++ b/photologue/migrations/0005_add_can_resolve_censorship_permission.py @@ -0,0 +1,17 @@ +# Generated by Django 5.2.13 on 2026-04-21 09:07 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('photologue', '0004_alter_photo_options_gallery_public_token'), + ] + + operations = [ + migrations.AlterModelOptions( + name='photo', + options={'get_latest_by': 'date_added', 'ordering': ['title'], 'permissions': [('can_resolve_censorship', 'Can resolve censorship (restore private photos)')], 'verbose_name': 'photo', 'verbose_name_plural': 'photos'}, + ), + ] diff --git a/photologue/models.py b/photologue/models.py index 3ca67b5..a466c92 100644 --- a/photologue/models.py +++ b/photologue/models.py @@ -553,6 +553,9 @@ class Photo(ImageModel): get_latest_by = "date_added" verbose_name = _("photo") verbose_name_plural = _("photos") + permissions = [ + ("can_resolve_censorship", "Can resolve censorship (restore private photos)"), + ] def __str__(self): return self.title diff --git a/photologue/static/lightgallery/plugins/admin/lg-admin.js b/photologue/static/lightgallery/plugins/admin/lg-admin.js index a6748e1..5b198c9 100644 --- a/photologue/static/lightgallery/plugins/admin/lg-admin.js +++ b/photologue/static/lightgallery/plugins/admin/lg-admin.js @@ -12,6 +12,7 @@ class lgAdmin { this.$LG = $LG; this.isStaff = document.querySelector('[name=is_staff]').value === "true"; this.userId = document.querySelector('[name=user_id]').value; + this.canResolveCensorship = document.querySelector('[name=can_resolve_censorship]').value === "true"; this.csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value; this.photoId = 0; return this; @@ -35,6 +36,12 @@ class lgAdmin { this.core.$toolbar.append(`${reportIcon}`); document.getElementById("lg-report").addEventListener('click', this.onReport.bind(this)); + // Add button to restore a censored photo + const restoreIcon = ""; + this.core.$toolbar.append(`${restoreIcon}`); + document.getElementById("lg-restore").style.display = 'none'; + document.getElementById("lg-restore").addEventListener('click', this.onRestore.bind(this)); + this.core.LGel.on("lgAfterSlide.admin", this.onAfterSlide.bind(this)); } @@ -46,6 +53,31 @@ class lgAdmin { const ownerId = el ? el.dataset.ownerId : null; const canDelete = this.isStaff || (ownerId && ownerId === this.userId); document.getElementById("lg-delete").style.display = canDelete ? 'block' : 'none'; + const isCensored = el ? el.dataset.isPublic === 'false' : false; + document.getElementById("lg-restore").style.display = (this.canResolveCensorship && isCensored) ? 'block' : 'none'; + } + + // Event called when user clicks the restore (uncensor) button + onRestore(event) { + event.preventDefault(); + if(confirm("Restore this photo and make it public again?")) { + const photoId = this.photoId; + let data = new FormData(); + data.append('csrfmiddlewaretoken', this.csrfToken); + fetch(`/photo/${photoId}/uncensor/`, { + method: "POST", + body: data, + credentials: 'same-origin', + }).then(() => { + const el = document.querySelector(`[data-slide-name='${photoId}']`); + if (el) { + el.dataset.isPublic = 'true'; + const img = el.querySelector('img'); + if (img) img.classList.remove('border-danger', 'border-5'); + } + document.getElementById("lg-restore").style.display = 'none'; + }); + } } // Navigate away from a photo that was just deleted/hidden. diff --git a/photologue/templates/photologue/gallery_detail.html b/photologue/templates/photologue/gallery_detail.html index 9771b23..a8b9d18 100644 --- a/photologue/templates/photologue/gallery_detail.html +++ b/photologue/templates/photologue/gallery_detail.html @@ -19,6 +19,7 @@ SPDX-License-Identifier: GPL-3.0-or-later {% csrf_token %} + @@ -88,7 +89,7 @@ SPDX-License-Identifier: GPL-3.0-or-later {% endif %}
{% 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 003109c..28a25a7 100644 --- a/photologue/urls.py +++ b/photologue/urls.py @@ -15,6 +15,7 @@ from .views import ( PhotoDeleteView, PhotoDetailView, PhotoReportView, + PhotoUncensorView, TagDetail, ) @@ -41,6 +42,7 @@ urlpatterns = [ path("photo//", PhotoDetailView.as_view(), name="pl-photo"), path("photo//delete/", PhotoDeleteView.as_view(), name="pl-photo-delete"), 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("share//", GalleryPublicView.as_view(), name="pl-gallery-public"), path("gallery//token/", GalleryTokenView.as_view(), name="pl-gallery-token"), diff --git a/photologue/views.py b/photologue/views.py index ba7eeea..22074fc 100644 --- a/photologue/views.py +++ b/photologue/views.py @@ -121,6 +121,17 @@ class PhotoReportView(DetailView): return redirect(url) +class PhotoUncensorView(LoginRequiredMixin, View): + def post(self, request, pk): + if not request.user.has_perm("photologue.can_resolve_censorship"): + from django.core.exceptions import PermissionDenied + raise PermissionDenied + photo = get_object_or_404(Photo, pk=pk) + photo.is_public = True + photo.save() + return JsonResponse({"ok": True}) + + class TagDetail(LoginRequiredMixin, DetailView): model = Tag @@ -146,8 +157,11 @@ class GalleryDetailView(LoginRequiredMixin, DetailView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) + can_resolve = self.request.user.has_perm("photologue.can_resolve_censorship") + context["can_resolve_censorship"] = can_resolve + # Non-staff members only see public photos + prefetch all owners informations (Optimisation) - if self.request.user.is_staff: + if self.request.user.is_staff or can_resolve: context["photos"] = self.object.photos.all().select_related("owner") else: context["photos"] = self.object.photos.filter(