Merge pull request #715 from mouse-reeve/reporting

Reporting
This commit is contained in:
Mouse Reeve 2021-03-12 16:28:15 -08:00 committed by GitHub
commit b64fb3e0aa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 679 additions and 21 deletions

3
.gitignore vendored
View file

@ -2,6 +2,7 @@
/venv
*.pyc
*.swp
**/__pycache__
# VSCode
/.vscode
@ -15,4 +16,4 @@
/images/
# Testing
.coverage
.coverage

View file

@ -231,3 +231,9 @@ class ListForm(CustomForm):
class Meta:
model = models.List
fields = ["user", "name", "description", "curation", "privacy"]
class ReportForm(CustomForm):
class Meta:
model = models.Report
fields = ["user", "reporter", "statuses", "note"]

View file

@ -0,0 +1,113 @@
# Generated by Django 3.0.7 on 2021-03-09 01:56
import bookwyrm.models.fields
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.db.models.expressions
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0048_merge_20210308_1754"),
]
operations = [
migrations.CreateModel(
name="Report",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created_date", models.DateTimeField(auto_now_add=True)),
("updated_date", models.DateTimeField(auto_now=True)),
(
"remote_id",
bookwyrm.models.fields.RemoteIdField(
max_length=255,
null=True,
validators=[bookwyrm.models.fields.validate_remote_id],
),
),
("note", models.TextField(blank=True, null=True)),
("resolved", models.BooleanField(default=False)),
(
"reporter",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="reporter",
to=settings.AUTH_USER_MODEL,
),
),
(
"statuses",
models.ManyToManyField(blank=True, null=True, to="bookwyrm.Status"),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
to=settings.AUTH_USER_MODEL,
),
),
],
),
migrations.CreateModel(
name="ReportComment",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created_date", models.DateTimeField(auto_now_add=True)),
("updated_date", models.DateTimeField(auto_now=True)),
(
"remote_id",
bookwyrm.models.fields.RemoteIdField(
max_length=255,
null=True,
validators=[bookwyrm.models.fields.validate_remote_id],
),
),
("note", models.TextField()),
(
"report",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
to="bookwyrm.Report",
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"abstract": False,
},
),
migrations.AddConstraint(
model_name="report",
constraint=models.CheckConstraint(
check=models.Q(
_negated=True, reporter=django.db.models.expressions.F("user")
),
name="self_report",
),
),
]

View file

@ -21,6 +21,7 @@ from .tag import Tag, UserTag
from .user import User, KeyPair, AnnualGoal
from .relationship import UserFollows, UserFollowRequest, UserBlocks
from .report import Report, ReportComment
from .federated_server import FederatedServer
from .import_job import ImportJob, ImportItem

View file

@ -4,7 +4,7 @@ from .base_model import BookWyrmModel
class FederatedServer(BookWyrmModel):
""" store which server's we federate with """
""" store which servers we federate with """
server_name = models.CharField(max_length=255, unique=True)
# federated, blocked, whatever else

37
bookwyrm/models/report.py Normal file
View file

@ -0,0 +1,37 @@
""" flagged for moderation """
from django.db import models
from django.db.models import F, Q
from .base_model import BookWyrmModel
class Report(BookWyrmModel):
""" reported status or user """
reporter = models.ForeignKey(
"User", related_name="reporter", on_delete=models.PROTECT
)
note = models.TextField(null=True, blank=True)
user = models.ForeignKey("User", on_delete=models.PROTECT)
statuses = models.ManyToManyField("Status", null=True, blank=True)
resolved = models.BooleanField(default=False)
class Meta:
""" don't let users report themselves """
constraints = [
models.CheckConstraint(check=~Q(reporter=F("user")), name="self_report")
]
ordering = ("-created_date",)
class ReportComment(BookWyrmModel):
""" updates on a report """
user = models.ForeignKey("User", on_delete=models.PROTECT)
note = models.TextField()
report = models.ForeignKey(Report, on_delete=models.PROTECT)
class Meta:
""" sort comments """
ordering = ("-created_date",)

View file

@ -114,8 +114,8 @@
{% endif %}
{% if perms.bookwyrm.edit_instance_settings %}
<li>
<a href="{% url 'settings-site' %}" class="navbar-item">
{% trans 'Site Configuration' %}
<a href="{% url 'settings-reports' %}" class="navbar-item">
{% trans 'Admin' %}
</a>
</li>
{% endif %}

