Skip to content

Commit

Permalink
Merge branch 'WordPress:main' into 3847_add_variable_to_disable_remov…
Browse files Browse the repository at this point in the history
…ing_sql_source_files_for_ingestion_workflows
  • Loading branch information
madewithkode authored Apr 30, 2024
2 parents c0105eb + 1d875c8 commit 97a5855
Show file tree
Hide file tree
Showing 306 changed files with 1,317 additions and 832 deletions.
53 changes: 9 additions & 44 deletions api/api/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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__(),
Expand Down
163 changes: 163 additions & 0 deletions api/api/admin/media_report.py
Original file line number Diff line number Diff line change
@@ -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
15 changes: 13 additions & 2 deletions api/api/docs/audio_docs.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from rest_framework.exceptions import (
AuthenticationFailed,
NotAuthenticated,
NotFound,
ValidationError,
Expand Down Expand Up @@ -75,7 +76,10 @@
By using this endpoint, you can obtain info about content providers such
as {fields_to_md(ProviderSerializer.Meta.fields)}.""",
res={200: (ProviderSerializer(many=True), audio_stats_200_example)},
res={
200: (ProviderSerializer(many=True), audio_stats_200_example),
401: (AuthenticationFailed, None),
},
eg=[audio_stats_curl],
)

Expand All @@ -87,6 +91,7 @@
{fields_to_md(AudioSerializer.Meta.fields)}""",
res={
200: (AudioSerializer, audio_detail_200_example),
401: (AuthenticationFailed, None),
404: (NotFound, audio_detail_404_example),
},
eg=[audio_detail_curl],
Expand All @@ -100,6 +105,7 @@
{fields_to_md(AudioSerializer.Meta.fields)}.""",
res={
200: (AudioSerializer(many=True), audio_related_200_example),
401: (AuthenticationFailed, None),
404: (NotFound, audio_related_404_example),
},
eg=[audio_related_curl],
Expand All @@ -109,18 +115,23 @@
res={
201: (AudioReportRequestSerializer, audio_complain_201_example),
400: (ValidationError, None),
401: (AuthenticationFailed, None),
},
eg=[audio_complain_curl],
)

thumbnail = extend_schema(
parameters=[MediaThumbnailRequestSerializer],
responses={200: OpenApiResponse(description="Thumbnail image")},
responses={
200: OpenApiResponse(description="Thumbnail image"),
401: AuthenticationFailed,
},
)

waveform = custom_extend_schema(
res={
200: (AudioWaveformSerializer, audio_waveform_200_example),
401: (AuthenticationFailed, None),
404: (NotFound, audio_waveform_404_example),
},
eg=[audio_waveform_curl],
Expand Down
82 changes: 75 additions & 7 deletions api/api/docs/base_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@

from django.conf import settings
from rest_framework.exceptions import (
APIException,
NotFound,
ValidationError,
)

from drf_spectacular.extensions import OpenApiSerializerExtension
from drf_spectacular.openapi import AutoSchema
from drf_spectacular.utils import (
OpenApiExample,
Expand All @@ -31,6 +34,77 @@ def fields_to_md(field_names):
return f"{all_but_last} and `{last}`"


class APIExceptionOpenApiSerializerExtension(OpenApiSerializerExtension):
target_class = APIException
match_subclasses = True

@classmethod
def _get_detail(cls, target):
return getattr(target, "detail", target.default_detail)

def get_name(self, *args):
cls = self.target if isinstance(self.target, type) else self.target.__class__
return cls.__name__

def map_serializer(self, *args):
cls = self.target if isinstance(self.target, type) else self.target.__class__

detail_string = {
"type": "string",
"description": "A description of what went wrong.",
}

if cls == ValidationError or issubclass(cls, ValidationError):
return {
"title": "ValidationError",
"type": "object",
"properties": {
"detail": {
"oneOf": [
detail_string,
{
"type": "object",
"additionalProperties": True,
},
]
}
},
}

return {
"title": cls.__name__,
"type": "object",
"properties": {"detail": detail_string},
}

@classmethod
def exception_example(cls, exception):
if exception == ValidationError:
return {"detail": {"<request parameter>": "<error details>"}}

return {"detail": cls._get_detail(exception)}


def get_examples(code, serializer, example):
if (
not example
and isinstance(serializer, type)
and issubclass(serializer, APIException)
):
example = APIExceptionOpenApiSerializerExtension.exception_example(serializer)
elif example:
example = example["application/json"]
else:
return []

return [
OpenApiExample(
http_responses[code],
value=example,
)
]


def custom_extend_schema(**kwargs):
extend_args = {}

Expand All @@ -51,13 +125,7 @@ def custom_extend_schema(**kwargs):
code: OpenApiResponse(
serializer,
description=http_responses[code],
examples=[
OpenApiExample(
http_responses[code], value=example["application/json"]
)
]
if example
else [],
examples=get_examples(code, serializer, example),
)
for code, (serializer, example) in responses.items()
}
Expand Down
Loading

0 comments on commit 97a5855

Please sign in to comment.