Merge pull request #6354 from akatsoulas/extract-moderation-tool

Extract moderation tool to a separate url
This commit is contained in:
Tasos Katsoulas 2024-11-15 10:24:48 +02:00 коммит произвёл GitHub
Родитель 345ec8150b 1c98787226
Коммит 8a157f72de
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
10 изменённых файлов: 231 добавлений и 136 удалений

Просмотреть файл

@ -0,0 +1,36 @@
{% extends "flagit/flagit_base.html" %}
{% block flagged_items %}
{% for object in objects %}
<li class="{{ object.content_type }}">
<div class="flagged-item-content">
<hgroup>
<h2 class="sumo-card-heading">{{ _('Flagged {t} (Reason: {r})')|f(t=object.content_type, r=object.get_reason_display()) }}</h2>
{% if object.notes %}
<p class="notes">{{ _('Other reason:') }} {{ object.notes }}</p>
{% endif %}
</hgroup>
<div class="wrap">
{% if object.content_object %}
{% include 'flagit/includes/flagged_%s.html' % object.content_type.model %}
{% else %}
<p>{{ _('{t} with id={id} no longer exists.')|f(t=object.content_type, id=object.object_id) }}</p>
{% endif %}
<h3 class="sumo-card-heading"><br>{{ _('Update Status:') }}</h3>
<form class="update inline-form" action="{{ object.form_action }}" method="post">
{% csrf_token %}
<select name="status">
<option value="">{{ _('Please select...') }}</option>
<option value="1">{{ _('Content categorization is updated.') }}</option>
<option value="2">{{ _('Content is appropriately categorized.') }}</option>
</select>
<input id="update-status-button-{{ object.content_object.id }}" type="submit"
class="sumo-button primary-button button-lg btn" value={{ _('Update') }} />
</form>
</div>
</div>
</li>
{% else %}
<p>{{ _('There is no content pending moderation.') }}</p>
{% endfor %}
{% endblock %}

Просмотреть файл