View file

@ -0,0 +1,76 @@
{% extends 'settings/admin_layout.html' %}
{% load i18n %}
{% load humanize %}
{% block title %}{% blocktrans with report_id=report.id username=report.user.username %}Report #{{ report_id }}: {{ username }}{% endblocktrans %}{% endblock %}
{% block header %}{% blocktrans with report_id=report.id username=report.user.username %}Report #{{ report_id }}: {{ username }}{% endblocktrans %}{% endblock %}
{% block panel %}
<div class="block">
<a href="{% url 'settings-reports' %}">{% trans "Back to reports" %}</a>
</div>
<div class="block">
{% include 'moderation/report_preview.html' with report=report %}
</div>
<div class="block content">
<h3>{% trans "Actions" %}</h3>
<p><a href="{{ report.user.local_path }}">{% trans "View user profile" %}</a></p>
<div class="is-flex">
<p class="mr-1">
<a class="button" href="{% url 'direct-messages-user' report.user.username %}">{% trans "Send direct message" %}</a>
</p>
<form name="deactivate" method="post" action="{% url 'settings-report-deactivate' report.id %}">
{% csrf_token %}
{% if report.user.is_active %}
<button type="submit" class="button is-danger is-light">{% trans "Deactivate user" %}</button>
{% else %}
<button class="button">{% trans "Reactivate user" %}</button>
{% endif %}
</form>
</div>
</div>
<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>
<div class="card-footer-item">
{{ comment.created_date | naturaltime }}
</div>
</div>
</div>
{% endfor %}
<form class="block" name="report-comment" method="post" action="{% url 'settings-report' report.id %}">
{% csrf_token %}
<label for="report_comment" class="label">Comment on report</label>
<textarea name="note" id="report_comment" class="textarea"></textarea>
<button class="button">{% trans "Comment" %}</button>
</form>
</div>
<div class="block">
<h3 class="title is-4">{% trans "Reported statuses" %}</h3>
{% if not report.statuses.exists %}
<em>{% trans "No statuses reported" %}</em>
{% else %}
<ul>
{% for status in report.statuses.select_subclasses.all %}
<li>
{% if status.deleted %}
<em>{% trans "Statuses has been deleted" %}</em>
{% else %}
{% include 'snippets/status/status.html' with status=status moderation_mode=True %}
{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% endblock %}

View file

@ -0,0 +1,37 @@
{% extends 'components/modal.html' %}
{% load i18n %}
{% load humanize %}
{% block modal-title %}
{% blocktrans with username=user.username %}Report @{{ username }}{% endblocktrans %}
{% endblock %}
{% block modal-form-open %}
<form name="report" method="post" action="{% url 'report' %}">
{% endblock %}
{% block modal-body %}
{% csrf_token %}
<input type="hidden" name="reporter" value="{{ reporter.id }}">
<input type="hidden" name="user" value="{{ user.id }}">
<input type="hidden" name="statuses" value="{{ status.id }}">
<section class="content">
<p>{% blocktrans with site_name=site.name %}This report will be sent to {{ site_name }}'s moderators for review.{% endblocktrans %}</p>
<label class="label" for="id_{{ controls_uid }}_report_note">{% trans "More info about this report:" %}</label>
<textarea class="textarea" name="note" id="id_{{ controls_uid }}_report_note"></textarea>
</section>
{% endblock %}
{% block modal-footer %}
<button class="button is-success" type="submit">{% trans "Submit" %}</button>
{% trans "Cancel" as button_text %}
{% include 'snippets/toggle/toggle_button.html' with text=button_text controls_text="report" controls_uid=report_uuid class="" %}
{% endblock %}
{% block modal-form-close %}</form>{% endblock %}

View file

