diff --git a/api/api/admin/media_report.py b/api/api/admin/media_report.py index 50e4f96ad0f..c239c25f0fe 100644 --- a/api/api/admin/media_report.py +++ b/api/api/admin/media_report.py @@ -50,6 +50,25 @@ def register(site): site.register(AudioDecision, AudioDecisionAdmin) +def get_report_form(media_type: str): + report_class = { + "image": ImageReport, + "audio": AudioReport, + }[media_type] + + class MediaReportForm(forms.ModelForm): + class Meta: + fields = ["media_obj", "reason", "description"] + model = report_class + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + for field in self.fields.values(): + field.widget.attrs.update({"form": "report-create"}) + + return MediaReportForm + + class MultipleValueField(forms.MultipleChoiceField): """ This is a variant of ``MultipleChoiceField`` that does not validate @@ -142,25 +161,51 @@ def _get_default_ordering(self): return [] -class PendingRecordCountFilter(admin.SimpleListFilter): - title = "pending record count" - parameter_name = "pending_record_count" - - def choices(self, changelist): - """Set default to "pending" rather than "all".""" - choices = list(super().choices(changelist)) - choices[0]["display"] = "Pending" - return choices - - def lookups(self, request, model_admin): - return (("all", "All"),) +def get_pending_record_filter(media_type: str): + class PendingRecordCountFilter(admin.SimpleListFilter): + title = "pending record count" + parameter_name = "pending_record_count" + + def choices(self, changelist): + for lookup, title in self.lookup_choices: + yield { + "selected": self.value() == lookup, + "query_string": changelist.get_query_string( + {self.parameter_name: lookup}, [] + ), + "display": title, + } + + def lookups(self, request, model_admin): + return [ + (None, "Moderation queue"), + ("all", "All"), + ] + + def queryset(self, request, qs): + value = self.value() + if value is None: + # Filter down to only instances with reports + qs = qs.filter(**{f"{media_type}_report__isnull": False}) + + # Annotate and order by report count + qs = qs.annotate(total_report_count=Count(f"{media_type}_report")) + # Show total pending reports by subtracting the number of reports + # from the number of reports that have decisions + qs = qs.annotate( + pending_report_count=F("total_report_count") + - Count(f"{media_type}_report__decision__pk") + ) + qs = qs.annotate( + oldest_report_date=Min(f"{media_type}_report__created_at") + ) + qs = qs.order_by( + "-total_report_count", "-pending_report_count", "oldest_report_date" + ) - def queryset(self, request, queryset): - value = self.value() - if value != "all": - return queryset.filter(pending_report_count__gt=0) + return qs - return queryset + return PendingRecordCountFilter class MediaListAdmin(admin.ModelAdmin): @@ -193,9 +238,14 @@ def wrapper(*args, **kwargs): # appear just before the catch-all view. urls[-1:-1] = [ path( - "/moderate/", - wrap(self.moderate_view), - name=f"{app}_{model}_moderate", + "/report_create/", + wrap(self.report_create_view), + name=f"{app}_{model}_report_create", + ), + path( + "/decision_create/", + wrap(self.decision_create_view), + name=f"{app}_{model}_decision_create", ), path( "/lock/", @@ -238,19 +288,29 @@ def has_sensitive_text(self, obj): ############# change_list_template = "admin/api/media/change_list.html" - list_display = ( - "identifier", - "total_report_count", - "pending_report_count", - "oldest_report_date", - "pending_reports_links", - "has_sensitive_text", - ) - list_filter = (PendingRecordCountFilter,) + list_display = ("identifier",) list_display_links = ("identifier",) search_fields = _production_deferred("identifier") sortable_by = () # Ordering is defined in ``get_queryset``. + def get_list_filter(self, request): + return (get_pending_record_filter(self.media_type),) + + def get_list_display(self, request): + if request.GET.get("pending_record_count") != "all": + return self.list_display + ( + "total_report_count", + "pending_report_count", + "oldest_report_date", + "pending_reports_links", + "has_sensitive_text", + ) + else: + return self.list_display + ( + "source", + "provider", + ) + def total_report_count(self, obj): return obj.total_report_count @@ -340,6 +400,8 @@ def change_view(self, request, object_id, form_url="", extra_context=None): extra_context["mod_form"] = get_decision_form(self.media_type)() + extra_context["report_form"] = get_report_form(self.media_type)() + return super().change_view(request, object_id, form_url, extra_context) ############# @@ -363,11 +425,11 @@ def lock_view(self, request, object_id): return redirect(f"admin:api_{self.media_type}_change", object_id) - ################# - # Moderate view # - ################# + ######################## + # Decision create view # + ######################## - def moderate_view(self, request, object_id): + def decision_create_view(self, request, object_id): """ Create a decision for the media object and associate selected reports referencing the media with this decision. @@ -413,37 +475,42 @@ def moderate_view(self, request, object_id): return redirect(f"admin:api_{self.media_type}_change", object_id) + ###################### + # Report create view # + ###################### + + def report_create_view(self, request, object_id): + """Create a report for the media object.""" + + if request.method == "POST": + media_obj = self.get_object(request, object_id) + + form = get_report_form(self.media_type)(request.POST) + if form.is_valid(): + report = form.save(commit=False) + report.media_obj = media_obj + report.save() + + logger.info( + "Report created", + report=report.id, + reason=report.reason, + description=report.description, + media_obj=media_obj.id, + ) + else: + logger.warning( + "Form is invalid", + **form.cleaned_data, + errors=form.errors, + ) + + return redirect(f"admin:api_{self.media_type}_change", object_id) + ############# # Overrides # ############# - # TODO: This construct breaks down if a decision is associated with - # a media item that does not have any reports. Such an item cannot - # be reached at the URL ``admin/api/{media_type}/{id}/change/``. - def get_queryset(self, request): - qs = super().get_queryset(request) - # Return all available image if this is for an autocomplete request - if "autocomplete" in request.path: - return qs - - # Filter down to only instances with reports - qs = qs.filter(**{f"{self.media_type}_report__isnull": False}) - # Annotate and order by report count - qs = qs.annotate(total_report_count=Count(f"{self.media_type}_report")) - # Show total pending reports by subtracting the number of reports - # from the number of reports that have decisions - qs = qs.annotate( - pending_report_count=F("total_report_count") - - Count(f"{self.media_type}_report__decision__pk") - ) - qs = qs.annotate( - oldest_report_date=Min(f"{self.media_type}_report__created_at") - ) - qs = qs.order_by( - "-total_report_count", "-pending_report_count", "oldest_report_date" - ) - return qs - def get_changelist(self, request, **kwargs): return PredeterminedOrderChangelist diff --git a/api/api/templates/admin/api/components/media_complain.html b/api/api/templates/admin/api/components/media_complain.html new file mode 100644 index 00000000000..05bf8734e56 --- /dev/null +++ b/api/api/templates/admin/api/components/media_complain.html @@ -0,0 +1,38 @@ +{% comment %} +Props: +- media_type +- media_obj +- report_form +{% endcomment %} + +
+

