Skip to content

Commit

Permalink
Make media items the centre for all moderation activity (#4386)
Browse files Browse the repository at this point in the history
Co-authored-by: sarayourfriend <[email protected]>
Co-authored-by: Madison Swain-Bowden <[email protected]>
  • Loading branch information
3 people authored May 30, 2024
1 parent cf2d194 commit 1f24ba1
Show file tree
Hide file tree
Showing 16 changed files with 1,061 additions and 135 deletions.
557 changes: 438 additions & 119 deletions api/api/admin/media_report.py

Large diffs are not rendered by default.

12 changes: 11 additions & 1 deletion api/api/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,23 @@
from api.models.audio import (
AltAudioFile,
Audio,
AudioDecision,
AudioDecisionThrough,
AudioList,
AudioReport,
AudioSet,
DeletedAudio,
SensitiveAudio,
)
from api.models.image import DeletedImage, Image, ImageList, ImageReport, SensitiveImage
from api.models.image import (
DeletedImage,
Image,
ImageDecision,
ImageDecisionThrough,
ImageList,
ImageReport,
SensitiveImage,
)
from api.models.media import (
DEINDEXED,
DMCA,
Expand Down
7 changes: 7 additions & 0 deletions api/api/models/audio.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,13 @@ def get_or_create_waveform(self):
class Meta(AbstractMedia.Meta):
db_table = "audio"

def get_absolute_url(self):
"""Enable the "View on site" link in the Django Admin."""

from django.urls import reverse

return reverse("audio-detail", args=[str(self.identifier)])


class DeletedAudio(AbstractDeletedMedia):
"""
Expand Down
7 changes: 7 additions & 0 deletions api/api/models/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ class Image(ImageFileMixin, AbstractMedia):
class Meta(AbstractMedia.Meta):
db_table = "image"

def get_absolute_url(self):
"""Enable the "View on site" link in the Django Admin."""

from django.urls import reverse

return reverse("image-detail", args=[str(self.identifier)])

@property
def sensitive(self) -> bool:
return hasattr(self, "sensitive_image")
Expand Down
18 changes: 17 additions & 1 deletion api/api/models/moderation.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from django.conf import settings
from django.contrib.auth import get_user_model
from django.db import models
from django.db.models import Q
from django.db.models.signals import post_save
from django.dispatch import receiver

Expand All @@ -9,7 +11,7 @@ class UserPreferences(models.Model):
preferences = models.JSONField(default=dict)

def __str__(self):
return f"{self.user.username}'s preferences"
return f"{self.user.get_username()}'s preferences"

@property
def moderator(self):
Expand All @@ -36,3 +38,17 @@ def create_or_update_user_profile(sender, instance, created, **kwargs):
if created:
UserPreferences.objects.create(user=instance)
instance.userpreferences.save()


def get_moderators() -> models.QuerySet:
"""
Get all users who either are members of the "Content Moderators"
group or have superuser privileges.
:return: a ``QuerySet`` of ``User``s who can perform moderation
"""

User = get_user_model()
return User.objects.filter(
Q(groups__name="Content Moderators") | Q(is_superuser=True)
)
69 changes: 69 additions & 0 deletions api/api/templates/admin/api/components/media_additional.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
{% comment %}
Props:
- media_type
- media_obj
- tags
{% endcomment %}

<fieldset class="module aligned">
<h2>Additional information</h2>

<div class="form-row field-height">
<div>
<div class="flex-container">
<label>Preview:</label>
{% if media_type == 'image' %}
<div class="overflow-hidden">
<img
src="{% url media_type|add:'-thumb' identifier=media_obj.identifier %}"
alt="Media Image"
class="transition-filter blur-60px"
height="300"
onclick="toggleBlur(this)"
onerror="this.onerror=null;this.src='{{ media_obj.url }}';">
</div>
<style>
.overflow-hidden { overflow: hidden; }
.transition-filter { transition: filter 0.3s; }
.blur-60px { filter: blur(60px); }
</style>
<script>
function toggleBlur(img) {
img.classList.toggle('blur-60px');
}
</script>

{% elif media_type == 'audio' %}
<audio controls>
<source src="{{ media_obj.url }}">
Your browser does not support the audio element.
</audio>
{% endif %}
</div>
<div class="help">
{% if media_type == 'image' %}
<div>Click to show/hide content.</div>
{% endif %}
</div>
</div>
</div>

<div class="form-row field-height">
<div>
<div class="flex-container">
<label>Tags:</label>
<div>
<dl class="pl-0">
{% for provider, provider_tags in tags.items %}
<dt>{{ provider }}:</dt>
<dd>{{ provider_tags|join:', ' }}</dd>
{% endfor %}
</dl>
<style>
dl.pl-0 { padding-left: 0; }
</style>
</div>
</div>
</div>
</div>
</fieldset>
55 changes: 55 additions & 0 deletions api/api/templates/admin/api/components/media_decisions.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{% comment %}
Props:
- media_type
- decision_throughs
{% endcomment %}

{% load get_attr %}

<fieldset class="module">
<h2>Decisions</h2>

<table class="w-full">
<thead>
<tr>
<th class="hidden"></th>
<th>ID</th>
<th>Date</th>
<th>Moderator</th>
<th>Action</th>
<th>Notes</th>
<th>Reports</th>
</tr>
</thead>
<tbody>
{% for decision_through in decision_throughs %}
{% with decision_through.decision as decision %}
<tr>
<td class="hidden"><!-- Hidden inputs etc. --></td>
<td>
<a href="{% url 'admin:api_'|add:media_type|add:'decision_change' decision.id %}">{{ decision.id }}</a>
</td>
<td>{{ decision.created_on }}</td>
<td>{{ decision.moderator }}</td>
<td>{{ decision.action }}</td>
<td>{{ decision.notes }}</td>
<td>
{% with media_type|add:'report_set' as attr_name %}
{% with decision|get_attr:attr_name as reports %}
{% for report in reports.all %}
<a href="{% url 'admin:api_'|add:media_type|add:'report_change' report.id %}">{{ report.id }}</a> ({{report.reason}})<br/>
{% endfor %}
{% endwith %}
{% endwith %}
</td>
</tr>
{% endwith %}
{% endfor %}
</tbody>
</table>

<style>
.hidden { display: none; }
.w-full { width: 100%; }
</style>
</fieldset>
105 changes: 105 additions & 0 deletions api/api/templates/admin/api/components/media_reports.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
{% comment %}
Props:
- media_type
- reports
- pending_report_count
- mod_form
{% endcomment %}

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

{% if pending_report_count %}
<p>
You can take a decision for the pending reports by selecting one
or more of them and creating a decision.
</p>
{% endif %}
<table class="w-full">
<thead>
<tr>
<th class="hidden"></th>
{% if pending_report_count %}<th>Select</th>{% endif %}
<th>ID</th>
<th>Date</th>
<th>Reason</th>
<th>Description</th>
<th>Is pending?</th>
<th>Decision</th>
</tr>
</thead>
<tbody>
{% for report in reports %}
<tr>
<td class="hidden"><!-- Hidden inputs etc. --></td>
{% if pending_report_count %}
<td>
{% if report.is_pending %}
<input
form="decision-create"
type="checkbox"
name="report_id"
value="{{ report.id }}">
{% endif %}
</td>
{% endif %}
<td>
<a href="{% url 'admin:api_'|add:media_type|add:'report_change' report.id %}">{{ report.id }}</a>
</td>
<td>{{ report.created_at }}</td>
<td>{{ report.reason }}</td>
<td>{{ report.description }}</td>
<td>
{% if report.is_pending %}
<img src="/static/admin/img/icon-yes.svg" alt="False">
{% else %}
<img src="/static/admin/img/icon-no.svg" alt="False">
{% endif %}
</td>
<td>
{% if report.decision %}
<a href="{% url 'admin:api_'|add:media_type|add:'decision_change' report.decision.id %}">{{ report.decision.id }}</a>
({{ report.decision.action }})
{% else %}
-
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>

<style>
.hidden { display: none; }
.w-full { width: 100%; }
</style>
{% if pending_report_count %}
<div class="form-row field-height">
<div>
<div class="flex-container">
{{ mod_form.action.label_tag }}
{{ mod_form.action }}
</div>
<div class="help">{{ mod_form.action.help_text }}</div>
</div>
</div>

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

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

<style>
.p-10px { padding: 10px; }
</style>
{% endif %}
</fieldset>
60 changes: 60 additions & 0 deletions api/api/templates/admin/api/media/change_form.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
{% extends "admin/change_form.html" %}

{% block extrahead %}{{ block.super }}
<!--
This script make links clickable. Since we've stored URLs in
`TextField`s, they are not rendered as links in the Django Admin UI.
This script identifies `<div>`s containing links and converts them to
`<a>` tags.
-->
<script>
document.addEventListener("DOMContentLoaded", function() {
document.querySelectorAll("div.readonly").forEach(div => {
if (div.textContent.match(/https?:\/\/\S+/)) {
div.innerHTML = div.textContent.replace(/(https?:\/\/\S+)/g, '<a href="$1">$1</a>');
}
});
});
</script>

<!--
This script polls the soft-lock endpoint at intervals shorter than the
lock TTL to keep the lock alive.
-->
<script>
function softLock() {
fetch('{% url "admin:api_"|add:media_type|add:"_lock" object_id=object_id %}', {
method: "POST",
keepalive: true, // This makes the request equivalent to a beacon.
headers: {
"Content-Type": "application/json",
"X-CSRFToken": document.querySelector('[name="csrfmiddlewaretoken"]').value
},
})
}

document.addEventListener("DOMContentLoaded", function() {
softLock()
setInterval(softLock, 5000)
})
</script>
{% endblock %}

{% block content %}{{ block.super }}
<!-- Fields for this form are supplied separately in `media_reports.html`. -->
{% if pending_report_count %}
<form id="decision-create" method="POST" action="{% url 'admin:api_'|add:media_type|add:'_moderate' object_id %}">
{% csrf_token %}
</form>
{% endif %}
{% endblock %}

{% block object-tools-items %}{{ block.super }}
<li><a href="https://openverse.org/{{media_type}}/{{media_obj.identifier}}" class="viewsitelink">View on openverse.org</a></li>
{% endblock %}

{% 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_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 %}
Loading

0 comments on commit 1f24ba1

Please sign in to comment.