From 5348a451e949aca6ddeccce9f703d6fee34bdf36 Mon Sep 17 00:00:00 2001 From: imperosol Date: Thu, 10 Oct 2024 18:53:49 +0200 Subject: [PATCH] feat: picture moderation requests --- locale/fr/LC_MESSAGES/django.po | 59 ++++++++++++---- sas/admin.py | 15 +++- sas/forms.py | 36 +++++++++- .../0004_picturemoderationrequest_and_more.py | 68 +++++++++++++++++++ sas/models.py | 41 +++++++++-- sas/templates/sas/ask_picture_removal.jinja | 28 ++++++++ sas/templates/sas/picture.jinja | 4 +- sas/urls.py | 5 ++ sas/views.py | 52 +++++++++++--- 9 files changed, 276 insertions(+), 32 deletions(-) create mode 100644 sas/migrations/0004_picturemoderationrequest_and_more.py create mode 100644 sas/templates/sas/ask_picture_removal.jinja diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 67055c96..df24f703 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -616,7 +616,7 @@ msgstr "No" #: counter/templates/counter/last_ops.jinja:20 #: counter/templates/counter/last_ops.jinja:45 #: counter/templates/counter/refilling_list.jinja:16 -#: rootplace/templates/rootplace/logs.jinja:12 sas/views.py:310 +#: rootplace/templates/rootplace/logs.jinja:12 sas/forms.py:90 #: trombi/templates/trombi/user_profile.jinja:40 msgid "Date" msgstr "Date" @@ -997,7 +997,7 @@ msgstr "Vous ne pouvez pas ajouter deux fois le même utilisateur" msgid "You should specify a role" msgstr "Vous devez choisir un rôle" -#: club/forms.py:283 sas/views.py:52 sas/views.py:176 +#: club/forms.py:283 sas/views.py:58 sas/views.py:177 msgid "You do not have the permission to do that" msgstr "Vous n'avez pas la permission de faire cela" @@ -1613,7 +1613,7 @@ msgstr "Résumé" #: com/templates/com/news_admin_list.jinja:252 #: com/templates/com/news_admin_list.jinja:289 #: com/templates/com/weekmail.jinja:17 com/templates/com/weekmail.jinja:46 -#: forum/templates/forum/forum.jinja:55 +#: forum/templates/forum/forum.jinja:55 sas/models.py:298 msgid "Author" msgstr "Auteur" @@ -2246,7 +2246,7 @@ msgstr "avoir une notification pour chaque click" msgid "get a notification for every refilling" msgstr "avoir une notification pour chaque rechargement" -#: core/models.py:914 sas/forms.py:86 +#: core/models.py:914 sas/forms.py:89 msgid "file name" msgstr "nom du fichier" @@ -2582,6 +2582,7 @@ msgstr "Confirmation" #: core/templates/core/delete_confirm.jinja:20 #: core/templates/core/file_delete_confirm.jinja:14 #: counter/templates/counter/counter_click.jinja:121 +#: sas/templates/sas/ask_picture_removal.jinja:20 msgid "Cancel" msgstr "Annuler" @@ -3496,12 +3497,12 @@ msgid "Error creating folder %(folder_name)s: %(msg)s" msgstr "Erreur de création du dossier %(folder_name)s : %(msg)s" #: core/views/files.py:153 core/views/forms.py:277 core/views/forms.py:284 -#: sas/forms.py:57 +#: sas/forms.py:60 #, python-format msgid "Error uploading file %(file_name)s: %(msg)s" msgstr "Erreur d'envoi du fichier %(file_name)s : %(msg)s" -#: core/views/files.py:228 sas/forms.py:90 +#: core/views/files.py:228 sas/forms.py:93 msgid "Apply rights recursively" msgstr "Appliquer les droits récursivement" @@ -3996,7 +3997,7 @@ msgstr "Ce n'est pas un UID de carte étudiante valide" #: counter/templates/counter/invoices_call.jinja:16 #: launderette/templates/launderette/launderette_admin.jinja:35 #: launderette/templates/launderette/launderette_click.jinja:13 -#: sas/templates/sas/picture.jinja:141 +#: sas/templates/sas/picture.jinja:143 #: subscription/templates/subscription/stats.jinja:19 msgid "Go" msgstr "Valider" @@ -5099,7 +5100,7 @@ msgstr "non noté" msgid "UV comment moderation" msgstr "Modération des commentaires d'UV" -#: pedagogy/templates/pedagogy/moderation.jinja:14 +#: pedagogy/templates/pedagogy/moderation.jinja:14 sas/models.py:309 msgid "Reason" msgstr "Raison" @@ -5267,27 +5268,47 @@ msgstr "Utilisateur qui sera supprimé" msgid "User to be selected" msgstr "Utilisateur à sélectionner" -#: sas/forms.py:13 +#: sas/forms.py:16 msgid "Add a new album" msgstr "Ajouter un nouvel album" -#: sas/forms.py:16 +#: sas/forms.py:19 msgid "Upload images" msgstr "Envoyer les images" -#: sas/forms.py:34 +#: sas/forms.py:37 #, python-format msgid "Error creating album %(album)s: %(msg)s" msgstr "Erreur de création de l'album %(album)s : %(msg)s" -#: sas/forms.py:69 trombi/templates/trombi/detail.jinja:15 +#: sas/forms.py:72 trombi/templates/trombi/detail.jinja:15 msgid "Add user" msgstr "Ajouter une personne" -#: sas/models.py:282 +#: sas/forms.py:117 +msgid "You already requested moderation for this picture." +msgstr "Vous avez déjà déposé une demande de retrait pour cette photo." + +#: sas/models.py:280 msgid "picture" msgstr "photo" +#: sas/models.py:304 +msgid "Picture" +msgstr "Photo" + +#: sas/models.py:311 +msgid "Why do you want this image to be removed ?" +msgstr "Pourquoi voulez-vous retirer cette image ?" + +#: sas/models.py:315 +msgid "Picture moderation request" +msgstr "Demande de modération de photo" + +#: sas/models.py:316 +msgid "Picture moderation requests" +msgstr "Demandes de modération de photo" + #: sas/templates/sas/album.jinja:9 #: sas/templates/sas/ask_picture_removal.jinja:4 sas/templates/sas/main.jinja:8 #: sas/templates/sas/main.jinja:17 sas/templates/sas/picture.jinja:12 @@ -5306,6 +5327,14 @@ msgstr "Envoyer" msgid "Template generation time: " msgstr "Temps de génération du template : " +#: sas/templates/sas/ask_picture_removal.jinja:9 +msgid "Image removal request" +msgstr "Demande de retrait d'image" + +#: sas/templates/sas/ask_picture_removal.jinja:25 +msgid "Request removal" +msgstr "Demander le retrait" + #: sas/templates/sas/main.jinja:20 msgid "You must be logged in to see the SAS." msgstr "Vous devez être connecté pour voir les photos." @@ -5339,7 +5368,7 @@ msgstr "" msgid "HD version" msgstr "Version HD" -#: sas/templates/sas/picture.jinja:98 +#: sas/templates/sas/picture.jinja:99 msgid "Ask for removal" msgstr "Demander le retrait" @@ -5347,7 +5376,7 @@ msgstr "Demander le retrait" msgid "Previous picture" msgstr "Image précédente" -#: sas/templates/sas/picture.jinja:137 +#: sas/templates/sas/picture.jinja:139 msgid "People" msgstr "Personne(s)" diff --git a/sas/admin.py b/sas/admin.py index f2845ad3..ac980341 100644 --- a/sas/admin.py +++ b/sas/admin.py @@ -15,7 +15,7 @@ from django.contrib import admin -from sas.models import Album, PeoplePictureRelation, Picture +from sas.models import Album, PeoplePictureRelation, Picture, PictureModerationRequest @admin.register(Picture) @@ -31,4 +31,15 @@ class PeoplePictureRelationAdmin(admin.ModelAdmin): autocomplete_fields = ("picture", "user") -admin.site.register(Album) +@admin.register(Album) +class AlbumAdmin(admin.ModelAdmin): + list_display = ("name", "parent", "date", "owner", "is_moderated") + search_fields = ("name",) + autocomplete_fields = ("owner", "parent", "edit_groups", "view_groups") + + +@admin.register(PictureModerationRequest) +class PictureModerationRequestAdmin(admin.ModelAdmin): + list_display = ("author", "picture", "created_at") + search_fields = ("author", "picture") + autocomplete_fields = ("author", "picture") diff --git a/sas/forms.py b/sas/forms.py index 4750dab9..6569e92a 100644 --- a/sas/forms.py +++ b/sas/forms.py @@ -1,11 +1,14 @@ +from typing import Any + from ajax_select import make_ajax_field from ajax_select.fields import AutoCompleteSelectMultipleField from django import forms from django.utils.translation import gettext_lazy as _ +from core.models import User from core.views import MultipleImageField from core.views.forms import SelectDate -from sas.models import Album, PeoplePictureRelation, Picture +from sas.models import Album, PeoplePictureRelation, Picture, PictureModerationRequest class SASForm(forms.Form): @@ -88,3 +91,34 @@ class AlbumEditForm(forms.ModelForm): parent = make_ajax_field(Album, "parent", "files", help_text="") edit_groups = make_ajax_field(Album, "edit_groups", "groups", help_text="") recursive = forms.BooleanField(label=_("Apply rights recursively"), required=False) + + +class PictureModerationRequestForm(forms.ModelForm): + """Form to create a PictureModerationRequest. + + The form only manages the reason field, + because the author and the picture are set in the view. + """ + + class Meta: + model = PictureModerationRequest + fields = ["reason"] + + def __init__(self, *args, user: User, picture: Picture, **kwargs): + super().__init__(*args, **kwargs) + self.user = user + self.picture = picture + + def clean(self) -> dict[str, Any]: + if PictureModerationRequest.objects.filter( + author=self.user, picture=self.picture + ).exists(): + raise forms.ValidationError( + _("You already requested moderation for this picture.") + ) + return super().clean() + + def save(self, *, commit=True) -> PictureModerationRequest: + self.instance.author = self.user + self.instance.picture = self.picture + return super().save(commit) diff --git a/sas/migrations/0004_picturemoderationrequest_and_more.py b/sas/migrations/0004_picturemoderationrequest_and_more.py new file mode 100644 index 00000000..e07b925d --- /dev/null +++ b/sas/migrations/0004_picturemoderationrequest_and_more.py @@ -0,0 +1,68 @@ +# Generated by Django 4.2.16 on 2024-10-10 20:44 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("sas", "0003_sasfile"), + ] + + operations = [ + migrations.CreateModel( + name="PictureModerationRequest", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ( + "reason", + models.TextField( + default="", + help_text="Why do you want this image to be removed ?", + verbose_name="Reason", + ), + ), + ], + options={ + "verbose_name": "Picture moderation request", + "verbose_name_plural": "Picture moderation requests", + }, + ), + migrations.AddField( + model_name="picturemoderationrequest", + name="author", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="moderation_requests", + to=settings.AUTH_USER_MODEL, + verbose_name="Author", + ), + ), + migrations.AddField( + model_name="picturemoderationrequest", + name="picture", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="moderation_requests", + to="sas.picture", + verbose_name="Picture", + ), + ), + migrations.AddConstraint( + model_name="picturemoderationrequest", + constraint=models.UniqueConstraint( + fields=("author", "picture"), name="one_request_per_user_per_picture" + ), + ), + ] diff --git a/sas/models.py b/sas/models.py index 43f26ccd..82bc87f2 100644 --- a/sas/models.py +++ b/sas/models.py @@ -273,16 +273,12 @@ class PeoplePictureRelation(models.Model): User, verbose_name=_("user"), related_name="pictures", - null=False, - blank=False, on_delete=models.CASCADE, ) picture = models.ForeignKey( Picture, verbose_name=_("picture"), related_name="people", - null=False, - blank=False, on_delete=models.CASCADE, ) @@ -290,4 +286,39 @@ class PeoplePictureRelation(models.Model): unique_together = ["user", "picture"] def __str__(self): - return self.user.get_display_name() + " - " + str(self.picture) + return f"Moderation request by {self.user.get_short_name()} - {self.picture}" + + +class PictureModerationRequest(models.Model): + """A request to remove a Picture from the SAS.""" + + created_at = models.DateTimeField(auto_now_add=True) + author = models.ForeignKey( + User, + verbose_name=_("Author"), + related_name="moderation_requests", + on_delete=models.CASCADE, + ) + picture = models.ForeignKey( + Picture, + verbose_name=_("Picture"), + related_name="moderation_requests", + on_delete=models.CASCADE, + ) + reason = models.TextField( + verbose_name=_("Reason"), + default="", + help_text=_("Why do you want this image to be removed ?"), + ) + + class Meta: + verbose_name = _("Picture moderation request") + verbose_name_plural = _("Picture moderation requests") + constraints = [ + models.UniqueConstraint( + fields=["author", "picture"], name="one_request_per_user_per_picture" + ) + ] + + def __str__(self): + return f"Moderation request by {self.author.get_short_name()}" diff --git a/sas/templates/sas/ask_picture_removal.jinja b/sas/templates/sas/ask_picture_removal.jinja new file mode 100644 index 00000000..26c345a0 --- /dev/null +++ b/sas/templates/sas/ask_picture_removal.jinja @@ -0,0 +1,28 @@ +{% extends "core/base.jinja" %} + +{% block title %} + {% trans %}SAS{% endtrans %} +{% endblock %} + + +{% block content %} +

