mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-11-22 09:31:08 +00:00
Converts report "comments" into broader "actions" table
This table will now track all actions taken on a report, like resolving it, re-opening it, suspending the reported user, et cetera, in addition to comments. When there are multiple admins, this change will make it easier to understand what actions have been taken by whom on a report.
This commit is contained in:
parent
ab146f652a
commit
b3a519c082
11 changed files with 185 additions and 68 deletions
|
@ -6,13 +6,31 @@ from django.db import migrations, models
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0178_auto_20230328_2132'),
|
||||
("bookwyrm", "0178_auto_20230328_2132"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='reportcomment',
|
||||
name='comment_type',
|
||||
field=models.CharField(choices=[('comment', 'Comment'), ('resolve', 'Resolved report'), ('reopen', 'Re-opened report'), ('message_reporter', 'Messaged reporter'), ('message_offender', 'Messaged reported user'), ('user_suspension', 'Suspended user'), ('user_deletion', 'Deleted user account'), ('block_domain', 'Blocked domain'), ('approve_domain', 'Approved domain'), ('delete_item', 'Deleted item')], default='comment', max_length=20),
|
||||
model_name="reportcomment",
|
||||
name="action_type",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("comment", "Comment"),
|
||||
("resolve", "Resolved report"),
|
||||
("reopen", "Re-opened report"),
|
||||
("message_reporter", "Messaged reporter"),
|
||||
("message_offender", "Messaged reported user"),
|
||||
("user_suspension", "Suspended user"),
|
||||
("user_unsuspension", "Un-suspended user"),
|
||||
("user_perms", "Changed user permission level"),
|
||||
("user_deletion", "Deleted user account"),
|
||||
("block_domain", "Blocked domain"),
|
||||
("approve_domain", "Approved domain"),
|
||||
("delete_item", "Deleted item"),
|
||||
],
|
||||
default="comment",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
migrations.RenameModel("ReportComment", "ReportAction"),
|
||||
]
|
||||
|
|
|
@ -20,7 +20,7 @@ from .readthrough import ReadThrough, ProgressUpdate, ProgressMode
|
|||
from .user import User, KeyPair
|
||||
from .annual_goal import AnnualGoal
|
||||
from .relationship import UserFollows, UserFollowRequest, UserBlocks
|
||||
from .report import Report, ReportComment
|
||||
from .report import Report, ReportAction
|
||||
from .federated_server import FederatedServer
|
||||
|
||||
from .group import Group, GroupMember, GroupMemberInvitation
|
||||
|
|
|
@ -7,6 +7,21 @@ from bookwyrm.settings import DOMAIN
|
|||
from .base_model import BookWyrmModel
|
||||
|
||||
|
||||
# Report action enums
|
||||
COMMENT = "comment"
|
||||
RESOLVE = "resolve"
|
||||
REOPEN = "reopen"
|
||||
MESSAGE_REPORTER = "message_reporter"
|
||||
MESSAGE_OFFENDER = "message_offender"
|
||||
USER_SUSPENSION = "user_suspension"
|
||||
USER_UNSUSPENSION = "user_unsuspension"
|
||||
USER_DELETION = "user_deletion"
|
||||
USER_PERMS = "user_perms"
|
||||
BLOCK_DOMAIN = "block_domain"
|
||||
APPROVE_DOMAIN = "approve_domain"
|
||||
DELETE_ITEM = "delete_item"
|
||||
|
||||
|
||||
class Report(BookWyrmModel):
|
||||
"""reported status or user"""
|
||||
|
||||
|
@ -33,30 +48,60 @@ class Report(BookWyrmModel):
|
|||
def get_remote_id(self):
|
||||
return f"https://{DOMAIN}/settings/reports/{self.id}"
|
||||
|
||||
def comment(self, user, note):
|
||||
"""comment on a report"""
|
||||
ReportAction.objects.create(
|
||||
action_type=COMMENT, user=user, note=note, report=self
|
||||
)
|
||||
|
||||
def resolve(self, user):
|
||||
"""Mark a report as complete"""
|
||||
self.resolved = True
|
||||
self.save()
|
||||
ReportAction.objects.create(action_type=RESOLVE, user=user, report=self)
|
||||
|
||||
def reopen(self, user):
|
||||
"""Wait! This report isn't complete after all"""
|
||||
self.resolved = False
|
||||
self.save()
|
||||
ReportAction.objects.create(action_type=REOPEN, user=user, report=self)
|
||||
|
||||
@classmethod
|
||||
def record_action(cls, report_id: int, action: str, user):
|
||||
"""Note that someone did something"""
|
||||
if not report_id:
|
||||
return
|
||||
report = cls.objects.get(id=report_id)
|
||||
ReportAction.objects.create(action_type=action, user=user, report=report)
|
||||
|
||||
class Meta:
|
||||
"""set order by default"""
|
||||
|
||||
ordering = ("-created_date",)
|
||||
|
||||
|
||||
ReportCommentTypes = [
|
||||
("comment", _("Comment")),
|
||||
("resolve", _("Resolved report")),
|
||||
("reopen", _("Re-opened report")),
|
||||
("message_reporter", _("Messaged reporter")),
|
||||
("message_offender", _("Messaged reported user")),
|
||||
("user_suspension", _("Suspended user")),
|
||||
("user_deletion", _("Deleted user account")),
|
||||
("block_domain", _("Blocked domain")),
|
||||
("approve_domain", _("Approved domain")),
|
||||
("delete_item", _("Deleted item")),
|
||||
ReportActionTypes = [
|
||||
(COMMENT, _("Comment")),
|
||||
(RESOLVE, _("Resolved report")),
|
||||
(REOPEN, _("Re-opened report")),
|
||||
(MESSAGE_REPORTER, _("Messaged reporter")),
|
||||
(MESSAGE_OFFENDER, _("Messaged reported user")),
|
||||
(USER_SUSPENSION, _("Suspended user")),
|
||||
(USER_UNSUSPENSION, _("Un-suspended user")),
|
||||
(USER_PERMS, _("Changed user permission level")),
|
||||
(USER_DELETION, _("Deleted user account")),
|
||||
(BLOCK_DOMAIN, _("Blocked domain")),
|
||||
(APPROVE_DOMAIN, _("Approved domain")),
|
||||
(DELETE_ITEM, _("Deleted item")),
|
||||
]
|
||||
class ReportComment(BookWyrmModel):
|
||||
|
||||
|
||||
class ReportAction(BookWyrmModel):
|
||||
"""updates on a report"""
|
||||
|
||||
user = models.ForeignKey("User", on_delete=models.PROTECT)
|
||||
comment_type = models.CharField(
|
||||
max_length=20, blank=False, default="comment", choices=ReportCommentTypes
|
||||
action_type = models.CharField(
|
||||
max_length=20, blank=False, default="comment", choices=ReportActionTypes
|
||||
)
|
||||
note = models.TextField()
|
||||
report = models.ForeignKey(Report, on_delete=models.PROTECT)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{% extends 'settings/layout.html' %}
|
||||
{% load i18n %}
|
||||
{% load humanize %}
|
||||
{% load utilities %}
|
||||
{% load feed_page_tags %}
|
||||
|
||||
{% block title %}
|
||||
|
@ -21,7 +22,7 @@
|
|||
<div class="block">
|
||||
<details class="details-panel box">
|
||||
<summary>
|
||||
<span class="title is-4">{% trans "Message reporter" %}</span>
|
||||
<span class="title is-6">{% trans "Message reporter" %}</span>
|
||||
<span class="details-close icon icon-x" aria-hidden="true"></span>
|
||||
</summary>
|
||||
<div class="box">
|
||||
|
@ -61,30 +62,59 @@
|
|||
{% include 'settings/users/user_moderation_actions.html' with user=report.user %}
|
||||
{% endif %}
|
||||
|
||||
<div class="block">
|
||||
<h3 class="title is-4">{% trans "Moderator Comments" %}</h3>
|
||||
{% for comment in report.reportcomment_set.all %}
|
||||
<div class="card block">
|
||||
<p class="card-content">{{ comment.note }}</p>
|
||||
<div class="card-footer">
|
||||
<div class="card-footer-item">
|
||||
<a href="{{ comment.user.local_path }}">{{ comment.user.display_name }}</a>
|
||||
<div class="block content">
|
||||
<h3 class="title is-4">{% trans "Moderation Activity" %}</h3>
|
||||
|
||||
<div class="box">
|
||||
<ul class="mt-0">
|
||||
{% for comment in report.reportaction_set.all %}
|
||||
<li class="mb-2">
|
||||
<div class="is-flex">
|
||||
<p class="mb-0 is-flex-grow-1">
|
||||
{% if comment.action_type == "comment" %}
|
||||
{% blocktrans trimmed with user=comment.user|username user_link=comment.user.local_path %}
|
||||
<a href="{{ user_link }}">{{ user}}</a> commented on this report:
|
||||
{% endblocktrans %}
|
||||
{% else %}
|
||||
{% blocktrans trimmed with user=comment.user|username user_link=comment.user.local_path %}
|
||||
<a href="{{ user_link }}">{{ user}}</a> took an action on this report:
|
||||
{% endblocktrans %}
|
||||
<span class="has-text-weight-bold">
|
||||
{{ comment.get_action_type_display }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
<span class="tag">{{ comment.created_date }}</span>
|
||||
</div>
|
||||
|
||||
{% if comment.note %}
|
||||
<blockquote>{{ comment.note }}</blockquote>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
<li class="mb-2">
|
||||
<div class="is-flex">
|
||||
<p class="mb-0 is-flex-grow-1">
|
||||
{% blocktrans trimmed with user=report.reporter|username user_link=report.reporter.local_path %}
|
||||
<a href="{{ user_link }}">{{ user}}</a> opened this report
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<span class="tag">{{ report.created_date }}</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<form class="block" name="report-comment" method="post" action="{% url 'settings-report' report.id %}">
|
||||
{% csrf_token %}
|
||||
<div class="field">
|
||||
<label for="report_comment" class="label">Comment on report</label>
|
||||
<textarea name="note" id="report_comment" class="textarea"></textarea>
|
||||
</div>
|
||||
<div class="card-footer-item">
|
||||
{{ comment.created_date|naturaltime }}
|
||||
<div class="field">
|
||||
<button class="button">{% trans "Comment" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<form class="block" name="report-comment" method="post" action="{% url 'settings-report' report.id %}">
|
||||
{% csrf_token %}
|
||||
<div class="field">
|
||||
<label for="report_comment" class="label">Comment on report</label>
|
||||
<textarea name="note" id="report_comment" class="textarea"></textarea>
|
||||
</div>
|
||||
<div class="field">
|
||||
<button class="button">{% trans "Comment" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -13,8 +13,18 @@
|
|||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<form>
|
||||
{% if link.domain.status != "approved" %}
|
||||
<form method="POST" action="{% url 'settings-link-domain-status' link.domain.id 'approved' report.id %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="button is-success is-light">{% trans "Approve domain" %}</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
{% if link.domain.status != "blocked" %}
|
||||
<form method="POST" action="{% url 'settings-link-domain-status' link.domain.id 'blocked' report.id %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="button is-danger is-light">{% trans "Block domain" %}</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endblock %}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block form %}
|
||||
<form name="delete-user" action="{% url 'settings-delete-user' user.id %}" method="post">
|
||||
<form name="delete-user" action="{% url 'settings-delete-user' user.id report.id %}" method="post">
|
||||
{% csrf_token %}
|
||||
<p>
|
||||
{% blocktrans trimmed with username=user.localname %}
|
||||
|
|
|
@ -22,12 +22,12 @@
|
|||
</form>
|
||||
{% endif %}
|
||||
{% if user.is_active or user.deactivation_reason == "pending" %}
|
||||
<form name="suspend" method="post" action="{% url 'settings-report-suspend' user.id %}" class="mr-1">
|
||||
<form name="suspend" method="post" action="{% url 'settings-report-suspend' user.id report.id %}" class="mr-1">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="button is-danger is-light">{% trans "Suspend user" %}</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<form name="unsuspend" method="post" action="{% url 'settings-report-unsuspend' user.id %}" class="mr-1">
|
||||
<form name="unsuspend" method="post" action="{% url 'settings-report-unsuspend' user.id report.id %}" class="mr-1">
|
||||
{% csrf_token %}
|
||||
<button class="button">{% trans "Un-suspend user" %}</button>
|
||||
</form>
|
||||
|
@ -49,7 +49,7 @@
|
|||
|
||||
{% if user.local %}
|
||||
<div>
|
||||
<form name="permission" method="post" action="{% url 'settings-user' user.id %}">
|
||||
<form name="permission" method="post" action="{% url 'settings-user' user.id report.id %}">
|
||||
{% csrf_token %}
|
||||
<label class="label" for="id_user_group">{% trans "Access level:" %}</label>
|
||||
{% if group_form.non_field_errors %}
|
||||
|
|
|
@ -141,7 +141,7 @@ urlpatterns = [
|
|||
name="settings-users",
|
||||
),
|
||||
re_path(
|
||||
r"^settings/users/(?P<user>\d+)/?$",
|
||||
r"^settings/users/(?P<user>\d+)/(?P<report_id>\d+)??$",
|
||||
views.UserAdmin.as_view(),
|
||||
name="settings-user",
|
||||
),
|
||||
|
@ -231,7 +231,7 @@ urlpatterns = [
|
|||
name="settings-link-domain",
|
||||
),
|
||||
re_path(
|
||||
r"^setting/link-domains/(?P<domain_id>\d+)/(?P<status>(pending|approved|blocked))/?$",
|
||||
r"^setting/link-domains/(?P<domain_id>\d+)/(?P<status>(pending|approved|blocked))/(?P<report_id>\d+)?$",
|
||||
views.update_domain_status,
|
||||
name="settings-link-domain-status",
|
||||
),
|
||||
|
@ -275,17 +275,17 @@ urlpatterns = [
|
|||
name="settings-report",
|
||||
),
|
||||
re_path(
|
||||
r"^settings/reports/(?P<user_id>\d+)/suspend/?$",
|
||||
r"^settings/reports/(?P<user_id>\d+)/suspend/(?P<report_id>\d+)?$",
|
||||
views.suspend_user,
|
||||
name="settings-report-suspend",
|
||||
),
|
||||
re_path(
|
||||
r"^settings/reports/(?P<user_id>\d+)/unsuspend/?$",
|
||||
r"^settings/reports/(?P<user_id>\d+)/unsuspend/(?P<report_id>\d+)?$",
|
||||
views.unsuspend_user,
|
||||
name="settings-report-unsuspend",
|
||||
),
|
||||
re_path(
|
||||
r"^settings/reports/(?P<user_id>\d+)/delete/?$",
|
||||
r"^settings/reports/(?P<user_id>\d+)/delete/(?P<report_id>\d+)?$",
|
||||
views.moderator_delete_user,
|
||||
name="settings-delete-user",
|
||||
),
|
||||
|
|
|
@ -7,6 +7,8 @@ from django.views import View
|
|||
from django.views.decorators.http import require_POST
|
||||
|
||||
from bookwyrm import forms, models
|
||||
from bookwyrm.models.report import APPROVE_DOMAIN, BLOCK_DOMAIN
|
||||
from bookwyrm.views.helpers import redirect_to_referer
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
|
@ -46,11 +48,17 @@ class LinkDomain(View):
|
|||
@require_POST
|
||||
@login_required
|
||||
@permission_required("bookwyrm.moderate_user")
|
||||
def update_domain_status(request, domain_id, status):
|
||||
def update_domain_status(request, domain_id, status, report_id=None):
|
||||
"""This domain seems fine"""
|
||||
domain = get_object_or_404(models.LinkDomain, id=domain_id)
|
||||
domain.raise_not_editable(request.user)
|
||||
|
||||
domain.status = status
|
||||
domain.save()
|
||||
return redirect("settings-link-domain", status="pending")
|
||||
|
||||
if status == "approved":
|
||||
models.Report.record_action(report_id, APPROVE_DOMAIN, request.user)
|
||||
elif status == "blocked":
|
||||
models.Report.record_action(report_id, BLOCK_DOMAIN, request.user)
|
||||
|
||||
return redirect_to_referer(request, "settings-link-domain", status="pending")
|
||||
|
|
|
@ -8,6 +8,7 @@ from django.utils.decorators import method_decorator
|
|||
from django.views import View
|
||||
|
||||
from bookwyrm import forms, models
|
||||
from bookwyrm.models.report import USER_SUSPENSION, USER_UNSUSPENSION, USER_DELETION
|
||||
from bookwyrm.views.helpers import redirect_to_referer
|
||||
from bookwyrm.settings import PAGE_LENGTH
|
||||
|
||||
|
@ -81,41 +82,42 @@ class ReportAdmin(View):
|
|||
def post(self, request, report_id):
|
||||
"""comment on a report"""
|
||||
report = get_object_or_404(models.Report, id=report_id)
|
||||
models.ReportComment.objects.create(
|
||||
user=request.user,
|
||||
report=report,
|
||||
note=request.POST.get("note"),
|
||||
)
|
||||
note = request.POST.get("note")
|
||||
report.comment(request.user, note)
|
||||
return redirect("settings-report", report.id)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required("bookwyrm.moderate_user")
|
||||
def suspend_user(request, user_id):
|
||||
def suspend_user(request, user_id, report_id=None):
|
||||
"""mark an account as inactive"""
|
||||
user = get_object_or_404(models.User, id=user_id)
|
||||
user.is_active = False
|
||||
user.deactivation_reason = "moderator_suspension"
|
||||
# this isn't a full deletion, so we don't want to tell the world
|
||||
user.save(broadcast=False)
|
||||
|
||||
models.ReportAction.record_action(report_id, USER_SUSPENSION, request.user)
|
||||
return redirect_to_referer(request, "settings-user", user.id)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required("bookwyrm.moderate_user")
|
||||
def unsuspend_user(request, user_id):
|
||||
def unsuspend_user(request, user_id, report_id=None):
|
||||
"""mark an account as inactive"""
|
||||
user = get_object_or_404(models.User, id=user_id)
|
||||
user.is_active = True
|
||||
user.deactivation_reason = None
|
||||
# this isn't a full deletion, so we don't want to tell the world
|
||||
user.save(broadcast=False)
|
||||
|
||||
models.ReportAction.record_action(report_id, USER_UNSUSPENSION, request.user)
|
||||
return redirect_to_referer(request, "settings-user", user.id)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required("bookwyrm.moderate_user")
|
||||
def moderator_delete_user(request, user_id):
|
||||
def moderator_delete_user(request, user_id, report_id=None):
|
||||
"""permanently delete a user"""
|
||||
user = get_object_or_404(models.User, id=user_id)
|
||||
|
||||
|
@ -130,6 +132,9 @@ def moderator_delete_user(request, user_id):
|
|||
if form.is_valid() and moderator.check_password(form.cleaned_data["password"]):
|
||||
user.deactivation_reason = "moderator_deletion"
|
||||
user.delete()
|
||||
|
||||
# make a note of the fact that we did this
|
||||
models.ReportAction.record_action(report_id, USER_DELETION, request.user)
|
||||
return redirect_to_referer(request, "settings-user", user.id)
|
||||
|
||||
form.errors["password"] = ["Invalid password"]
|
||||
|
@ -140,11 +145,12 @@ def moderator_delete_user(request, user_id):
|
|||
|
||||
@login_required
|
||||
@permission_required("bookwyrm.moderate_post")
|
||||
def resolve_report(_, report_id):
|
||||
def resolve_report(request, report_id):
|
||||
"""mark a report as (un)resolved"""
|
||||
report = get_object_or_404(models.Report, id=report_id)
|
||||
report.resolved = not report.resolved
|
||||
report.save()
|
||||
if not report.resolved:
|
||||
if report.resolved:
|
||||
report.reopen(request.user)
|
||||
return redirect("settings-report", report.id)
|
||||
|
||||
report.resolve(request.user)
|
||||
return redirect("settings-reports")
|
||||
|
|
|
@ -222,7 +222,7 @@ def maybe_redirect_local_path(request, model):
|
|||
return redirect(new_path, permanent=True)
|
||||
|
||||
|
||||
def redirect_to_referer(request, *args):
|
||||
def redirect_to_referer(request, *args, **kwargs):
|
||||
"""Redirect to the referrer, if it's in our domain, with get params"""
|
||||
# make sure the refer is part of this instance
|
||||
validated = validate_url_domain(request.META.get("HTTP_REFERER"))
|
||||
|
@ -231,4 +231,4 @@ def redirect_to_referer(request, *args):
|
|||
return redirect(validated)
|
||||
|
||||
# if not, use the args passed you'd normally pass to redirect()
|
||||
return redirect(*args or "/")
|
||||
return redirect(*args or "/", **kwargs)
|
||||
|
|
Loading…
Reference in a new issue