@ -0,0 +1,37 @@
{% extends 'components/card.html' %}
{% load i18n %}
{% load humanize %}
{% block card-header %}
<h2 class="card-header-title has-background-white-ter is-block">
<a href="{% url 'settings-report' report.id %}">{% blocktrans with report_id=report.id username=report.user.username %}Report #{{ report_id }}: {{ username }}{% endblocktrans %}</a>
</h2>
{% endblock %}
{% block card-content %}
<div class="block content">
<p>
{% if report.note %}{{ report.note }}{% else %}<em>{% trans "No notes provided" %}</em>{% endif %}
</p>
</div>
{% endblock %}
{% block card-footer %}
<div class="card-footer-item">
<p>{% blocktrans with username=report.reporter.display_name path=report.reporter.local_path %}Reported by <a href="{{ path }}">{{ username }}</a>{% endblocktrans %}</p>
</div>
<div class="card-footer-item">
{{ report.created_date | naturaltime }}
</div>
<div class="card-footer-item">
<form name="resolve" method="post" action="{% url 'settings-report-resolve' report.id %}">
{% csrf_token %}
<button class="button" type="submit">
{% if report.resolved %}
{% trans "Re-open" %}
{% else %}
{% trans "Resolve" %}
{% endif %}
</button>
</form>
</div>
{% endblock %}

View file

@ -0,0 +1,28 @@
{% extends 'settings/admin_layout.html' %}
{% load i18n %}
{% block title %}{% trans "Reports" %}{% endblock %}
{% block header %}{% trans "Reports" %}{% endblock %}
{% block panel %}
<div class="tabs">
<ul>
<li class="{% if not resolved %}is-active{% endif %}"{% if not resolved == 'open' %} aria-current="page"{% endif %}>
<a href="{% url 'settings-reports' %}?resolved=false">{% trans "Open" %}</a>
</li>
<li class="{% if resolved %}is-active{% endif %}"{% if resolved %} aria-current="page"{% endif %}>
<a href="{% url 'settings-reports' %}?resolved=true">{% trans "Resolved" %}</a>
</li>
</ul>
</div>
<div class="block">
{% for report in reports %}
<div class="block">
{% include 'moderation/report_preview.html' with report=report %}
</div>
{% endfor %}
</div>
{% endblock %}

View file

@ -115,15 +115,7 @@
<div class="notification py-2 {% if notification.id in unread %}is-primary is-light{% else %}has-background-white{% if notification.notification_type == 'REPLY' or notification.notification_type == 'MENTION' %} has-text-black{% else %}-bis has-text-grey-dark{% endif %}{% endif %}">
<div class="columns">
<div class="column">
{% if related_status.content %}
<a href="{{ related_status.local_path }}">
{{ related_status.content | safe | truncatewords_html:10 }}{% if related_status.mention_books %} <em>{{ related_status.mention_books.first.title }}</em>{% endif %}
</a>
{% elif related_status.quote %}
<a href="{{ related_status.local_path }}">{{ related_status.quote | safe | truncatewords_html:10 }}</a>
{% elif related_status.rating %}
{% include 'snippets/stars.html' with rating=related_status.rating %}
{% endif %}
{% include 'snippets/status_preview.html' with status=related_status %}
</div>
<div class="column is-narrow {% if notification.notification_type == 'REPLY' or notification.notification_type == 'MENTION' %}has-text-black{% else %}has-text-grey-dark{% endif %}">
{{ related_status.published_date | post_date }}

View file

@ -18,6 +18,10 @@
{% url 'settings-invites' as url %}
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Invites" %}</a>
</li>
<li>
{% url 'settings-reports' as url %}
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Reports" %}</a>
</li>
<li>
{% url 'settings-federation' as url %}
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Federated Servers" %}</a>
@ -42,7 +46,7 @@
</ul>
{% endif %}
</nav>
<div class="column content">
<div class="column">
{% block panel %}{% endblock %}
</div>
</div>

View file

@ -0,0 +1,10 @@
{% load i18n %}
{% load bookwyrm_tags %}
{% with 0|uuid as report_uuid %}
{% trans "Report" as button_text %}
{% include 'snippets/toggle/toggle_button.html' with class="is-danger is-light is-small is-fullwidth" text=button_text controls_text="report" controls_uid=report_uuid focus="modal-title-report" disabled=is_current %}
{% include 'moderation/report_modal.html' with user=user reporter=request.user controls_text="report" controls_uid=report_uuid %}
{% endwith %}

View file

