-
- {{ _('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:') }}
-
-
+{% 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:') }}
+
-
- {% else %}
-
{{ _('There is no content pending moderation.') }}
- {% endfor %}
-
+
+
+ {% else %}
+
{{ _('There is no content pending moderation.') }}
+ {% endfor %}
+{% endblock %}
-
+{% 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 => {