Abuse reports: moderator form for resolving/dismissing + notification #173
@ -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',
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -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)
|
||||||
|
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(
|
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):
|
||||||
|
@ -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>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</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 %}
|
</div>
|
||||||
<dt>Reported user</dt>
|
</div>
|
||||||
<dd>{{ object.name }}</dd>
|
|
||||||
{% endif %}
|
{% 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-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 %}
|
||||||
|
@ -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 %}
|
||||||
|
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user