diff --git a/kitsune/flagit/jinja2/flagit/content_moderation.html b/kitsune/flagit/jinja2/flagit/content_moderation.html new file mode 100644 index 000000000..2ac5ca291 --- /dev/null +++ b/kitsune/flagit/jinja2/flagit/content_moderation.html @@ -0,0 +1,36 @@ +{% extends "flagit/flagit_base.html" %} + +{% block flagged_items %} + {% for object in objects %} +
  • +
    +
    +

    {{ _('Flagged {t} (Reason: {r})')|f(t=object.content_type, r=object.get_reason_display()) }}

    + {% if object.notes %} +

    {{ _('Other reason:') }} {{ object.notes }}

    + {% endif %} +
    +
    + {% if object.content_object %} + {% include 'flagit/includes/flagged_%s.html' % object.content_type.model %} + {% else %} +

    {{ _('{t} with id={id} no longer exists.')|f(t=object.content_type, id=object.object_id) }}

    + {% endif %} +


    {{ _('Update Status:') }}

    +
    + {% csrf_token %} + + +
    +
    +
    +
  • + {% else %} +

    {{ _('There is no content pending moderation.') }}

    + {% endfor %} +{% endblock %} \ No newline at end of file diff --git a/kitsune/flagit/jinja2/flagit/flagit_base.html b/kitsune/flagit/jinja2/flagit/flagit_base.html new file mode 100644 index 000000000..3fa0280a5 --- /dev/null +++ b/kitsune/flagit/jinja2/flagit/flagit_base.html @@ -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 %} +
    +

    {{ _('Content Pending Moderation') }}

    + + + {% block deactivation_log %} +
    + {{ _('View all deactivated users') }} +
    + {% endblock %} + +{% endblock %} + +{% block side_top %} + +{% endblock %} diff --git a/kitsune/flagit/jinja2/flagit/includes/flagged_question.html b/kitsune/flagit/jinja2/flagit/includes/flagged_question.html index 8d2df3b5b..9e8cbbb06 100644 --- a/kitsune/flagit/jinja2/flagit/includes/flagged_question.html +++ b/kitsune/flagit/jinja2/flagit/includes/flagged_question.html @@ -21,7 +21,7 @@

    {{ _('Flagged:') }}

    {{ date_by_user(object.created, object.creator) }}

    - {% 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') %}

    {{ _('Take Action:') }}

    diff --git a/kitsune/flagit/jinja2/flagit/queue.html b/kitsune/flagit/jinja2/flagit/queue.html index 2eab60323..328eb069e 100644 --- a/kitsune/flagit/jinja2/flagit/queue.html +++ b/kitsune/flagit/jinja2/flagit/queue.html @@ -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 %} -
    -

    {{ _('Content Pending Moderation') }}

    - - +
    + + {% else %} +

    {{ _('There is no content pending moderation.') }}

    + {% endfor %} +{% endblock %} -
    - {{ _('View all deactivated users') }} -
    +{% block side_top_reason %} + +
    +
    + + +
    {% endblock %} - -{% block side_top %} - -{% endblock %} diff --git a/kitsune/flagit/models.py b/kitsune/flagit/models.py index fe133d739..f816e0d9d 100644 --- a/kitsune/flagit/models.py +++ b/kitsune/flagit/models.py @@ -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 diff --git a/kitsune/flagit/urls.py b/kitsune/flagit/urls.py index 128c76d07..5a3d97656 100644 --- a/kitsune/flagit/urls.py +++ b/kitsune/flagit/urls.py @@ -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\d+)$", views.update, name="flagit.update"), ] diff --git a/kitsune/flagit/views.py b/kitsune/flagit/views.py index 05cf39b45..d620c0579 100644 --- a/kitsune/flagit/views.py +++ b/kitsune/flagit/views.py @@ -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)) diff --git a/kitsune/questions/models.py b/kitsune/questions/models.py index 6008e9eea..8d7a4d2dd 100755 --- a/kitsune/questions/models.py +++ b/kitsune/questions/models.py @@ -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", ) diff --git a/kitsune/questions/tests/test_models.py b/kitsune/questions/tests/test_models.py index 9746a9c44..3c5dfe5fc 100644 --- a/kitsune/questions/tests/test_models.py +++ b/kitsune/questions/tests/test_models.py @@ -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()) diff --git a/kitsune/sumo/static/sumo/js/flagit.js b/kitsune/sumo/static/sumo/js/flagit.js index 2dc9e1070..59465bc75 100644 --- a/kitsune/sumo/static/sumo/js/flagit.js +++ b/kitsune/sumo/static/sumo/js/flagit.js @@ -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 => {