Abuse reports: moderator form for resolving/dismissing + notification #173

Merged
Oleg-Komarov merged 7 commits from resolve-report-form into main 2024-06-07 17:04:54 +02:00
7 changed files with 127 additions and 27 deletions
Showing only changes of commit d47931e689 - Show all commits

View File

@ -19,8 +19,7 @@ class AbuseReportTypeFilter(admin.SimpleListFilter):
parameter_name = 'type' parameter_name = 'type'
def lookups(self, request, model_admin): 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 tuple is the coded value for the option that will
appear in the URL query. The second element is the appear in the URL query. The second element is the
human-readable name for the option that will appear human-readable name for the option that will appear
@ -32,8 +31,7 @@ class AbuseReportTypeFilter(admin.SimpleListFilter):
) )
def queryset(self, request, queryset): 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 provided in the query string and retrievable via
`self.value()`. `self.value()`.
""" """
@ -78,6 +76,8 @@ class AbuseReportAdmin(CommaSearchInAdminMixin, admin.ModelAdmin):
'message', 'message',
'extension_version', 'extension_version',
'version', 'version',
'processed_by_moderator',
'moderator_note',
) )
fieldsets = ( fieldsets = (
('Abuse Report Core Information', {'fields': ('status', 'reason', 'message')}), ('Abuse Report Core Information', {'fields': ('status', 'reason', 'message')}),
@ -89,6 +89,8 @@ class AbuseReportAdmin(CommaSearchInAdminMixin, admin.ModelAdmin):
'date_modified', 'date_modified',
'reporter', 'reporter',
'version', 'version',
'processed_by_moderator',
'moderator_note',
) )
}, },
), ),

View File

@ -41,3 +41,33 @@ class ReportRatingForm(forms.ModelForm):
class Meta: class Meta:
model = abuse.models.AbuseReport model = abuse.models.AbuseReport
fields = ('reason', 'message') 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)

View 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),
),
]

View File

@ -29,7 +29,7 @@ class AbuseReport(CreatedModifiedMixin, TrackChangesMixin, models.Model):
STATUSES = Choices( STATUSES = Choices(
('UNTRIAGED', 1, 'Untriaged'), ('UNTRIAGED', 1, 'Untriaged'),
('CONFIRMED', 2, 'Confirmed'), ('DISMISSED', 2, 'Dismissed'),
('RESOLVED', 3, 'Resolved'), ('RESOLVED', 3, 'Resolved'),
) )
@ -68,6 +68,13 @@ class AbuseReport(CreatedModifiedMixin, TrackChangesMixin, models.Model):
) )
status = models.PositiveSmallIntegerField(default=STATUSES.UNTRIAGED, choices=STATUSES.choices) 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 @classmethod
def lookup_country_code_from_ip(cls, ip): def lookup_country_code_from_ip(cls, ip):

View File

@ -9,6 +9,7 @@
<div class="hero extension-detail"> <div class="hero extension-detail">
<div class="container"> <div class="container">
<div class="hero-content"> <div class="hero-content">
{% if user_is_moderator %}
{% block hero_breadcrumbs %} {% block hero_breadcrumbs %}
<div class="hero-breadcrumbs"> <div class="hero-breadcrumbs">
<a href="{% url 'abuse:report-list' %}"> <a href="{% url 'abuse:report-list' %}">
@ -17,6 +18,7 @@
</a> </a>
</div> </div>
{% endblock hero_breadcrumbs %} {% endblock hero_breadcrumbs %}
{% endif %}
<h1>{{ object.get_type_display }} Report</h1> <h1>{{ object.get_type_display }} Report</h1>
@ -73,12 +75,12 @@
<div class="mb-3"> <div class="mb-3">
<div class="row"> <div class="row">
<div class="col-md-8"> <div class="col-md-8">
<h2>Reason</h2>
<p>"{{ object.get_reason_display }}"</p>
{% if object.rating %} {% if object.rating %}
{% include "ratings/components/rating.html" with rating=object.rating classes="mb-3" %} {% include "ratings/components/rating.html" with rating=object.rating classes="mb-3" %}
<hr class="my-4"> <hr class="my-4">
{% endif %} {% endif %}
<h2>Reason</h2>
<p>"{{ object.get_reason_display }}"</p>
<h2 class="mt-4 mb-3">Message</h2> <h2 class="mt-4 mb-3">Message</h2>
<div class="card p-3 mx-1 mb-3"> <div class="card p-3 mx-1 mb-3">
{% if object.message %} {% if object.message %}
@ -96,30 +98,25 @@
<dt>Status</dt> <dt>Status</dt>
<dd>{% include "common/components/status.html" %}</dd> <dd>{% include "common/components/status.html" %}</dd>
</div> </div>
{% if object.processed_by_moderator %}
<div class="dl-col">
<dt>Processed by</dt>
<dd>{{ object.processed_by_moderator }}</dd>
</div>
{% endif %}
</div> </div>
{% if extension %}
<div class="dl-row"> <div class="dl-row">
<div class="dl-col"> <div class="dl-col">
{% if extension %}
<dt>{{ extension.get_type_display }}</dt> <dt>{{ extension.get_type_display }}</dt>
<dd> <dd>
<a href="{{ extension.get_absolute_url }}"> <a href="{{ extension.get_absolute_url }}">
{{ extension.name }} {{ extension.name }}
</a> </a>
</dd> </dd>
{% else %}
<dt>Reported user</dt>
<dd>{{ object.name }}</dd>
{% 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> </div>
{% endif %}
<div class="dl-row"> <div class="dl-row">
<div class="dl-col"> <div class="dl-col">
<dt>Submitted</dt> <dt>Submitted</dt>
@ -145,4 +142,23 @@
</div> </div>
</div> </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 %} {% endblock content %}

View File

@ -1,5 +1,5 @@
{% extends "common/base.html" %} {% extends "common/base.html" %}
{% load i18n humanize filters %} {% load common i18n humanize filters %}
{% block page_title %}Approval queue{% endblock page_title %} {% block page_title %}Approval queue{% endblock page_title %}
@ -25,6 +25,7 @@
<th>{% trans "Reporter" %}</th> <th>{% trans "Reporter" %}</th>
<th>{% trans "Submitted" %}</th> <th>{% trans "Submitted" %}</th>
<th>{% trans "Status" %}</th> <th>{% trans "Status" %}</th>
<th>{% trans "Process by" %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -50,6 +51,9 @@
{% include "common/components/status.html" with object=report classes="d-block" %} {% include "common/components/status.html" with object=report classes="d-block" %}
</a> </a>
</td> </td>
<td>
{{ report.processed_by_moderator|default:"-" }}
</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@ -58,4 +62,5 @@
<p>{% trans "No extensions to review." %}</p> <p>{% trans "No extensions to review." %}</p>
{% endif %} {% endif %}
</section> </section>
{{ page_obj|paginator }}
{% endblock content %} {% endblock content %}

View File

@ -1,13 +1,12 @@
import logging import logging
from django.contrib.auth.mixins import UserPassesTestMixin, LoginRequiredMixin from django.contrib.auth.mixins import UserPassesTestMixin, LoginRequiredMixin
from django.http import Http404 from django.http import Http404, HttpResponseForbidden
from django.views.generic import DetailView
from django.views.generic.list import ListView 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 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 constants.base import ABUSE_TYPE_EXTENSION, ABUSE_TYPE_RATING
from abuse.models import AbuseReport from abuse.models import AbuseReport
from ratings.models import Rating from ratings.models import Rating
@ -29,7 +28,7 @@ class ReportList(
return self.request.user.is_moderator return self.request.user.is_moderator
def get_queryset(self): 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' template_name = 'abuse/abusereport_list.html'
@ -123,8 +122,10 @@ class ReportRatingView(
return self.object.get_absolute_url() return self.object.get_absolute_url()
class ReportView(LoginRequiredMixin, DetailView): class ReportView(LoginRequiredMixin, UpdateView):
form_class = ResolveReportForm
model = AbuseReport model = AbuseReport
template_name = 'abuse/abusereport_detail.html'
def get_object(self, *args, **kwargs): def get_object(self, *args, **kwargs):
obj = super().get_object(*args, **kwargs) obj = super().get_object(*args, **kwargs)
@ -133,3 +134,11 @@ class ReportView(LoginRequiredMixin, DetailView):
): ):
return obj return obj
raise Http404() 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)