{% trans %}Image removal request{% endtrans %}

+
+ {% csrf_token %} + {{ form.non_field_errors() }} +
+ {{ form.reason.help_text }} +
+ {{ form.reason }} +
+
+ + {% trans %}Cancel{% endtrans %} + + +
+{% endblock content %} \ No newline at end of file diff --git a/sas/templates/sas/picture.jinja b/sas/templates/sas/picture.jinja index e1d675ba..a3582068 100644 --- a/sas/templates/sas/picture.jinja +++ b/sas/templates/sas/picture.jinja @@ -96,7 +96,9 @@ {% trans %}HD version{% endtrans %}
- {% trans %}Ask for removal{% endtrans %} + + {% trans %}Ask for removal{% endtrans %} +
diff --git a/sas/urls.py b/sas/urls.py index ad7da367..845a66ac 100644 --- a/sas/urls.py +++ b/sas/urls.py @@ -34,6 +34,11 @@ urlpatterns = [ PictureEditView.as_view(), name="picture_edit", ), + path( + "picture//report", + PictureAskRemovalView.as_view(), + name="picture_ask_removal", + ), path("picture//download/", send_pict, name="download"), path( "picture//download/compressed/", diff --git a/sas/views.py b/sas/views.py index 28381c04..7a24edc8 100644 --- a/sas/views.py +++ b/sas/views.py @@ -12,11 +12,12 @@ # OR WITHIN THE LOCAL FILE "LICENSE" # # +from typing import Any from django.conf import settings from django.core.exceptions import PermissionDenied -from django.http import Http404, HttpResponse -from django.shortcuts import get_object_or_404, redirect +from django.http import Http404, HttpResponse, HttpResponseRedirect +from django.shortcuts import get_object_or_404 from django.urls import reverse, reverse_lazy from django.utils.translation import gettext_lazy as _ from django.views.generic import DetailView, TemplateView @@ -25,7 +26,12 @@ from django.views.generic.edit import FormMixin, FormView, UpdateView from core.models import SithFile, User from core.views import CanEditMixin, CanViewMixin from core.views.files import FileView, send_file -from sas.forms import AlbumEditForm, PictureEditForm, SASForm +from sas.forms import ( + AlbumEditForm, + PictureEditForm, + PictureModerationRequestForm, + SASForm, +) from sas.models import Album, Picture @@ -73,11 +79,6 @@ class PictureView(CanViewMixin, DetailView): self.object.rotate(270) if "rotate_left" in request.GET: self.object.rotate(90) - if "ask_removal" in request.GET.keys(): - self.object.is_moderated = False - self.object.asked_for_removal = True - self.object.save() - return redirect("sas:album", album_id=self.object.parent.id) return super().get(request, *args, **kwargs) def get_context_data(self, **kwargs): @@ -235,6 +236,41 @@ class PictureEditView(CanEditMixin, UpdateView): pk_url_kwarg = "picture_id" +class PictureAskRemovalView(CanViewMixin, DetailView, FormView): + """View to allow users to ask pictures to be removed.""" + + model = Picture + template_name = "sas/ask_picture_removal.jinja" + pk_url_kwarg = "picture_id" + form_class = PictureModerationRequestForm + + def get_form_kwargs(self) -> dict[str, Any]: + """Add the user and picture to the form kwargs. + + Those are required to create the PictureModerationRequest, + and aren't part of the form itself + (picture is a path parameter, and user is the request user). + """ + return super().get_form_kwargs() | { + "user": self.request.user, + "picture": self.object, + } + + def get_success_url(self) -> str: + """Return the URL to the album containing the picture.""" + album = Album.objects.filter(pk=self.object.parent_id).first() + if not album: + return reverse("sas:main") + return album.get_absolute_url() + + def form_valid(self, form: PictureModerationRequestForm) -> HttpResponseRedirect: + form.save() + self.object.is_moderated = False + self.object.asked_for_removal = True + self.object.save() + return super().form_valid(form) + + class AlbumEditView(CanEditMixin, UpdateView): model = Album form_class = AlbumEditForm