feat: picture moderation requests

This commit is contained in:
imperosol 2024-10-10 18:53:49 +02:00
parent 83ae21140d
commit 5348a451e9
9 changed files with 276 additions and 32 deletions

View File

@ -616,7 +616,7 @@ msgstr "No"
#: counter/templates/counter/last_ops.jinja:20 #: counter/templates/counter/last_ops.jinja:20
#: counter/templates/counter/last_ops.jinja:45 #: counter/templates/counter/last_ops.jinja:45
#: counter/templates/counter/refilling_list.jinja:16 #: 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 #: trombi/templates/trombi/user_profile.jinja:40
msgid "Date" msgid "Date"
msgstr "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" msgid "You should specify a role"
msgstr "Vous devez choisir un rôle" 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" msgid "You do not have the permission to do that"
msgstr "Vous n'avez pas la permission de faire cela" 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:252
#: com/templates/com/news_admin_list.jinja:289 #: com/templates/com/news_admin_list.jinja:289
#: com/templates/com/weekmail.jinja:17 com/templates/com/weekmail.jinja:46 #: 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" msgid "Author"
msgstr "Auteur" msgstr "Auteur"
@ -2246,7 +2246,7 @@ msgstr "avoir une notification pour chaque click"
msgid "get a notification for every refilling" msgid "get a notification for every refilling"
msgstr "avoir une notification pour chaque rechargement" 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" msgid "file name"
msgstr "nom du fichier" msgstr "nom du fichier"
@ -2582,6 +2582,7 @@ msgstr "Confirmation"
#: core/templates/core/delete_confirm.jinja:20 #: core/templates/core/delete_confirm.jinja:20
#: core/templates/core/file_delete_confirm.jinja:14 #: core/templates/core/file_delete_confirm.jinja:14
#: counter/templates/counter/counter_click.jinja:121 #: counter/templates/counter/counter_click.jinja:121
#: sas/templates/sas/ask_picture_removal.jinja:20
msgid "Cancel" msgid "Cancel"
msgstr "Annuler" 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" 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 #: 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 #, python-format
msgid "Error uploading file %(file_name)s: %(msg)s" msgid "Error uploading file %(file_name)s: %(msg)s"
msgstr "Erreur d'envoi du fichier %(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" msgid "Apply rights recursively"
msgstr "Appliquer les droits récursivement" 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 #: counter/templates/counter/invoices_call.jinja:16
#: launderette/templates/launderette/launderette_admin.jinja:35 #: launderette/templates/launderette/launderette_admin.jinja:35
#: launderette/templates/launderette/launderette_click.jinja:13 #: 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 #: subscription/templates/subscription/stats.jinja:19
msgid "Go" msgid "Go"
msgstr "Valider" msgstr "Valider"
@ -5099,7 +5100,7 @@ msgstr "non noté"
msgid "UV comment moderation" msgid "UV comment moderation"
msgstr "Modération des commentaires d'UV" 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" msgid "Reason"
msgstr "Raison" msgstr "Raison"
@ -5267,27 +5268,47 @@ msgstr "Utilisateur qui sera supprimé"
msgid "User to be selected" msgid "User to be selected"
msgstr "Utilisateur à sélectionner" msgstr "Utilisateur à sélectionner"
#: sas/forms.py:13 #: sas/forms.py:16
msgid "Add a new album" msgid "Add a new album"
msgstr "Ajouter un nouvel album" msgstr "Ajouter un nouvel album"
#: sas/forms.py:16 #: sas/forms.py:19
msgid "Upload images" msgid "Upload images"
msgstr "Envoyer les images" msgstr "Envoyer les images"
#: sas/forms.py:34 #: sas/forms.py:37
#, python-format #, python-format
msgid "Error creating album %(album)s: %(msg)s" msgid "Error creating album %(album)s: %(msg)s"
msgstr "Erreur de création de l'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" msgid "Add user"
msgstr "Ajouter une personne" 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" msgid "picture"
msgstr "photo" 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/album.jinja:9
#: sas/templates/sas/ask_picture_removal.jinja:4 sas/templates/sas/main.jinja:8 #: 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 #: sas/templates/sas/main.jinja:17 sas/templates/sas/picture.jinja:12
@ -5306,6 +5327,14 @@ msgstr "Envoyer"
msgid "Template generation time: " msgid "Template generation time: "
msgstr "Temps de génération du template : " 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 #: sas/templates/sas/main.jinja:20
msgid "You must be logged in to see the SAS." msgid "You must be logged in to see the SAS."
msgstr "Vous devez être connecté pour voir les photos." msgstr "Vous devez être connecté pour voir les photos."
@ -5339,7 +5368,7 @@ msgstr ""
msgid "HD version" msgid "HD version"
msgstr "Version HD" msgstr "Version HD"
#: sas/templates/sas/picture.jinja:98 #: sas/templates/sas/picture.jinja:99
msgid "Ask for removal" msgid "Ask for removal"
msgstr "Demander le retrait" msgstr "Demander le retrait"
@ -5347,7 +5376,7 @@ msgstr "Demander le retrait"
msgid "Previous picture" msgid "Previous picture"
msgstr "Image précédente" msgstr "Image précédente"
#: sas/templates/sas/picture.jinja:137 #: sas/templates/sas/picture.jinja:139
msgid "People" msgid "People"
msgstr "Personne(s)" msgstr "Personne(s)"

View File