Create report

+ +
+
+
+ {{ report_form.reason.label_tag }} + {{ report_form.reason }} +
+
{{ report_form.reason.help_text }}
+
+
+ +
+
+
+ {{ report_form.description.label_tag }} + {{ report_form.description }} +
+
{{ report_form.description.help_text }}
+
+
+ +
+ +
+ + +
diff --git a/api/api/templates/admin/api/components/media_decisions.html b/api/api/templates/admin/api/components/media_decisions.html index 6ce5b6a126b..a1d7fbbc65d 100644 --- a/api/api/templates/admin/api/components/media_decisions.html +++ b/api/api/templates/admin/api/components/media_decisions.html @@ -9,6 +9,7 @@

Decisions

+ {% if decision_throughs %} @@ -52,4 +53,10 @@

Decisions

.hidden { display: none; } .w-full { width: 100%; } + {% else %} +
+ There are no decisions. You can take a decision using the form + "Create decision" above. +
+ {% endif %} diff --git a/api/api/templates/admin/api/components/media_reports.html b/api/api/templates/admin/api/components/media_reports.html index eb595388d84..5a638382b97 100644 --- a/api/api/templates/admin/api/components/media_reports.html +++ b/api/api/templates/admin/api/components/media_reports.html @@ -6,15 +6,22 @@ - mod_form {% endcomment %} +{% load static %} +
-

Reports

+

Reports/Create decision

{% if pending_report_count %} -

+

You can take a decision for the pending reports by selecting one or more of them and creating a decision. -

+
+ {% endif %} + + {% if reports %}
@@ -51,9 +58,9 @@

Reports

{{ report.description }} {% if report.is_pending %} - False + False {% else %} - False + False {% endif %} @@ -73,6 +80,13 @@

Reports

.hidden { display: none; } .w-full { width: 100%; } + {% else %} +
+ There are no reports. You can file a report using the form "Create + report" above. +
+ {% endif %} + {% if pending_report_count %}
diff --git a/api/api/templates/admin/api/media/change_form.html b/api/api/templates/admin/api/media/change_form.html index 765eb8ecf34..3dea00a5303 100644 --- a/api/api/templates/admin/api/media/change_form.html +++ b/api/api/templates/admin/api/media/change_form.html @@ -41,9 +41,18 @@ {% endblock %} {% block content %}{{ block.super }} - + +
+ {% csrf_token %} + +
+ {% if pending_report_count %} -
+ + {% csrf_token %}
{% endif %} @@ -55,6 +64,7 @@ {% block after_field_sets %} {% include 'admin/api/components/media_additional.html' with media_type=media_type media_obj=media_obj tags=tags only %} +{% include 'admin/api/components/media_complain.html' with media_type=media_type media_obj=media_obj report_form=report_form only %} {% include 'admin/api/components/media_reports.html' with media_type=media_type reports=reports pending_report_count=pending_report_count mod_form=mod_form only %} {% include 'admin/api/components/media_decisions.html' with media_type=media_type decision_throughs=decision_throughs only %} {% endblock %}