From 1d875c867688e40e9555b23dd680c97b48b14544 Mon Sep 17 00:00:00 2001 From: Olga Bulat Date: Tue, 30 Apr 2024 08:48:19 +0300 Subject: [PATCH] Update media moderation view (#4169) * Update media moderation view Signed-off-by: Olga Bulat * Add has_sensitive_text Signed-off-by: Olga Bulat * Move media report admins to a separate file Signed-off-by: Olga Bulat * Move form html to the template Signed-off-by: Olga Bulat * Update api/api/admin/media_report.py Co-authored-by: Madison Swain-Bowden * Remove left-over templates from python file Signed-off-by: Olga Bulat * Fix thumb and report url; tags display Signed-off-by: Olga Bulat * Reorder code Signed-off-by: Olga Bulat * Add changes from code review Signed-off-by: Olga Bulat --------- Signed-off-by: Olga Bulat Co-authored-by: Madison Swain-Bowden --- api/api/admin/__init__.py | 53 +----- api/api/admin/media_report.py | 163 ++++++++++++++++++ api/api/models/media.py | 6 +- .../admin/api/media_report/attribution.html | 25 +++ .../admin/api/media_report/change_form.html | 150 ++++++++++++++++ 5 files changed, 352 insertions(+), 45 deletions(-) create mode 100644 api/api/admin/media_report.py create mode 100644 api/api/templates/admin/api/media_report/attribution.html create mode 100644 api/api/templates/admin/api/media_report/change_form.html diff --git a/api/api/admin/__init__.py b/api/api/admin/__init__.py index 3390383aef6..0e83cce92e5 100644 --- a/api/api/admin/__init__.py +++ b/api/api/admin/__init__.py @@ -7,9 +7,14 @@ from oauth2_provider.models import AccessToken from api.admin.forms import UserPreferencesAdminForm +from api.admin.media_report import ( + AudioReportAdmin, + ImageReportAdmin, + MediaReportAdmin, + MediaSubreportAdmin, +) from api.admin.site import openverse_admin from api.models import ( - PENDING, Audio, AudioReport, ContentProvider, @@ -40,50 +45,10 @@ class AudioAdmin(admin.ModelAdmin): search_fields = ("identifier",) -class MediaReportAdmin(admin.ModelAdmin): - list_display = ("id", "reason", "is_pending", "description", "created_at", "url") - list_filter = ( - ("decision", admin.EmptyFieldListFilter), # ~status, i.e. pending or moderated - "reason", - ) - list_display_links = ("id",) - search_fields = ("description", "media_obj__identifier") - autocomplete_fields = ("media_obj",) - actions = None - - def get_exclude(self, request, obj=None): - # ``identifier`` cannot be edited on an existing report. - if request.path.endswith("/change/"): - return ["media_obj"] - - def get_readonly_fields(self, request, obj=None): - if obj is None: - return [] - readonly_fields = [ - "reason", - "description", - "media_obj_id", - "created_at", - ] - # ``status`` cannot be changed on a finalised report. - if obj.status != PENDING: - readonly_fields.append("status") - return readonly_fields - - -admin.site.register(AudioReport, MediaReportAdmin) -admin.site.register(ImageReport, MediaReportAdmin) - - -class MediaSubreportAdmin(admin.ModelAdmin): - exclude = ("media_obj",) - search_fields = ("media_obj__identifier",) - readonly_fields = ("media_obj_id",) - - def has_add_permission(self, *args, **kwargs): - """Create ``_Report`` instances instead.""" - return False +# Register the MediaReportAdmin classes and its subclasses +admin.site.register(AudioReport, AudioReportAdmin) +admin.site.register(ImageReport, ImageReportAdmin) for klass in [ *AbstractSensitiveMedia.__subclasses__(), diff --git a/api/api/admin/media_report.py b/api/api/admin/media_report.py new file mode 100644 index 00000000000..a596ee494a0 --- /dev/null +++ b/api/api/admin/media_report.py @@ -0,0 +1,163 @@ +import logging + +from django.conf import settings +from django.contrib import admin + +from elasticsearch import NotFoundError +from elasticsearch_dsl import Search +from openverse_attribution.license import License + +from api.models import PENDING + + +class MediaReportAdmin(admin.ModelAdmin): + change_form_template = "admin/api/media_report/change_form.html" + list_display = ("id", "reason", "is_pending", "description", "created_at", "url") + list_filter = ( + ("decision", admin.EmptyFieldListFilter), # ~status, i.e. pending or moderated + "reason", + ) + list_display_links = ("id",) + list_select_related = ("media_obj",) + search_fields = ("description", "media_obj__identifier") + autocomplete_fields = ("media_obj",) + actions = None + media_type = None + + def get_fieldsets(self, request, obj=None): + if obj is None: + return [ + ( + "Report details", + {"fields": ["status", "decision", "reason", "description"]}, + ), + ("Media details", {"fields": ["media_obj"]}), + ] + return [ + ( + "Report details", + { + "fields": [ + "created_at", + "status", + "decision", + "reason", + "description", + "has_sensitive_text", + ], + }, + ), + ] + + def get_exclude(self, request, obj=None): + # ``identifier`` cannot be edited on an existing report. + if request.path.endswith("/change/"): + return ["media_obj"] + + def get_readonly_fields(self, request, obj=None): + if obj is None: + return [] + readonly_fields = [ + "created_at", + "reason", + "description", + "has_sensitive_text", + "media_obj_id", + ] + # ``status`` cannot be changed on a finalised report. + if obj.status != PENDING: + readonly_fields.append("status") + return readonly_fields + + @admin.display(description="Has sensitive text") + def has_sensitive_text(self, obj): + """ + Return `True` if the item cannot be found in the filtered index - which means the item + was filtered out due to text sensitivity. + """ + if not self.media_type or not obj: + return None + + filtered_index = f"{settings.MEDIA_INDEX_MAPPING[self.media_type]}-filtered" + try: + search = ( + Search(index=filtered_index) + .query("term", identifier=obj.media_obj.identifier) + .execute() + ) + if search.hits: + return False + except NotFoundError: + logging.error(f"Could not resolve index {filtered_index}") + return None + return True + + def get_other_reports(self, obj): + if not self.media_type or not obj: + return [] + + reports = ( + self.model.objects.filter(media_obj__identifier=obj.media_obj.identifier) + .exclude(id=obj.id) + .order_by("created_at") + ) + return reports + + def _get_media_obj_data(self, obj): + tags_by_provider = {} + if obj.media_obj.tags: + for tag in obj.media_obj.tags: + tags_by_provider.setdefault(tag["provider"], []).append(tag["name"]) + additional_data = { + "other_reports": self.get_other_reports(obj), + "media_obj": obj.media_obj, + "license": License(obj.media_obj.license).name( + obj.media_obj.license_version + ), + "tags": tags_by_provider, + "description": obj.media_obj.meta_data.get("description", ""), + } + logging.info(f"Additional data: {additional_data}") + return additional_data + + def change_view(self, request, object_id, form_url="", extra_context=None): + extra_context = extra_context or {} + extra_context["media_type"] = self.media_type + + obj = self.get_object(request, object_id) + if obj and obj.media_obj: + additional_data = self._get_media_obj_data(obj) + extra_context = {**extra_context, **additional_data} + + return super().change_view( + request, + object_id, + form_url, + extra_context=extra_context, + ) + + def render_change_form( + self, request, context, add=False, change=False, form_url="", obj=None + ): + context.update({"add": add, "change": change}) + return super().render_change_form( + request, context, add=add, change=change, form_url=form_url, obj=obj + ) + + +class ImageReportAdmin(MediaReportAdmin): + media_type = "image" + + +class AudioReportAdmin(MediaReportAdmin): + media_type = "audio" + + +class MediaSubreportAdmin(admin.ModelAdmin): + exclude = ("media_obj",) + search_fields = ("media_obj__identifier",) + readonly_fields = ("media_obj_id",) + + def has_add_permission(self, *args, **kwargs): + """Create ``_Report`` instances instead.""" + return False diff --git a/api/api/models/media.py b/api/api/models/media.py index 42b33810369..8396957dd90 100644 --- a/api/api/models/media.py +++ b/api/api/models/media.py @@ -211,7 +211,7 @@ def clean(self): f"with identifier '{self.media_obj_id}'." ) - def url(self, request=None) -> str: + def media_url(self, request=None) -> str: """ Build the URL of the media item. This uses ``reverse`` and ``request.build_absolute_uri`` to build the URL without having to worry @@ -227,6 +227,10 @@ def url(self, request=None) -> str: ) if request is not None: url = request.build_absolute_uri(url) + return url + + def url(self, request=None) -> str: + url = self.media_url(request) return format_html(f"{url}") @property diff --git a/api/api/templates/admin/api/media_report/attribution.html b/api/api/templates/admin/api/media_report/attribution.html new file mode 100644 index 00000000000..c751bf5e6fb --- /dev/null +++ b/api/api/templates/admin/api/media_report/attribution.html @@ -0,0 +1,25 @@ +

+{% if media_obj.title %} + "{% if media_obj.url %}{% endif %}{{ media_obj.title }}{% if media_obj.url %}{% endif %}" +{% else %} + {% if media_obj.url %}{% endif %} + This work + {% if media_obj.url %}{% endif %} +{% endif %} + by + {% if media_obj.creator %} + {% if media_obj.creator_url %} + {{ media_obj.creator }} + {% else %} + {{ media_obj.creator }} + {% endif %} + {% endif %} +{% if media_obj.license %} + {% if media_obj.license.is_pd %} + is marked with + {% else %} + is licensed under + {% endif %} + {{ license }} +{% endif %} +

diff --git a/api/api/templates/admin/api/media_report/change_form.html b/api/api/templates/admin/api/media_report/change_form.html new file mode 100644 index 00000000000..7040928d499 --- /dev/null +++ b/api/api/templates/admin/api/media_report/change_form.html @@ -0,0 +1,150 @@ +{% extends "admin/change_form.html" %} +{% load static %} + +{% block extrahead %} +{{ block.super }} + +{% endblock %} + +{% block after_field_sets %} +{{ block.super }} + +{% if not add %} +
+

Media details

+ {% if media_type == 'image' %} +
+ Media Image +

Show content

+
+ + {% elif media_type == 'audio' %} + + {% endif %} + +
+
+ +
+ {% include "admin/api/media_report/attribution.html" with media_obj=media_obj license=license %} +

View media at source ({{media_obj.source}}), + on openverse.org, + on Openverse API.

+

Provider: {{ media_obj.provider }}.

+
+
+
+ +
+
+ +
+ {% for provider, provider_tags in tags.items %} +

{{ provider }}

+

{{ provider_tags|join:', ' }}

+ {% endfor %} +
+
+
+
+
+ +
+

{{ description }}

+
+
+
+ +
+

Other reports

+
+ {% if other_reports %} + + + + + + + + + + + {% for report in other_reports %} + + + + + + + {% endfor %} + +
DateReport reasonStatusReport link
{{ report.created_at }}{{ report.reason }}{{ report.status }}{{ report.id }}
+ {% else %} +

No other reports for {{ media_obj.identifier }} found.

+ {% endif %} +
+ +
+{% endif %} +{% endblock %} + + +{% block admin_change_form_document_ready %} +{{ block.super }} + +{% endblock %}