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
This commit is contained in:
parent
40352cffee
commit
f5ccb40e16
6 changed files with 71 additions and 2 deletions
|
|
@ -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'},
|
||||
),
|
||||
]
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(`<a href="#" id="lg-report" title="Notify abuse" class="lg-icon lg-bi-icon">${reportIcon}</a>`);
|
||||
document.getElementById("lg-report").addEventListener('click', this.onReport.bind(this));
|
||||
|
||||
// Add button to restore a censored photo
|
||||
const restoreIcon = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" fill=\"currentColor\" class=\"bi\" viewBox=\"0 0 16 16\"><path d=\"M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z\"/><path d=\"M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z\"/></svg>";
|
||||
this.core.$toolbar.append(`<a href="#" id="lg-restore" title="Restore photo (remove censorship)" class="lg-icon lg-bi-icon">${restoreIcon}</a>`);
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||
{% csrf_token %}
|
||||
<input type="hidden" name="is_staff" value="{{ request.user.is_staff|yesno:'true,false' }}">
|
||||
<input type="hidden" name="user_id" value="{{ request.user.id }}">
|
||||
<input type="hidden" name="can_resolve_censorship" value="{{ can_resolve_censorship|yesno:'true,false' }}">
|
||||
<script src="{% static 'lightgallery/lightgallery.min.js' %}"></script>
|
||||
<script src="{% static 'lightgallery/plugins/admin/lg-admin.js' %}"></script>
|
||||
<script src="{% static 'lightgallery/plugins/hash/lg-hash.min.js' %}"></script>
|
||||
|
|
@ -88,7 +89,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||
{% endif %}
|
||||
<div class="card-body row" id="lightgallery">
|
||||
{% for photo in photos %}
|
||||
<a class="col-6 col-md-3 mb-2 text-center" href="{{ photo.get_absolute_url }}" data-src="{{ photo.get_display_url}}" data-download-url="{{ photo.image.url }}" data-slide-name="{{ photo.id }}" data-owner-id="{{ photo.owner.id }}">
|
||||
<a class="col-6 col-md-3 mb-2 text-center" href="{{ photo.get_absolute_url }}" data-src="{{ photo.get_display_url}}" data-download-url="{{ photo.image.url }}" data-slide-name="{{ photo.id }}" data-owner-id="{{ photo.owner.id }}" data-is-public="{{ photo.is_public|yesno:'true,false' }}">
|
||||
<img src="{{ photo.get_thumbnail_url }}" loading="lazy" class="img-thumbnail p-0{% if not photo.is_public %} border-danger border-5{% endif %}" alt="{{ 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 %}">
|
||||
</a>
|
||||
{% endfor %}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ from .views import (
|
|||
PhotoDeleteView,
|
||||
PhotoDetailView,
|
||||
PhotoReportView,
|
||||
PhotoUncensorView,
|
||||
TagDetail,
|
||||
)
|
||||
|
||||
|
|
@ -41,6 +42,7 @@ urlpatterns = [
|
|||
path("photo/<int:pk>/", PhotoDetailView.as_view(), name="pl-photo"),
|
||||
path("photo/<int:pk>/delete/", PhotoDeleteView.as_view(), name="pl-photo-delete"),
|
||||
path("photo/<int:pk>/report/", PhotoReportView.as_view(), name="pl-photo-report"),
|
||||
path("photo/<int:pk>/uncensor/", PhotoUncensorView.as_view(), name="pl-photo-uncensor"),
|
||||
path("upload/", GalleryUpload.as_view(), name="pl-gallery-upload"),
|
||||
path("share/<uuid:token>/", GalleryPublicView.as_view(), name="pl-gallery-public"),
|
||||
path("gallery/<slug:slug>/token/", GalleryTokenView.as_view(), name="pl-gallery-token"),
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue