Abuse reports: moderator form for resolving/dismissing + notification #173
@ -19,8 +19,7 @@ class AbuseReportTypeFilter(admin.SimpleListFilter):
|
||||
parameter_name = 'type'
|
||||
|
||||
def lookups(self, request, model_admin):
|
||||
"""
|
||||
Returns a list of tuples. The first element in each
|
||||
"""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
|
||||
@ -32,8 +31,7 @@ class AbuseReportTypeFilter(admin.SimpleListFilter):
|
||||
)
|
||||
|
||||
def queryset(self, request, queryset):
|
||||
"""
|
||||
Returns the filtered queryset based on the value
|
||||
"""Returns the filtered queryset based on the value
|
||||
provided in the query string and retrievable via
|
||||
`self.value()`.
|
||||
"""
|
||||
@ -78,6 +76,8 @@ class AbuseReportAdmin(CommaSearchInAdminMixin, admin.ModelAdmin):
|
||||
'message',
|
||||
'extension_version',
|
||||
'version',
|
||||
'processed_by_moderator',
|
||||
'moderator_note',
|
||||
)
|
||||
fieldsets = (
|
||||
('Abuse Report Core Information', {'fields': ('status', 'reason', 'message')}),
|
||||
@ -89,6 +89,8 @@ class AbuseReportAdmin(CommaSearchInAdminMixin, admin.ModelAdmin):
|
||||
'date_modified',
|
||||
'reporter',
|
||||
'version',
|
||||
'processed_by_moderator',
|
||||
'moderator_note',
|
||||
)
|
||||
},
|
||||
),
|
||||
|
@ -41,3 +41,33 @@ class ReportRatingForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = abuse.models.AbuseReport
|
||||
fields = ('reason', 'message')
|
||||
|
||||
|
||||
class ResolveReportForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = abuse.models.AbuseReport
|
||||
fields = ('moderator_note',)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.request = kwargs.pop('request')
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
if 'dismiss' in self.data:
|
||||
self.instance.status = self.instance.STATUSES.DISMISSED
|
||||
if 'resolve' in self.data:
|
||||
self.instance.status = self.instance.STATUSES.RESOLVED
|
||||
self.instance.processed_by_moderator = self.request.user
|
||||
return self.cleaned_data
|
||||
|
||||
def clean_moderator_note(self, *args, **kwargs):
|
||||
moderator_note = self.cleaned_data.get('moderator_note')
|
||||
if not moderator_note:
|
||||
raise forms.ValidationError('this field is required')
|
||||
return moderator_note
|
||||
|
||||
def is_valid(self, *args, **kwargs) -> bool:
|
||||
if 'dismiss' not in self.data and 'resolve' not in self.data:
|
||||
return False
|
||||
return super().is_valid(*args, **kwargs)
|
||||
|
31
abuse/migrations/0010_abusereport_moderator_note_and_more.py
Normal file
31
abuse/migrations/0010_abusereport_moderator_note_and_more.py
Normal file
@ -0,0 +1,31 @@
|
||||
# Generated by Django 4.2.11 on 2024-06-07 12:40
|
||||
|
||||
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),
|
||||
('abuse', '0009_alter_abusereport_user'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='abusereport',
|
||||
name='moderator_note',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='abusereport',
|
||||
name='processed_by_moderator',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='abuse_reports_processed', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='abusereport',
|
||||
name='status',
|
||||
field=models.PositiveSmallIntegerField(choices=[(1, 'Untriaged'), (2, 'Dismissed'), (3, 'Resolved')], default=1),
|
||||
),
|
||||
]
|
@ -29,7 +29,7 @@ class AbuseReport(CreatedModifiedMixin, TrackChangesMixin, models.Model):
|
||||
|
||||
STATUSES = Choices(
|
||||
('UNTRIAGED', 1, 'Untriaged'),
|
||||
('CONFIRMED', 2, 'Confirmed'),
|
||||
('DISMISSED', 2, 'Dismissed'),
|
||||
('RESOLVED', 3, 'Resolved'),
|
||||
)
|
||||
|
||||
@ -68,6 +68,13 @@ class AbuseReport(CreatedModifiedMixin, TrackChangesMixin, models.Model):
|
||||
)
|
||||
|
||||
status = models.PositiveSmallIntegerField(default=STATUSES.UNTRIAGED, choices=STATUSES.choices)
|
||||
moderator_note = models.TextField(blank=True, null=True)
|
||||
processed_by_moderator = models.ForeignKey(
|
||||
User,
|
||||
null=True,
|
||||
related_name='abuse_reports_processed',
|
||||
on_delete=models.PROTECT,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def lookup_country_code_from_ip(cls, ip):
|
||||
|
@ -9,6 +9,7 @@
|
||||
<div class="hero extension-detail">
|
||||
<div class="container">
|
||||
<div class="hero-content">
|
||||
{% if user_is_moderator %}
|
||||
{% block hero_breadcrumbs %}
|
||||
<div class="hero-breadcrumbs">
|
||||
<a href="{% url 'abuse:report-list' %}">
|
||||
@ -17,6 +18,7 @@
|
||||
</a>
|
||||
</div>
|
||||
{% endblock hero_breadcrumbs %}
|
||||
{% endif %}
|
||||
|
||||
<h1>{{ object.get_type_display }} Report</h1>
|
||||
|
||||
@ -73,12 +75,12 @@
|
||||
<div class="mb-3">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<h2>Reason</h2>
|
||||
<p>"{{ object.get_reason_display }}"</p>
|
||||
{% if object.rating %}
|
||||
{% include "ratings/components/rating.html" with rating=object.rating classes="mb-3" %}
|
||||
<hr class="my-4">
|
||||
{% endif %}
|
||||
<h2>Reason</h2>
|
||||
<p>"{{ object.get_reason_display }}"</p>
|
||||
<h2 class="mt-4 mb-3">Message</h2>
|
||||
<div class="card p-3 mx-1 mb-3">
|
||||
{% if object.message %}
|
||||
@ -96,30 +98,25 @@
|
||||
<dt>Status</dt>
|
||||
<dd>{% include "common/components/status.html" %}</dd>
|
||||
</div>
|
||||
{% if object.processed_by_moderator %}
|
||||
<div class="dl-col">
|
||||
<dt>Processed by</dt>
|
||||
<dd>{{ object.processed_by_moderator }}</dd>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if extension %}
|
||||
<div class="dl-row">
|
||||
<div class="dl-col">
|
||||
{% if extension %}
|
||||
<dt>{{ extension.get_type_display }}</dt>
|
||||
<dd>
|
||||
<a href="{{ extension.get_absolute_url }}">
|
||||
{{ extension.name }}
|
||||
</a>
|
||||
</dd>
|
||||
{% else %}
|
||||
<dt>Reported user</dt>
|
||||
<dd>{{ object.name }}</dd>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="dl-row">
|
||||
<div class="dl-col">
|
||||
<dt>Compatibility</dt>
|
||||
<dd>
|
||||
{% if object.version %}Blender {{ object.version }}{% else %}Other{% endif %}
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dl-row">
|
||||
<div class="dl-col">
|
||||
<dt>Submitted</dt>
|
||||
@ -145,4 +142,23 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if user_is_moderator %}
|
||||
<div class="col-md-8">
|
||||
<form method="post" enctype="multipart/form-data" class="mt-3">
|
||||
{% csrf_token %}
|
||||
{% with form=form|add_form_classes %}
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
{% include "common/components/field.html" with field=form.moderator_note placeholder="Add a note..." required=True %}
|
||||
</div>
|
||||
</div>
|
||||
{% endwith %}
|
||||
<button type="submit" class="btn btn-primary" name="resolve"><i class="i-check"></i> Resolve</button>
|
||||
<button type="submit" class="btn btn-secondary" name="dismiss"><i class="i-trash"></i> Dismiss</button>
|
||||
</form>
|
||||
</div>
|
||||
{% elif object.moderator_note %}
|
||||
<h2>Moderator note</h2>
|
||||
<p>{{ object.moderator_note }}</p>
|
||||
{% endif %}
|
||||
{% endblock content %}
|
||||
|
@ -1,5 +1,5 @@
|
||||
{% extends "common/base.html" %}
|
||||
{% load i18n humanize filters %}
|
||||
{% load common i18n humanize filters %}
|
||||
|
||||
{% block page_title %}Approval queue{% endblock page_title %}
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
<th>{% trans "Reporter" %}</th>
|
||||
<th>{% trans "Submitted" %}</th>
|
||||
<th>{% trans "Status" %}</th>
|
||||
<th>{% trans "Process by" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -50,6 +51,9 @@
|
||||
{% include "common/components/status.html" with object=report classes="d-block" %}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
{{ report.processed_by_moderator|default:"-" }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@ -58,4 +62,5 @@
|
||||
<p>{% trans "No extensions to review." %}</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
{{ page_obj|paginator }}
|
||||
{% endblock content %}
|
||||
|
@ -1,13 +1,12 @@
|
||||
import logging
|
||||
|
||||
from django.contrib.auth.mixins import UserPassesTestMixin, LoginRequiredMixin
|
||||
from django.http import Http404
|
||||
from django.views.generic import DetailView
|
||||
from django.http import Http404, HttpResponseForbidden
|
||||
from django.views.generic.list import ListView
|
||||
from django.views.generic.edit import CreateView
|
||||
from django.views.generic.edit import CreateView, UpdateView
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
|
||||
from .forms import ReportExtensionForm, ReportRatingForm
|
||||
from .forms import ReportExtensionForm, ReportRatingForm, ResolveReportForm
|
||||
from constants.base import ABUSE_TYPE_EXTENSION, ABUSE_TYPE_RATING
|
||||
from abuse.models import AbuseReport
|
||||
from ratings.models import Rating
|
||||
@ -29,7 +28,7 @@ class ReportList(
|
||||
return self.request.user.is_moderator
|
||||
|
||||
def get_queryset(self):
|
||||
return AbuseReport.objects.all().order_by('-date_created')
|
||||
return AbuseReport.objects.all().order_by('status', '-date_created')
|
||||
|
||||
template_name = 'abuse/abusereport_list.html'
|
||||
|
||||
@ -123,8 +122,10 @@ class ReportRatingView(
|
||||
return self.object.get_absolute_url()
|
||||
|
||||
|
||||
class ReportView(LoginRequiredMixin, DetailView):
|
||||
class ReportView(LoginRequiredMixin, UpdateView):
|
||||
form_class = ResolveReportForm
|
||||
model = AbuseReport
|
||||
template_name = 'abuse/abusereport_detail.html'
|
||||
|
||||
def get_object(self, *args, **kwargs):
|
||||
obj = super().get_object(*args, **kwargs)
|
||||
@ -133,3 +134,11 @@ class ReportView(LoginRequiredMixin, DetailView):
|
||||
):
|
||||
return obj
|
||||
raise Http404()
|
||||
|
||||
def get_form(self):
|
||||
return ResolveReportForm(**self.get_form_kwargs(), request=self.request)
|
||||
|
||||
def post(self, *args, **kwargs):
|
||||
if not self.request.user.is_moderator:
|
||||
raise HttpResponseForbidden()
|
||||
return super().post(*args, **kwargs)
|
||||
|
Loading…
Reference in New Issue
Block a user