зеркало из https://github.com/mozilla/kitsune.git
Merge pull request #6354 from akatsoulas/extract-moderation-tool
Extract moderation tool to a separate url
This commit is contained in:
Коммит
8a157f72de
|
@ -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 => {
|
||||
|
|
Загрузка…
Ссылка в новой задаче