Skip to content

Commit

Permalink
Add report creation, better filtering and more improvements to admin …
Browse files Browse the repository at this point in the history
…views for media (#4414)
  • Loading branch information
dhruvkb authored May 31, 2024
1 parent c4cb14e commit 7e558ef
Show file tree
Hide file tree
Showing 5 changed files with 203 additions and 67 deletions.
187 changes: 127 additions & 60 deletions api/api/admin/media_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -193,9 +238,14 @@ def wrapper(*args, **kwargs):
# appear just before the catch-all view.
urls[-1:-1] = [
path(
"<path:object_id>/moderate/",
wrap(self.moderate_view),
name=f"{app}_{model}_moderate",
"<path:object_id>/report_create/",
wrap(self.report_create_view),
name=f"{app}_{model}_report_create",
),
path(
"<path:object_id>/decision_create/",
wrap(self.decision_create_view),
name=f"{app}_{model}_decision_create",
),
path(
"<path:object_id>/lock/",
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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)

#############
Expand All @@ -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.
Expand Down Expand Up @@ -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

Expand Down
38 changes: 38 additions & 0 deletions api/api/templates/admin/api/components/media_complain.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{% comment %}
Props:
- media_type
- media_obj
- report_form
{% endcomment %}

<fieldset class="module aligned">
<h2>Create report</h2>

<div class="form-row field-height">
<div>
<div class="flex-container">
{{ report_form.reason.label_tag }}
{{ report_form.reason }}
</div>
<div class="help">{{ report_form.reason.help_text }}</div>
</div>
</div>

<div class="form-row field-height">
<div>
<div class="flex-container">
{{ report_form.description.label_tag }}
{{ report_form.description }}
</div>
<div class="help">{{ report_form.description.help_text }}</div>
</div>
</div>

<div class="p-10px">
<input type="submit" form="report-create" value="Create report">
</div>

<style>
.p-10px { padding: 10px; }
</style>
</fieldset>
7 changes: 7 additions & 0 deletions api/api/templates/admin/api/components/media_decisions.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<fieldset class="module">
<h2>Decisions</h2>

{% if decision_throughs %}
<table class="w-full">
<thead>
<tr>
Expand Down Expand Up @@ -52,4 +53,10 @@ <h2>Decisions</h2>
.hidden { display: none; }
.w-full { width: 100%; }
</style>
{% else %}
<div class="description">
There are no decisions. You can take a decision using the form
"Create decision" above.
</div>
{% endif %}
</fieldset>
24 changes: 19 additions & 5 deletions api/api/templates/admin/api/components/media_reports.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,22 @@
- mod_form
{% endcomment %}

{% load static %}

<fieldset class="module aligned">
<h2>Reports</h2>
<h2>Reports/Create decision</h2>

{% if pending_report_count %}
<p>
<div class="description mb-5px">
You can take a decision for the pending reports by selecting one
or more of them and creating a decision.
</p>
</div>
<style>
.mb-5px { margin-bottom: 5px; }
</style>
{% endif %}

{% if reports %}
<table class="w-full">
<thead>
<tr>
Expand Down Expand Up @@ -51,9 +58,9 @@ <h2>Reports</h2>
<td>{{ report.description }}</td>
<td>
{% if report.is_pending %}
<img src="/static/admin/img/icon-yes.svg" alt="False">
<img src="{% static 'admin/img/icon-yes.svg' %}" alt="False">
{% else %}
<img src="/static/admin/img/icon-no.svg" alt="False">
<img src="{% static 'admin/img/icon-no.svg' %}" alt="False">
{% endif %}
</td>
<td>
Expand All @@ -73,6 +80,13 @@ <h2>Reports</h2>
.hidden { display: none; }
.w-full { width: 100%; }
</style>
{% else %}
<div class="description">
There are no reports. You can file a report using the form "Create
report" above.
</div>
{% endif %}

{% if pending_report_count %}
<div class="form-row field-height">
<div>
Expand Down
14 changes: 12 additions & 2 deletions api/api/templates/admin/api/media/change_form.html
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,18 @@
{% endblock %}

{% block content %}{{ block.super }}
<!-- Fields for this form are supplied separately in `media_reports.html`. -->
<!-- Fields for this form are supplied separately in `media_complain.html`. -->
<form id="report-create" method="POST" action="{% url 'admin:api_'|add:media_type|add:'_report_create' object_id %}">
{% csrf_token %}
<input
type="hidden"
name="media_obj"
value="{{ media_obj.identifier }}">
</form>

{% if pending_report_count %}
<form id="decision-create" method="POST" action="{% url 'admin:api_'|add:media_type|add:'_moderate' object_id %}">
<!-- Fields for this form are supplied separately in `media_reports.html`. -->
<form id="decision-create" method="POST" action="{% url 'admin:api_'|add:media_type|add:'_decision_create' object_id %}">
{% csrf_token %}
</form>
{% endif %}
Expand All @@ -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 %}

0 comments on commit 7e558ef

Please sign in to comment.