extensions-website/abuse/admin.py
Oleg Komarov 012845c712 Abuse reports: moderator form for resolving/dismissing + notification (#173)
It is possible to submit the form update the note and status multiple times,
e.g. a report that was initially dismissed, can be resolved later.
Each valid form submission will generate a notification for the reporter.

Once a note is saved, it becomes visible to the reporter.

The migration in this PR replaces the AbuseReport status Confirmed(=2)
that wasn't used in production with Dismissed(=2). Assuming that this is
a minor sin while the project is still in beta.

Reviewed-on: #173
Reviewed-by: Anna Sirota <annasirota@noreply.localhost>
2024-06-07 17:04:52 +02:00

196 lines
6.5 KiB
Python

from django.contrib import admin
from django.template.defaultfilters import truncatechars
from django.utils.translation import gettext
from rangefilter.filter import DateRangeFilter
from .models import AbuseReport
from access import acl
from common.admin import CommaSearchInAdminMixin
from constants import permissions
class AbuseReportTypeFilter(admin.SimpleListFilter):
# Human-readable title to be displayed in the sidebar just above the filter options.
# L10n: label for the list of abuse report types: extensions, users
title = gettext('type')
# Parameter for the filter that will be used in the URL query.
parameter_name = 'type'
def lookups(self, request, model_admin):
"""Returns a list of tuples. The first element in each
tuple is the coded value for the option that will
appear in the URL query. The second element is the
human-readable name for the option that will appear
in the right sidebar.
"""
return (
('user', gettext('Users')),
('extension', gettext('Extensions')),
)
def queryset(self, request, queryset):
"""Returns the filtered queryset based on the value
provided in the query string and retrievable via
`self.value()`.
"""
if self.value() == 'user':
return queryset.filter(user__isnull=False)
elif self.value() == 'extension':
return queryset.filter(extension_id__isnull=False)
return queryset
class AbuseReportAdmin(CommaSearchInAdminMixin, admin.ModelAdmin):
save_on_top = True
view_on_site = True
actions = ('delete_selected', 'mark_as_valid', 'mark_as_suspicious')
date_hierarchy = 'date_modified'
list_display = (
'date_created',
'name',
'type',
'status',
'reason',
'message_excerpt',
)
list_filter = (
AbuseReportTypeFilter,
'status',
'reason',
('date_created', DateRangeFilter),
)
list_select_related = ('user',)
# Shouldn't be needed because those fields should all be readonly, but just
# in case we change our mind, FKs should be raw id fields as usual in our
# admin tools.
raw_id_fields = ('user', 'reporter')
# All fields except status must be readonly - the submitted data should
# not be changed, only the status for triage.
readonly_fields = (
'date_created',
'date_modified',
'reporter',
'user',
'message',
'extension_version',
'version',
'processed_by',
'moderator_note',
)
fieldsets = (
('Abuse Report Core Information', {'fields': ('status', 'reason', 'message')}),
(
'Abuse Report Data',
{
'fields': (
'date_created',
'date_modified',
'reporter',
'version',
'processed_by',
'moderator_note',
)
},
),
)
# The first fieldset is going to be dynamically added through
# get_fieldsets() depending on the target (add-on, user or unknown add-on),
# using the fields below:
dynamic_fieldset_fields = {
# User
'user': (('User', {'fields': ('user',)}),),
# FIXME
# Extension, we only have the extension slug and maybe some extra extension_*
# fields that were submitted with the report, we'll try to display the
# extension card if we can find a matching add-on in the database though.
'extension': (('Extension', {'fields': ('extension', 'extension_version')}),),
}
def has_add_permission(self, request):
# Adding new abuse reports through the admin is useless, so we prevent it.
return False
def change_view(self, request, object_id, form_url='', extra_context=None):
extra_context = extra_context or {}
extra_context['show_save_and_continue'] = False # Don't need this.
return super().change_view(
request,
object_id,
form_url,
extra_context=extra_context,
)
def get_actions(self, request):
actions = super().get_actions(request)
if not acl.action_allowed_for(request.user, permissions.ABUSEREPORTS_EDIT):
# You need AbuseReports:Edit for the extra actions.
actions.pop('mark_as_valid')
actions.pop('mark_as_suspicious')
return actions
def get_search_fields(self, request):
"""Return search fields according to the type filter."""
type_ = request.GET.get('type')
if type_ == 'extension':
search_fields = (
'extension__name',
'extension__slug',
'message',
)
elif type_ == 'user':
search_fields = (
'message',
'=user__id',
'^user__username',
'^user__email',
)
else:
search_fields = ()
return search_fields
def get_search_id_field(self, request):
"""Return the field to use when all search terms are numeric, match the type filter."""
type_ = request.GET.get('type')
if type_ == 'user':
search_field = 'user_id'
else:
search_field = super().get_search_id_field(request)
return search_field
def get_fieldsets(self, request, obj=None):
if obj.user:
target = 'user'
else:
target = 'extension'
return self.dynamic_fieldset_fields[target] + self.fieldsets
def message_excerpt(self, obj):
return truncatechars(obj.message, 140) if obj.message else ''
message_excerpt.short_description = gettext('Message excerpt')
def mark_as_valid(self, request, qs):
for obj in qs:
obj.update(status=AbuseReport.STATUSES.VALID)
self.message_user(
request,
gettext('The %d selected reports have been marked as valid.' % (qs.count())),
)
mark_as_valid.short_description = 'Mark selected abuse reports as valid'
def mark_as_suspicious(self, request, qs):
for obj in qs:
obj.update(status=AbuseReport.STATUSES.SUSPICIOUS)
self.message_user(
request,
gettext('The %d selected reports have been marked as suspicious.' % (qs.count())),
)
mark_as_suspicious.short_description = gettext('Mark selected abuse reports as suspicious')
admin.site.register(AbuseReport, AbuseReportAdmin)