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

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

View File

@ -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)

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,
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()}"

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 %}
</a>
<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 class="buttons">
<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(),
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/compressed/",

View File

@ -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