-
Notifications
You must be signed in to change notification settings - Fork 742
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
provide flagged-object queues per user #6457
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,7 +6,16 @@ | |
<li id="flagged-item-{{ object.id }}" 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> | ||
<div class="flagged-item-header"> | ||
<h2 class="sumo-card-heading">{{ _('Flagged {t}')|f(t=object.content_type) }}</h2> | ||
<span> | ||
{% if object.assignee %} | ||
{{ _('Assigned: {username}')|f(username=object.assignee.username) }} | ||
{% else %} | ||
{{ _('Unassigned') }} | ||
{% endif %} | ||
</span> | ||
</div> | ||
{% if object.notes %} | ||
{% if object.content_type.model == 'question' %} | ||
<p class="notes">{{ _('Additional notes:') }} <a target="_blank" href="{{ object.content_object.get_absolute_url() }}">{{ object.notes }}</a></p> | ||
|
@@ -57,6 +66,29 @@ <h3 class="sumo-card-heading"><br>{{ _('Update Status:') }}</h3> | |
options=products, | ||
selected_filter=selected_product | ||
) }} | ||
{{ filter_dropdown( | ||
form_id='assignee-filter-form', | ||
select_id='flagit-assignee-filter', | ||
label='Filter by assignee:', | ||
name='assignee', | ||
default_option='All assignees', | ||
options=assignees, | ||
selected_filter=selected_assignee | ||
) }} | ||
<div id="my-queue-tools" data-current-username="{{ current_username }}" | ||
{% if not (selected_assignee and (selected_assignee == current_username)) %} hidden{% endif %}> | ||
<label>{{ _('Manage my queue') }}:</label> | ||
<form id="my-queue-assign" hx-post="" hx-target="#flagged-queue" hx-select="#flagged-queue"> | ||
{% csrf_token %} | ||
<input type="hidden" name="action" value="assign"> | ||
<input class="sumo-button secondary-button button-lg btn" type="submit" value="{{ _('Assign more') }}" /> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For buttons like |
||
</form> | ||
<form id="my-queue-unassign" hx-post="" hx-target="#flagged-queue" hx-select="#flagged-queue"> | ||
{% csrf_token %} | ||
<input type="hidden" name="action" value="unassign"> | ||
<input class="sumo-button secondary-button button-lg btn" type="submit" value="{{ _('Unassign all') }}" /> | ||
</form> | ||
</div> | ||
{% endblock %} | ||
{# Hide the deactivation log on content moderation #} | ||
{% block deactivation_log %} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
# Generated by Django 4.2.18 on 2025-01-16 09:37 | ||
|
||
from django.conf import settings | ||
from django.db import migrations, models | ||
import django.db.models.deletion | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
dependencies = [ | ||
migrations.swappable_dependency(settings.AUTH_USER_MODEL), | ||
("flagit", "0004_alter_flaggedobject_reason"), | ||
] | ||
|
||
operations = [ | ||
migrations.AddField( | ||
model_name="flaggedobject", | ||
name="assigned", | ||
field=models.DateTimeField(default=None, null=True), | ||
), | ||
migrations.AddField( | ||
model_name="flaggedobject", | ||
name="assignee", | ||
field=models.ForeignKey( | ||
null=True, | ||
on_delete=django.db.models.deletion.SET_NULL, | ||
related_name="assigned_flags", | ||
to=settings.AUTH_USER_MODEL, | ||
), | ||
), | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -54,6 +54,11 @@ class FlaggedObject(ModelBase): | |
handled = models.DateTimeField(default=datetime.now, db_index=True) | ||
handled_by = models.ForeignKey(User, on_delete=models.CASCADE, null=True) | ||
|
||
assignee = models.ForeignKey( | ||
User, on_delete=models.SET_NULL, null=True, related_name="assigned_flags" | ||
) | ||
assigned = models.DateTimeField(default=None, null=True) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: this can be confused with a boolean flag. Maybe rename it to something like |
||
|
||
objects = FlaggedObjectManager() | ||
|
||
class Meta: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,21 @@ | ||
import json | ||
|
||
from django.contrib import messages | ||
from django.contrib.auth.models import User | ||
from django.contrib.contenttypes.models import ContentType | ||
from django.core.cache import cache | ||
from django.http import HttpResponse, HttpResponseRedirect | ||
from django.db.models.functions import Now | ||
from django.http import ( | ||
HttpResponse, | ||
HttpResponseBadRequest, | ||
HttpResponseForbidden, | ||
HttpResponseNotFound, | ||
HttpResponseRedirect, | ||
) | ||
from django.shortcuts import get_object_or_404, render | ||
from django.utils import timezone | ||
from django.utils.translation import gettext as _ | ||
from django.views.decorators.http import require_POST | ||
from django.views.decorators.http import require_http_methods, require_POST | ||
|
||
from kitsune.access.decorators import group_required, login_required, permission_required | ||
from kitsune.flagit.models import FlaggedObject | ||
|
@@ -163,9 +171,26 @@ def build_hierarchy(parent_id=None, level=0): | |
|
||
|
||
@group_required("Content Moderators") | ||
@require_http_methods(["GET", "POST"]) | ||
def moderate_content(request): | ||
"""Display flagged content that needs moderation.""" | ||
product_slug = request.GET.get("product") | ||
assignee = request.GET.get("assignee") | ||
|
||
if request.method == "POST": | ||
if not (assignee and (request.user.username == assignee)): | ||
return HttpResponseForbidden() | ||
action = request.POST.get("action") | ||
if not (action and (action in ("assign", "unassign"))): | ||
return HttpResponseBadRequest() | ||
else: | ||
if ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can move this above the check if the request is if request.method == "POST": |
||
assignee | ||
and not User.objects.filter( | ||
is_active=True, username=assignee, groups__name="Content Moderators" | ||
).exists() | ||
): | ||
return HttpResponseNotFound() | ||
|
||
content_type = ContentType.objects.get_for_model(Question) | ||
objects = ( | ||
|
@@ -174,10 +199,22 @@ def moderate_content(request): | |
content_model=content_type, | ||
product_slug=product_slug, | ||
) | ||
.select_related("content_type", "creator") | ||
.select_related("content_type", "creator", "assignee") | ||
.prefetch_related("content_object__product") | ||
) | ||
|
||
if request.method == "POST": | ||
if action == "assign": | ||
# Assign another batch of objects to the user. | ||
assigment_qs = objects.filter(assignee__isnull=True)[:20].values_list("id", flat=True) | ||
objects.filter(id__in=assigment_qs).update(assignee=request.user, assigned=Now()) | ||
else: | ||
# Unassign all of the user's objects. | ||
objects.filter(assignee=request.user).update(assignee=None, assigned=None) | ||
|
||
if assignee: | ||
objects = objects.filter(assignee__username=assignee) | ||
|
||
# It's essential that the objects are ordered for pagination. The | ||
# default ordering for flagged objects is by ascending created date. | ||
objects = paginate(request, objects) | ||
|
@@ -204,6 +241,15 @@ def moderate_content(request): | |
for p in Product.active.filter(codename="", aaq_configs__is_active=True) | ||
], | ||
"selected_product": product_slug, | ||
"assignees": sorted( | ||
(user.username, user.get_full_name() or user.username) | ||
for user in User.objects.filter( | ||
is_active=True, | ||
groups__name="Content Moderators", | ||
).distinct() | ||
), | ||
"selected_assignee": assignee, | ||
"current_username": request.user.username, | ||
}, | ||
) | ||
|
||
|
@@ -227,6 +273,8 @@ def update(request, flagged_object_id): | |
QuestionReplyEvent(answer).fire(exclude=[answer.creator]) | ||
|
||
flagged.status = new_status | ||
flagged.assignee = None | ||
flagged.assigned = None | ||
flagged.save() | ||
if flagged.reason == FlaggedObject.REASON_CONTENT_MODERATION: | ||
question = flagged.content_object | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this ID
id
is not used anywhere.