@ -18,7 +18,17 @@
{% block card-footer %}
<div class="card-footer-item">
{% if request.user.is_authenticated %}
{% if moderation_mode and perms.bookwyrm.moderate_post %}
{# moderation options #}
<form class="dropdown-item pt-0 pb-0" name="delete-{{status.id}}" action="/delete-status/{{ status.id }}" method="post">
{% csrf_token %}
<button class="button is-danger is-light" type="submit">
{% trans "Delete status" %}
</button>
</form>
{% elif request.user.is_authenticated %}
<div class="field has-addons">
<div class="control">
{% trans "Reply" as button_text %}
@ -56,14 +66,16 @@
<div class="card-footer-item">
<a href="{{ status.remote_id }}">{{ status.published_date | post_date }}</a>
</div>
{% if not moderation_mode %}
<div class="card-footer-item">
{% include 'snippets/status/status_options.html' with class="is-small" right=True %}
</div>
{% endif %}
{% endblock %}
{% block card-bonus %}
{% if request.user.is_authenticated %}
{% if request.user.is_authenticated and not moderation_mode %}
{% with status.id|uuid as uuid %}
<section class="hidden" id="show-comment-{{ status.id }}">
<div class="card-footer">

View file

@ -10,6 +10,7 @@
{% block dropdown-list %}
{% if status.user == request.user %}
{# things you can do to your own statuses #}
<li role="menuitem">
<form class="dropdown-item pt-0 pb-0" name="delete-{{status.id}}" action="/delete-status/{{ status.id }}" method="post">
{% csrf_token %}
@ -19,8 +20,12 @@
</form>
</li>
{% else %}
{# things you can do to other people's statuses #}
<li role="menuitem">
<a href="/direct-messages/{{ status.user|username }}" class="button is-fullwidth is-small">{% trans "Send direct message" %}</a>
<a href="{% url 'direct-messages-user' status.user|username %}" class="button is-small is-fullwidth">{% trans "Send direct message" %}</a>
</li>
<li role="menuitem">
{% include 'snippets/report_button.html' with user=status.user status=status %}
</li>
<li role="menuitem">
{% include 'snippets/block_button.html' with user=status.user class="is-fullwidth" %}

View file

@ -0,0 +1,9 @@
{% if status.content %}
<a href="{{ status.local_path }}">
{{ status.content | safe | truncatewords_html:10 }}{% if status.mention_books %} <em>{{ status.mention_books.first.title }}</em>{% endif %}
</a>
{% elif status.quote %}
<a href="{{ status.local_path }}">{{ status.quote | safe | truncatewords_html:10 }}</a>
{% elif status.rating %}
{% include 'snippets/stars.html' with rating=status.rating %}
{% endif %}

View file

@ -12,6 +12,9 @@
<li role="menuitem">
<a href="/direct-messages/{{ user|username }}" class="button is-fullwidth is-small">{% trans "Send direct message" %}</a>
</li>
<li role="menuitem">
{% include 'snippets/report_button.html' with user=status.user class="is-fullwidth" %}
</li>
<li role="menuitem">
{% include 'snippets/block_button.html' with user=user class="is-fullwidth" %}
</li>

View file

@ -0,0 +1,136 @@
""" test for app action functionality """
from unittest.mock import patch
from django.template.response import TemplateResponse
from django.test import TestCase
from django.test.client import RequestFactory
from bookwyrm import forms, models, views
class ReportViews(TestCase):
""" every response to a get request, html or json """
def setUp(self):
""" we need basic test data and mocks """
self.factory = RequestFactory()
self.local_user = models.User.objects.create_user(
"mouse@local.com",
"mouse@mouse.mouse",
"password",
local=True,
localname="mouse",
)
self.rat = models.User.objects.create_user(
"rat@local.com",
"rat@mouse.mouse",
"password",
local=True,
localname="rat",
)
models.SiteSettings.objects.create()
def test_reports_page(self):
""" there are so many views, this just makes sure it LOADS """
view = views.Reports.as_view()
request = self.factory.get("")
request.user = self.local_user
request.user.is_superuser = True
result = view(request)
self.assertIsInstance(result, TemplateResponse)
result.render()
self.assertEqual(result.status_code, 200)
def test_reports_page_with_data(self):
""" there are so many views, this just makes sure it LOADS """
view = views.Reports.as_view()
request = self.factory.get("")
request.user = self.local_user
request.user.is_superuser = True
models.Report.objects.create(reporter=self.local_user, user=self.rat)
result = view(request)
self.assertIsInstance(result, TemplateResponse)
result.render()
self.assertEqual(result.status_code, 200)
def test_report_page(self):
""" there are so many views, this just makes sure it LOADS """
view = views.Report.as_view()
request = self.factory.get("")
request.user = self.local_user
request.user.is_superuser = True
report = models.Report.objects.create(reporter=self.local_user, user=self.rat)
result = view(request, report.id)
self.assertIsInstance(result, TemplateResponse)
result.render()
self.assertEqual(result.status_code, 200)
def test_report_comment(self):
""" comment on a report """
view = views.Report.as_view()
request = self.factory.post("", {"note": "hi"})
request.user = self.local_user
request.user.is_superuser = True
report = models.Report.objects.create(reporter=self.local_user, user=self.rat)
view(request, report.id)
comment = models.ReportComment.objects.get()
self.assertEqual(comment.user, self.local_user)
self.assertEqual(comment.note, "hi")
self.assertEqual(comment.report, report)
def test_make_report(self):
""" a user reports another user """
form = forms.ReportForm()
form.data["reporter"] = self.local_user.id
form.data["user"] = self.rat.id
request = self.factory.post("", form.data)
request.user = self.local_user
views.make_report(request)
report = models.Report.objects.get()
self.assertEqual(report.reporter, self.local_user)
self.assertEqual(report.user, self.rat)
def test_resolve_report(self):
""" toggle report resolution status """
report = models.Report.objects.create(reporter=self.local_user, user=self.rat)
self.assertFalse(report.resolved)
request = self.factory.post("")
request.user = self.local_user
request.user.is_superuser = True
# resolve
views.resolve_report(request, report.id)
report.refresh_from_db()
self.assertTrue(report.resolved)
# un-resolve
views.resolve_report(request, report.id)
report.refresh_from_db()
self.assertFalse(report.resolved)
def test_deactivate_user(self):
""" toggle whether a user is able to log in """
self.assertTrue(self.rat.is_active)
report = models.Report.objects.create(reporter=self.local_user, user=self.rat)
request = self.factory.post("")
request.user = self.local_user
request.user.is_superuser = True
# de-activate
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
views.deactivate_user(request, report.id)
self.rat.refresh_from_db()
self.assertFalse(self.rat.is_active)
# re-activate
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
views.deactivate_user(request, report.id)
self.rat.refresh_from_db()
self.assertTrue(self.rat.is_active)

View file

@ -216,7 +216,7 @@ class StatusViews(TestCase):
'<a href="%s">'
"archive.org/details/dli.granth.72113/page/n25/mode/2up</a>" % url,
)
url = "https://openlibrary.org/search" "?q=arkady+strugatsky&mode=everything"
url = "https://openlibrary.org/search?q=arkady+strugatsky&mode=everything"
self.assertEqual(
views.status.format_links(url),
'<a href="%s">openlibrary.org/search'
@ -253,3 +253,35 @@ class StatusViews(TestCase):
self.assertEqual(activity["object"]["type"], "Tombstone")
status.refresh_from_db()
self.assertTrue(status.deleted)
def test_handle_delete_status_permission_denied(self):
""" marks a status as deleted """
view = views.DeleteStatus.as_view()
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
status = models.Status.objects.create(user=self.local_user, content="hi")
self.assertFalse(status.deleted)
request = self.factory.post("")
request.user = self.remote_user
view(request, status.id)
status.refresh_from_db()
self.assertFalse(status.deleted)
def test_handle_delete_status_moderator(self):
""" marks a status as deleted """
view = views.DeleteStatus.as_view()
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
status = models.Status.objects.create(user=self.local_user, content="hi")
self.assertFalse(status.deleted)
request = self.factory.post("")
request.user = self.remote_user
request.user.is_superuser = True
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock:
view(request, status.id)
activity = json.loads(mock.call_args_list[0][0][1])
self.assertEqual(activity["type"], "Delete")
self.assertEqual(activity["object"]["type"], "Tombstone")
status.refresh_from_db()
self.assertTrue(status.deleted)

View file

@ -55,6 +55,24 @@ urlpatterns = [
r"^settings/invites/?$", views.ManageInvites.as_view(), name="settings-invites"
),
re_path(r"^invite/(?P<code>[A-Za-z0-9]+)/?$", views.Invite.as_view()),
# moderation
re_path(r"^settings/reports/?$", views.Reports.as_view(), name="settings-reports"),
re_path(
r"^settings/reports/(?P<report_id>\d+)/?$",
views.Report.as_view(),
name="settings-report",
),
re_path(
r"^settings/reports/(?P<report_id>\d+)/deactivate/?$",
views.deactivate_user,
name="settings-report-deactivate",
),
re_path(
r"^settings/reports/(?P<report_id>\d+)/resolve/?$",
views.resolve_report,
name="settings-report-resolve",
),
re_path(r"^report/?$", views.make_report, name="report"),
# landing pages
re_path(r"^about/?$", views.About.as_view()),
path("", views.Home.as_view()),
@ -62,10 +80,13 @@ urlpatterns = [
re_path(r"^notifications/?$", views.Notifications.as_view()),
# feeds
re_path(r"^(?P<tab>home|local|federated)/?$", views.Feed.as_view()),
re_path(r"^direct-messages/?$", views.DirectMessage.as_view()),
re_path(
r"^direct-messages/?$", views.DirectMessage.as_view(), name="direct-messages"
),
re_path(
r"^direct-messages/(?P<username>%s)?$" % regex.username,
views.DirectMessage.as_view(),
name="direct-messages-user",
),
# search
re_path(r"^search/?$", views.Search.as_view()),

View file

@ -20,15 +20,16 @@ from .notifications import Notifications
from .outbox import Outbox
from .reading import edit_readthrough, create_readthrough, delete_readthrough
from .reading import start_reading, finish_reading, delete_progressupdate
from .reports import Report, Reports, make_report, resolve_report, deactivate_user
from .rss_feed import RssFeed
from .password import PasswordResetRequest, PasswordReset, ChangePassword
from .tag import Tag, AddTag, RemoveTag
from .search import Search
from .shelf import Shelf
from .shelf import user_shelves_page, create_shelf, delete_shelf
from .shelf import shelve, unshelve
from .site import Site
from .status import CreateStatus, DeleteStatus
from .tag import Tag, AddTag, RemoveTag
from .updates import Updates
from .user import User, EditUser, Followers, Following
from .isbn import Isbn

97
bookwyrm/views/reports.py Normal file
View file

@ -0,0 +1,97 @@
""" moderation via flagged posts and users """
from django.contrib.auth.decorators import login_required, permission_required
from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.http import require_POST
from bookwyrm import forms, models
# pylint: disable=no-self-use
@method_decorator(login_required, name="dispatch")
@method_decorator(
permission_required("bookwyrm.moderate_user", raise_exception=True),
name="dispatch",
)
@method_decorator(
permission_required("bookwyrm.moderate_post", raise_exception=True),
name="dispatch",
)
class Reports(View):
""" list of reports """
def get(self, request):
""" view current reports """
resolved = request.GET.get("resolved") == "true"
data = {
"resolved": resolved,
"reports": models.Report.objects.filter(resolved=resolved),
}
return TemplateResponse(request, "moderation/reports.html", data)
@method_decorator(login_required, name="dispatch")
@method_decorator(
permission_required("bookwyrm.moderate_user", raise_exception=True),
name="dispatch",
)
@method_decorator(
permission_required("bookwyrm.moderate_post", raise_exception=True),
name="dispatch",
)
class Report(View):
""" view a specific report """
def get(self, request, report_id):
""" load a report """
data = {
"report": get_object_or_404(models.Report, id=report_id),
}
return TemplateResponse(request, "moderation/report.html", data)
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"),
)
return redirect("settings-report", report.id)
@login_required
@permission_required("bookwyrm_moderate_user")
def deactivate_user(_, report_id):
""" mark an account as inactive """
report = get_object_or_404(models.Report, id=report_id)
report.user.is_active = not report.user.is_active
report.user.save()
return redirect("settings-report", report.id)
@login_required
@permission_required("bookwyrm_moderate_post")
def resolve_report(_, 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:
return redirect("settings-report", report.id)
return redirect("settings-reports")
@login_required
@require_POST
def make_report(request):
""" a user reports something """
form = forms.ReportForm(request.POST)
if not form.is_valid():
print(form.errors)
return redirect(request.headers.get("Referer", "/"))
form.save()
return redirect(request.headers.get("Referer", "/"))

View file

@ -75,7 +75,7 @@ class DeleteStatus(View):
status = get_object_or_404(models.Status, id=status_id)
# don't let people delete other people's statuses
if status.user != request.user:
if status.user != request.user and not request.user.has_perm("moderate_post"):
return HttpResponseBadRequest()
# perform deletion