@ -15,7 +15,7 @@
from django.contrib import admin from django.contrib import admin
from sas.models import Album, PeoplePictureRelation, Picture from sas.models import Album, PeoplePictureRelation, Picture, PictureModerationRequest
@admin.register(Picture) @admin.register(Picture)
@ -31,4 +31,15 @@ class PeoplePictureRelationAdmin(admin.ModelAdmin):
autocomplete_fields = ("picture", "user") 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")

View File

@ -1,11 +1,14 @@
from typing import Any
from ajax_select import make_ajax_field from ajax_select import make_ajax_field
from ajax_select.fields import AutoCompleteSelectMultipleField from ajax_select.fields import AutoCompleteSelectMultipleField
from django import forms from django import forms
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from core.models import User
from core.views import MultipleImageField from core.views import MultipleImageField
from core.views.forms import SelectDate 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): class SASForm(forms.Form):
@ -88,3 +91,34 @@ class AlbumEditForm(forms.ModelForm):
parent = make_ajax_field(Album, "parent", "files", help_text="") parent = make_ajax_field(Album, "parent", "files", help_text="")
edit_groups = make_ajax_field(Album, "edit_groups", "groups", help_text="") edit_groups = make_ajax_field(Album, "edit_groups", "groups", help_text="")
recursive = forms.BooleanField(label=_("Apply rights recursively"), required=False) 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)

View File

@ -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"
),
),
]

View File

@ -273,16 +273,12 @@ class PeoplePictureRelation(models.Model):
User, User,
verbose_name=_("user"), verbose_name=_("user"),
related_name="pictures", related_name="pictures",
null=False,
blank=False,
on_delete=models.CASCADE, on_delete=models.CASCADE,
) )
picture = models.ForeignKey( picture = models.ForeignKey(
Picture, Picture,
verbose_name=_("picture"), verbose_name=_("picture"),
related_name="people", related_name="people",
null=False,
blank=False,
on_delete=models.CASCADE, on_delete=models.CASCADE,
) )
@ -290,4 +286,39 @@ class PeoplePictureRelation(models.Model):
unique_together = ["user", "picture"] unique_together = ["user", "picture"]
def __str__(self): 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()}"

View File

@ -0,0 +1,28 @@
{% extends "core/base.jinja" %}
{% block title %}
{% trans %}SAS{% endtrans %}
{% endblock %}
{% block content %}
<h1>{% trans %}Image removal request{% endtrans %}</h1>
<form action="" method="post">
{% csrf_token %}
{{ form.non_field_errors() }}
<div class="helptext">
{{ form.reason.help_text }}
</div>
{{ form.reason }}
<br/>
<br/>
<a class="clickable btn btn-grey" href="{{ object.get_absolute_url() }}">
{% trans %}Cancel{% endtrans %}
</a>
<input
class="btn btn-blue"
type="submit"
value="{% trans %}Request removal{% endtrans %}"
>
</form>
{% endblock content %}

View File

@ -96,7 +96,9 @@
{% trans %}HD version{% endtrans %} {% trans %}HD version{% endtrans %}
</a> </a>
<br> <br>
<a class="text danger" href="?ask_removal">{% trans %}Ask for removal{% endtrans %}</a> <a class="text danger" :href="`/sas/picture/${currentPicture.id}/report`">
{% trans %}Ask for removal{% endtrans %}
</a>
</div> </div>
<div class="buttons"> <div class="buttons">
<a class="button" :href="`/sas/picture/${currentPicture.id}/edit/`"><i class="fa-regular fa-pen-to-square edit-action"></i></a> <a class="button" :href="`/sas/picture/${currentPicture.id}/edit/`"><i class="fa-regular fa-pen-to-square edit-action"></i></a>

View File

@ -34,6 +34,11 @@ urlpatterns = [
PictureEditView.as_view(), PictureEditView.as_view(),
name="picture_edit", name="picture_edit",
), ),
path(
"picture/<int:picture_id>/report",
PictureAskRemovalView.as_view(),
name="picture_ask_removal",
),
path("picture/<int:picture_id>/download/", send_pict, name="download"), path("picture/<int:picture_id>/download/", send_pict, name="download"),
path( path(
"picture/<int:picture_id>/download/compressed/", "picture/<int:picture_id>/download/compressed/",

View File

@ -12,11 +12,12 @@
# OR WITHIN THE LOCAL FILE "LICENSE" # OR WITHIN THE LOCAL FILE "LICENSE"
# #
# #
from typing import Any
from django.conf import settings from django.conf import settings
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.http import Http404, HttpResponse from django.http import Http404, HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic import DetailView, TemplateView 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.models import SithFile, User
from core.views import CanEditMixin, CanViewMixin from core.views import CanEditMixin, CanViewMixin
from core.views.files import FileView, send_file 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 from sas.models import Album, Picture
@ -73,11 +79,6 @@ class PictureView(CanViewMixin, DetailView):
self.object.rotate(270) self.object.rotate(270)
if "rotate_left" in request.GET: if "rotate_left" in request.GET:
self.object.rotate(90) 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) return super().get(request, *args, **kwargs)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
@ -235,6 +236,41 @@ class PictureEditView(CanEditMixin, UpdateView):
pk_url_kwarg = "picture_id" 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): class AlbumEditView(CanEditMixin, UpdateView):
model = Album model = Album
form_class = AlbumEditForm form_class = AlbumEditForm