@ -0,0 +1,32 @@
{% extends "questions/base.html" %} {# TODO: liberate - remove questions dependency #}
{% from "includes/common_macros.html" import for_contributors_sidebar %}
{% set title = _('Flagged Content Pending Moderation') %}
{% set classes = 'flagged' %}
{% set scripts = ('flagit', ) %}
{% block content %}
<div id="flagged-queue" class="sumo-page-section">
<h1 class="sumo-page-heading">{{ _('Content Pending Moderation') }}</h1>
<ul class="flagged-items">
{% block flagged_items %}
{% endblock %}
</ul>
{% block deactivation_log %}
<div class="sumo-button-wrap"></div>
<a class="sumo-button primary-button" rel="nofollow" href="{{ url('users.deactivation_log') }}">{{ _('View all deactivated users') }}</a>
</div>
{% endblock %}
</div>
{% endblock %}
{% block side_top %}
<nav id="doc-tools">
<ul class="sidebar-nav sidebar-folding">
{{ for_contributors_sidebar(user, settings.WIKI_DEFAULT_LANGUAGE, active="flagit.flagged_queue", menu="contributor-tools", locale=locale) }}
</ul>
{% block side_top_reason %}
{% endblock %}
</nav>
{% endblock %}

Просмотреть файл

@ -21,7 +21,7 @@
<h3 class="flagged-content__subheading">{{ _('Flagged:') }}</h3>
<p>{{ date_by_user(object.created, object.creator) }}</p>
{% if object.reason == 'bug_support' and question_model and user.has_perm('questions.change_question') %}
{% if object.reason == 'content_moderation' and question_model and user.has_perm('questions.change_question') %}
<h3 class="flagged-content__subheading">{{ _('Take Action:') }}</h3>
<div class="flagged-content__topic-update">
<label> {{ _('Current topic:') }} </label>

Просмотреть файл

@ -1,84 +1,65 @@
{% extends "questions/base.html" %} {# TODO: liberate - remove questions dependency #}
{% from "includes/common_macros.html" import for_contributors_sidebar %}
{% set title = _('Flagged Content Pending Moderation') %}
{% set classes = 'flagged' %}
{% set scripts = ('flagit', ) %}
{% extends "flagit/flagit_base.html" %}
{% block content %}
<div id="flagged-queue" class="sumo-page-section">
<h1 class="sumo-page-heading">{{ _('Content Pending Moderation') }}</h1>
<ul class="flagged-items">
{% for object in objects %}
<li class="{{ object.content_type }}">
<div class="flagged-item-content">
<hgroup>
<h2 class="sumo-card-heading">{{ _('Flagged {t} (Reason: {r})')|f(t=object.content_type, r=object.get_reason_display()) }}</h2>
{% if object.notes %}
<p class="notes">{{ _('Other reason:') }} {{ object.notes }}</p>
{% endif %}
</hgroup>
<div class="wrap">
{% if object.content_object %}
{% include 'flagit/includes/flagged_%s.html' % object.content_type.model %}
{% else %}
<p>{{ _('{t} with id={id} no longer exists.')|f(t=object.content_type, id=object.object_id) }}</p>
{% endif %}
<h3 class="sumo-card-heading"><br>{{ _('Update Status:') }}</h3>
<form class="update inline-form" action="{{ object.form_action }}" method="post">
{% csrf_token %}
<select name="status">
<option value="">{{ _('Please select...') }}</option>
{% if object.reason == "spam" %}
<option value="1">{{ _('Removed spam content.') }}</option>
<option value="2">{{ _('No spam found.') }}</option>
{% elif object.reason == "abuse" %}
<option value="1">{{ _('Addressed abusive content.') }}</option>
<option value="2">{{ _('No abuse detected.') }}</option>
{% elif object.reason == "bug_support" %}
<option value="1">{{ _('Redirected support request.') }}</option>
<option value="2">{{ _('Content is appropriately placed.') }}</option>
{% elif object.reason == "language" %}
<option value="1">{{ _('Corrected language.') }}</option>
<option value="2">{{ _('Language is appropriate.') }}</option>
{% else %}
<option value="1">{{ _('Issue resolved.') }}</option>
<option value="2">{{ _('No issues found.') }}</option>
{% endif %}
</select>
<input id="update-status-button-{{ object.content_object.id }}" type="submit"
class="sumo-button primary-button button-lg btn" value={{ _('Update') }} />
</form>
</div>
{% block flagged_items %}
{% for object in objects %}
<li class="{{ object.content_type }}">
<div class="flagged-item-content">
<hgroup>
<h2 class="sumo-card-heading">{{ _('Flagged {t} (Reason: {r})')|f(t=object.content_type, r=object.get_reason_display()) }}</h2>
{% if object.notes %}
<p class="notes">{{ _('Other reason:') }} {{ object.notes }}</p>
{% endif %}
</hgroup>
<div class="wrap">
{% if object.content_object %}
{% include 'flagit/includes/flagged_%s.html' % object.content_type.model %}
{% else %}
<p>{{ _('{t} with id={id} no longer exists.')|f(t=object.content_type, id=object.object_id) }}</p>
{% endif %}
<h3 class="sumo-card-heading"><br>{{ _('Update Status:') }}</h3>
<form class="update inline-form" action="{{ object.form_action }}" method="post">
{% csrf_token %}
<select name="status">
<option value="">{{ _('Please select...') }}</option>
{% if object.reason == "spam" %}
<option value="1">{{ _('Removed spam content.') }}</option>
<option value="2">{{ _('No spam found.') }}</option>
{% elif object.reason == "abuse" %}
<option value="1">{{ _('Addressed abusive content.') }}</option>
<option value="2">{{ _('No abuse detected.') }}</option>
{% elif object.reason == "bug_support" %}
<option value="1">{{ _('Request moved.') }}</option>
<option value="2">{{ _('Request is appropriately placed.') }}</option>
{% elif object.reason == "language" %}
<option value="1">{{ _('Corrected language.') }}</option>
<option value="2">{{ _('Language is appropriate.') }}</option>
{% else %}
<option value="1">{{ _('Issue resolved.') }}</option>
<option value="2">{{ _('No issues found.') }}</option>
{% endif %}
</select>
<input id="update-status-button-{{ object.content_object.id }}" type="submit"
class="sumo-button primary-button button-lg btn" value={{ _('Update') }} />
</form>
</div>
</li>
{% else %}
<p>{{ _('There is no content pending moderation.') }}</p>
{% endfor %}
</ul>
</div>
</li>
{% else %}
<p>{{ _('There is no content pending moderation.') }}</p>
{% endfor %}
{% endblock %}
<div class="sumo-button-wrap">
<a class="sumo-button primary-button" rel="nofollow" href="{{ url('users.deactivation_log') }}">{{ _('View all deactivated users') }}</a>
</div>
{% block side_top_reason %}
<!-- Dropdown filter for reasons -->
<div class="filter-reasons">
<form id="reason-filter-form" method="get" action="">
<label for="reason">{{ _('Filter by reason:') }}</label>
<select name="reason" id="flagit-reason-filter">
<option value="">{{ _('All reasons') }}</option>
{% for value, display in reasons %}
<option value="{{ value }}" {% if selected_reason == value %}selected{% endif %}>{{ display }}</option>
{% endfor %}
</select>
</form>
</div>
{% endblock %}
{% block side_top %}
<nav id="doc-tools">
<ul class="sidebar-nav sidebar-folding">
{{ for_contributors_sidebar(user, settings.WIKI_DEFAULT_LANGUAGE, active="flagit.flagged_queue", menu="contributor-tools", locale=locale) }}
</ul>
<!-- Dropdown filter for reasons -->
<div class="filter-reasons">
<form id="reason-filter-form" method="get" action="">
<label for="reason">{{ _('Filter by reason:') }}</label>
<select name="reason" id="flagit-reason-filter">
<option value="">{{ _('All reasons') }}</option>
{% for value, display in reasons %}
<option value="{{ value }}" {% if selected_reason == value %}selected{% endif %}>{{ display }}</option>
{% endfor %}
</select>
</form>
</div>
</nav>
{% endblock %}

Просмотреть файл

@ -18,12 +18,18 @@ class FlaggedObjectManager(models.Manager):
class FlaggedObject(ModelBase):
"""A flag raised on an object."""
REASON_SPAM = "spam"
REASON_LANGUAGE = "language"
REASON_BUG_SUPPORT = "bug_support"
REASON_ABUSE = "abuse"
REASON_CONTENT_MODERATION = "content_moderation"
REASON_OTHER = "other"
REASONS = (
("spam", _lazy("Spam or other unrelated content")),
("language", _lazy("Inappropriate language/dialog")),
("bug_support", _lazy("Misplaced bug report or support request")),
("abuse", _lazy("Abusive content")),
("other", _lazy("Other (please specify)")),
(REASON_SPAM, _lazy("Spam or other unrelated content")),
(REASON_LANGUAGE, _lazy("Inappropriate language/dialog")),
(REASON_BUG_SUPPORT, _lazy("Misplaced bug report or support request")),
(REASON_ABUSE, _lazy("Abusive content")),
(REASON_OTHER, _lazy("Other (please specify)")),
)
FLAG_PENDING = 0

Просмотреть файл

@ -4,6 +4,7 @@ from kitsune.flagit import views
urlpatterns = [
re_path(r"^flagged$", views.flagged_queue, name="flagit.flagged_queue"),
re_path(r"^moderate$", views.moderate_content, name="flagit.moderate_content"),
re_path(r"^flag$", views.flag, name="flagit.flag"),
re_path(r"^update/(?P<flagged_object_id>\d+)$", views.update, name="flagit.update"),
]

Просмотреть файл

@ -16,44 +16,59 @@ from kitsune.sumo.templatetags.jinja_helpers import urlparams
from kitsune.sumo.urlresolvers import reverse
def get_flagged_objects(reason=None, exclude_reason=None, content_model=None):
"""Retrieve pending flagged objects with optional filtering, eager loading related fields."""
queryset = FlaggedObject.objects.pending().select_related("content_type", "creator")
if exclude_reason:
queryset = queryset.exclude(reason=exclude_reason)
if reason:
queryset = queryset.filter(reason=reason)
if content_model:
queryset = queryset.filter(content_type=content_model)
return queryset
def set_form_action_for_objects(objects, reason=None):
"""Generate form action URLs for flagged objects."""
for obj in objects:
base_url = reverse("flagit.update", args=[obj.id])
obj.form_action = urlparams(base_url, reason=reason)
return objects
@require_POST
@login_required
def flag(request, content_type=None, model=None, object_id=None, **kwargs):
if not content_type:
if model:
content_type = ContentType.objects.get_for_model(model).id
else:
content_type = request.POST.get("content_type")
if model:
content_type = ContentType.objects.get_for_model(model).id
content_type = content_type or request.POST.get("content_type")
object_id = int(object_id or request.POST.get("object_id"))
if not object_id:
object_id = int(request.POST.get("object_id"))
content_type = get_object_or_404(ContentType, id=int(content_type))
content_object = get_object_or_404(content_type.model_class(), pk=object_id)
reason = request.POST.get("reason")
notes = request.POST.get("other", "")
next = request.POST.get("next")
content_type = get_object_or_404(ContentType, id=int(content_type))
object_id = int(object_id)
content_object = get_object_or_404(content_type.model_class(), pk=object_id)
FlaggedObject.objects.filter(
content_type=content_type,
object_id=object_id,
reason="bug_support",
reason=FlaggedObject.REASON_CONTENT_MODERATION,
status=FlaggedObject.FLAG_PENDING,
).delete()
# Check that this user hasn't already flagged the object
try:
FlaggedObject.objects.get(
content_type=content_type, object_id=object_id, creator=request.user
)
msg = _("You already flagged this content.")
except FlaggedObject.DoesNotExist:
flag = FlaggedObject(
content_object=content_object, reason=reason, creator=request.user, notes=notes
)
flag.save()
msg = _("You have flagged this content. A moderator will review your submission shortly.")
_flagged, created = FlaggedObject.objects.get_or_create(
content_type=content_type,
object_id=object_id,
creator=request.user,
defaults={"content_object": content_object, "reason": reason, "notes": notes},
)
msg = (
_("You already flagged this content.")
if not created
else _("You have flagged this content. A moderator will review your submission shortly.")
)
if request.headers.get("x-requested-with") == "XMLHttpRequest":
return HttpResponse(json.dumps({"message": msg}))
@ -67,23 +82,15 @@ def flag(request, content_type=None, model=None, object_id=None, **kwargs):
@login_required
@permission_required("flagit.can_moderate")
def flagged_queue(request):
"""The flagged queue."""
"""Display the flagged queue with optimized queries."""
reason = request.GET.get("reason")
objects = FlaggedObject.objects.pending()
question_content_type = ContentType.objects.get_for_model(Question)
available_topics = []
if reason:
objects = objects.filter(reason=reason)
for object in objects:
if object.content_type == question_content_type:
question = object.content_object
available_topics = Topic.active.filter(products=question.product)
base_url = reverse("flagit.update", args=[object.id])
form_action = urlparams(base_url, query_dict=None, reason=reason)
object.available_topics = available_topics
object.form_action = form_action
objects = (
get_flagged_objects(reason=reason, exclude_reason=FlaggedObject.REASON_CONTENT_MODERATION)
.select_related("content_type", "creator")
.prefetch_related("content_object")
)
objects = set_form_action_for_objects(objects, reason=reason)
return render(
request,
@ -97,6 +104,35 @@ def flagged_queue(request):
)
@login_required
@permission_required("flagit.can_moderate")
def moderate_content(request):
"""Display flagged content that needs moderation."""
content_type = ContentType.objects.get_for_model(Question)
objects = (
get_flagged_objects(
reason=FlaggedObject.REASON_CONTENT_MODERATION, content_model=content_type
)
.select_related("content_type", "creator")
.prefetch_related("content_object__product")
)
objects = set_form_action_for_objects(objects, reason=FlaggedObject.REASON_CONTENT_MODERATION)
for obj in objects:
question = obj.content_object
obj.available_topics = Topic.active.filter(products=question.product, is_archived=False)
return render(
request,
"flagit/content_moderation.html",
{
"objects": objects,
"locale": request.LANGUAGE_CODE,
},
)
@require_POST
@login_required
@permission_required("flagit.can_moderate")
@ -116,5 +152,6 @@ def update(request, flagged_object_id):
flagged.status = new_status
flagged.save()
if flagged.reason == FlaggedObject.REASON_CONTENT_MODERATION:
return HttpResponseRedirect(reverse("flagit.moderate_content"))
return HttpResponseRedirect(urlparams(reverse("flagit.flagged_queue"), reason=reason))

Просмотреть файл

@ -209,7 +209,7 @@ class Question(AAQBase):
object_id=self.id,
creator=self.creator,
status=FlaggedObject.FLAG_PENDING,
reason="bug_support",
reason=FlaggedObject.REASON_CONTENT_MODERATION,
notes="New question, review topic",
)

Просмотреть файл

@ -660,4 +660,4 @@ class TestActions(TestCase):
"""Creating a question also creates a flag."""
switch_is_active.return_value = True
QuestionFactory(title="Test Question", content="Lorem Ipsum Dolor")
self.assertEqual(1, FlaggedObject.objects.filter(reason="bug_support").count())
self.assertEqual(1, FlaggedObject.objects.filter(reason="content_moderation").count())

Просмотреть файл

@ -61,19 +61,21 @@ document.addEventListener('DOMContentLoaded', () => {
}
}
let reason = updateUrlParameter('get', 'reason');
if (reason) {
reasonFilter.value = reason;
if (reasonFilter) {
let reason = updateUrlParameter('get', 'reason');
if (reason) {
reasonFilter.value = reason;
}
reasonFilter.addEventListener('change', async () => {
const selectedReason = reasonFilter.value;
updateUrlParameter('set', 'reason', selectedReason);
fetchAndUpdateContent(new URL(window.location.href));
});
}
reasonFilter.addEventListener('change', async () => {
const selectedReason = reasonFilter.value;
updateUrlParameter('set', 'reason', selectedReason);
fetchAndUpdateContent(new URL(window.location.href));
});
function handleDropdownChange() {
const dropdowns = document.querySelectorAll('.topic-dropdown, select[name="status"]');
dropdowns.forEach(dropdown => {