summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAndrew Godwin2022-12-17 14:45:31 -0700
committerAndrew Godwin2022-12-17 14:45:31 -0700
commite8d6dccbb27a8611311b5f94f593b69bcca99528 (patch)
tree440901d8495f29e682d5fd08bb3ee4e44e983505
parentb3b2c6effdd747db9076b3139963965f4718eee9 (diff)
downloadtakahe-e8d6dccbb27a8611311b5f94f593b69bcca99528.tar.gz
takahe-e8d6dccbb27a8611311b5f94f593b69bcca99528.tar.bz2
takahe-e8d6dccbb27a8611311b5f94f593b69bcca99528.zip
Report function and admin
-rw-r--r--activities/models/post.py1
-rw-r--r--static/css/style.css18
-rw-r--r--takahe/urls.py14
-rw-r--r--templates/activities/_post.html3
-rw-r--r--templates/admin/report_view.html84
-rw-r--r--templates/admin/reports.html43
-rw-r--r--templates/settings/_menu.html4
-rw-r--r--templates/users/report.html27
-rw-r--r--templates/users/report_sent.html13
-rw-r--r--users/admin.py6
-rw-r--r--users/migrations/0005_report.py119
-rw-r--r--users/models/__init__.py1
-rw-r--r--users/models/report.py129
-rw-r--r--users/views/admin/__init__.py1
-rw-r--r--users/views/admin/reports.py80
-rw-r--r--users/views/report.py76
16 files changed, 615 insertions, 4 deletions
diff --git a/activities/models/post.py b/activities/models/post.py
index d6914c9..553118a 100644
--- a/activities/models/post.py
+++ b/activities/models/post.py
@@ -251,6 +251,7 @@ class Post(StatorModel):
action_unboost = "{view}unboost/"
action_delete = "{view}delete/"
action_edit = "{view}edit/"
+ action_report = "{view}report/"
action_reply = "/compose/?reply_to={self.id}"
admin_edit = "/djadmin/activities/post/{self.id}/change/"
diff --git a/static/css/style.css b/static/css/style.css
index f525857..68efbdc 100644
--- a/static/css/style.css
+++ b/static/css/style.css
@@ -709,7 +709,9 @@ button,
}
button.delete,
-.button.delete {
+.button.delete,
+button.danger,
+.button.danger {
background: var(--color-delete);
}
@@ -833,6 +835,20 @@ table.metadata th {
font-weight: bold;
}
+table.buttons {
+ margin: -10px 0 10px 0;
+ text-align: left;
+}
+
+table.buttons th {
+ padding: 5px 20px 5px 0;
+ text-align: center;
+}
+
+table.buttons th button {
+ width: 100%;
+}
+
.stats {
margin: 0 0 20px 0;
}
diff --git a/takahe/urls.py b/takahe/urls.py
index 6ae0c88..d3572a9 100644
--- a/takahe/urls.py
+++ b/takahe/urls.py
@@ -7,7 +7,7 @@ from api.views import api_router, oauth
from core import views as core
from mediaproxy import views as mediaproxy
from stator import views as stator
-from users.views import activitypub, admin, auth, identity, settings
+from users.views import activitypub, admin, auth, identity, report, settings
urlpatterns = [
path("", core.homepage),
@@ -115,6 +115,16 @@ urlpatterns = [
name="admin_identity_edit",
),
path(
+ "admin/reports/",
+ admin.ReportsRoot.as_view(),
+ name="admin_reports",
+ ),
+ path(
+ "admin/reports/<id>/",
+ admin.ReportView.as_view(),
+ name="admin_report_view",
+ ),
+ path(
"admin/invites/",
admin.Invites.as_view(),
name="admin_invites",
@@ -147,6 +157,7 @@ urlpatterns = [
path("@<handle>/inbox/", activitypub.Inbox.as_view()),
path("@<handle>/action/", identity.ActionIdentity.as_view()),
path("@<handle>/rss/", identity.IdentityFeed()),
+ path("@<handle>/report/", report.SubmitReport.as_view()),
# Posts
path("compose/", compose.Compose.as_view(), name="compose"),
path(
@@ -160,6 +171,7 @@ urlpatterns = [
path("@<handle>/posts/<int:post_id>/boost/", posts.Boost.as_view()),
path("@<handle>/posts/<int:post_id>/unboost/", posts.Boost.as_view(undo=True)),
path("@<handle>/posts/<int:post_id>/delete/", posts.Delete.as_view()),
+ path("@<handle>/posts/<int:post_id>/report/", report.SubmitReport.as_view()),
path("@<handle>/posts/<int:post_id>/edit/", compose.Compose.as_view()),
# Authentication
path("auth/login/", auth.Login.as_view(), name="login"),
diff --git a/templates/activities/_post.html b/templates/activities/_post.html
index 5d75b78..990e457 100644
--- a/templates/activities/_post.html
+++ b/templates/activities/_post.html
@@ -37,6 +37,9 @@
<a href="{{ post.urls.view }}" role="menuitem">
<i class="fa-solid fa-comment"></i> View Post &amp; Replies
</a>
+ <a href="{{ post.urls.action_report }}" role="menuitem">
+ <i class="fa-solid fa-flag"></i> Report
+ </a>
{% if post.author == request.identity %}
<a href="{{ post.urls.action_edit }}" role="menuitem">
<i class="fa-solid fa-pen-to-square"></i> Edit
diff --git a/templates/admin/report_view.html b/templates/admin/report_view.html
new file mode 100644
index 0000000..c9819b4
--- /dev/null
+++ b/templates/admin/report_view.html
@@ -0,0 +1,84 @@
+{% extends "settings/base.html" %}
+
+{% block subtitle %}Report {{ report.pk }}{% endblock %}
+
+{% block content %}
+ <form action="." method="POST">
+ {% csrf_token %}
+ <fieldset>
+ <legend>Report</legend>
+ <label>Report about</label>
+ {% if report.subject_post %}
+ {% include "activities/_mini_post.html" with post=report.subject_post %}
+ {% else %}
+ {% include "activities/_identity.html" with identity=report.subject_identity %}
+ {% endif %}
+ <label>Reported by</label>
+ {% if report.source_identity %}
+ {% include "activities/_identity.html" with identity=report.source_identity %}
+ {% else %}
+ <p>Remote server {{ report.source_domain.domain }}</p>
+ {% endif %}
+ <label>Complaint</label>
+ <p>{{ report.complaint|linebreaks }}</p>
+ {% if report.resolved %}
+ <label>Resolved</label>
+ <p>
+ {{ report.resolved|timesince }} ago by
+ <a href="{{ report.moderator.urls.view }}">{{ report.moderator.name_or_handle }}</a>
+ </p>
+ {% endif %}
+ </fieldset>
+ <fieldset>
+ <legend>Moderator Notes</legend>
+ {% include "forms/_field.html" with field=form.notes %}
+ </fieldset>
+ <fieldset>
+ <legend>Resolution Options</legend>
+ <table class="buttons">
+ <tr>
+ {% if report.resolved and report.valid %}
+ <th><button disabled="true">Resolve Valid</button></th>
+ <td>Report is already resolved as valid</td>
+ {% else %}
+ <th><button name="valid">Resolve Valid</button></th>
+ <td>Mark report against the identity but take no further action</td>
+ {% endif %}
+ </tr>
+ <tr>
+ {% if report.resolved and not report.valid %}
+ <th><button disabled="true">Resolve Invalid</button></th>
+ <td>Report is already resolved as invalid</td>
+ {% else %}
+ <th><button name="invalid">Resolve Invalid</button></th>
+ <td>Mark report as invalid and take no action</td>
+ {% endif %}
+ </tr>
+ <tr>
+ {% if report.subject_identity.limited %}
+ <th><button class="danger" disabled="true">Limit</button></th>
+ <td>User is already limited</td>
+ {% else %}
+ <th><button class="danger" name="limit">Limit</button></th>
+ <td>Make them less visible on this server</td>
+ {% endif %}
+ </tr>
+ <tr>
+ {% if report.subject_identity.blocked %}
+ <th><button class="danger" disabled="true">Block</button></th>
+ <td>User is already blocked</td>
+ {% else %}
+ <th><button class="danger" name="block">Block</button></th>
+ <td>Remove their existence entirely from this server</td>
+ {% endif %}
+ </tr>
+ </table>
+ </fieldset>
+ <div class="buttons">
+ <a href="{{ report.urls.admin }}" class="button secondary left">Back</a>
+ <a href="{{ report.subject_identity.urls.view }}" class="button secondary">View Profile</a>
+ <a href="{{ report.subject_identity.urls.admin_edit }}" class="button secondary">Identity Admin</a>
+ <button>Save Notes</button>
+ </div>
+ </form>
+{% endblock %}
diff --git a/templates/admin/reports.html b/templates/admin/reports.html
new file mode 100644
index 0000000..1634443
--- /dev/null
+++ b/templates/admin/reports.html
@@ -0,0 +1,43 @@
+{% extends "settings/base.html" %}
+{% load activity_tags %}
+
+{% block subtitle %}Reports{% endblock %}
+
+{% block content %}
+ <div class="view-options">
+ {% if all %}
+ <a href="." class="selected"><i class="fa-solid fa-check"></i> Show Resolved</a>
+ {% else %}
+ <a href=".?all=true"><i class="fa-solid fa-xmark"></i> Show Resolved</a>
+ {% endif %}
+ </div>
+ <section class="icon-menu">
+ {% for report in page_obj %}
+ <a class="option" href="{{ report.urls.admin_view }}">
+ <img src="{{ report.subject_identity.local_icon_url.relative }}" class="icon" alt="Avatar for {{ report.subject_identity.name_or_handle }}">
+ <span class="handle">
+ {{ report.subject_identity.html_name_or_handle }}
+ {% if report.subject_post %}
+ (post {{ report.subject_post.pk }})
+ {% endif %}
+ <small>
+ {{ report.type|title }}
+ </small>
+ </span>
+ <time>{{ report.created|timedeltashort }} ago</time>
+ </a>
+ {% empty %}
+ <p class="option empty">
+ There are no {% if all %}reports yet{% else %}unresolved reports{% endif %}.
+ </p>
+ {% endfor %}
+ <div class="load-more">
+ {% if page_obj.has_previous %}
+ <a class="button" href=".?page={{ page_obj.previous_page_number }}{% if all %}&amp;all=true{% endif %}">Previous Page</a>
+ {% endif %}
+ {% if page_obj.has_next %}
+ <a class="button" href=".?page={{ page_obj.next_page_number }}{% if all %}&amp;all=true{% endif %}">Next Page</a>
+ {% endif %}
+ </div>
+ </section>
+{% endblock %}
diff --git a/templates/settings/_menu.html b/templates/settings/_menu.html
index 0a6062d..45b7ee3 100644
--- a/templates/settings/_menu.html
+++ b/templates/settings/_menu.html
@@ -39,8 +39,8 @@
<a href="{% url "admin_hashtags" %}" {% if section == "hashtags" %}class="selected"{% endif %} title="Hashtags">
<i class="fa-solid fa-hashtag"></i> Hashtags
</a>
- <a href="{% url "admin_tuning" %}" {% if section == "tuning" %}class="selected"{% endif %} title="Tuning">
- <i class="fa-solid fa-wrench"></i> Tuning
+ <a href="{% url "admin_reports" %}" {% if section == "reports" %}class="selected"{% endif %} title="Reports">
+ <i class="fa-solid fa-flag"></i> Reports
</a>
<a href="{% url "admin_stator" %}" {% if section == "stator" %}class="selected"{% endif %} title="Stator">
<i class="fa-solid fa-clock-rotate-left"></i> Stator
diff --git a/templates/users/report.html b/templates/users/report.html
new file mode 100644
index 0000000..557b35e
--- /dev/null
+++ b/templates/users/report.html
@@ -0,0 +1,27 @@
+{% extends "base.html" %}
+
+{% block title %}Report{% endblock %}
+
+{% block content %}
+ <form action="." method="POST">
+ {% csrf_token %}
+ <fieldset>
+ <legend>Report</legend>
+ <label>Reporting</label>
+ {% if post %}
+ {% include "activities/_mini_post.html" %}
+ {% else %}
+ {% include "activities/_identity.html" %}
+ {% endif %}
+ {% include "forms/_field.html" with field=form.type %}
+ {% include "forms/_field.html" with field=form.complaint %}
+ {% if not identity.local %}
+ {% include "forms/_field.html" with field=form.forward %}
+ {% endif %}
+ </fieldset>
+
+ <div class="buttons">
+ <button>Send Report</button>
+ </div>
+ </form>
+{% endblock %}
diff --git a/templates/users/report_sent.html b/templates/users/report_sent.html
new file mode 100644
index 0000000..4e8c78c
--- /dev/null
+++ b/templates/users/report_sent.html
@@ -0,0 +1,13 @@
+{% extends "base.html" %}
+
+{% block title %}Report Sent{% endblock %}
+
+{% block content %}
+ <form action="." method="POST">
+ {% csrf_token %}
+ <fieldset>
+ <legend>Report</legend>
+ <p>Your report has been sent.</p>
+ </fieldset>
+ </form>
+{% endblock %}
diff --git a/users/admin.py b/users/admin.py
index 6c89881..e780b16 100644
--- a/users/admin.py
+++ b/users/admin.py
@@ -9,6 +9,7 @@ from users.models import (
InboxMessage,
Invite,
PasswordReset,
+ Report,
User,
UserEvent,
)
@@ -113,3 +114,8 @@ class InboxMessageAdmin(admin.ModelAdmin):
@admin.register(Invite)
class InviteAdmin(admin.ModelAdmin):
list_display = ["id", "created", "token", "note"]
+
+
+@admin.register(Report)
+class ReportAdmin(admin.ModelAdmin):
+ list_display = ["id", "created", "resolved", "type", "subject_identity"]
diff --git a/users/migrations/0005_report.py b/users/migrations/0005_report.py
new file mode 100644
index 0000000..a4556b2
--- /dev/null
+++ b/users/migrations/0005_report.py
@@ -0,0 +1,119 @@
+# Generated by Django 4.1.4 on 2022-12-17 20:38
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+import stator.models
+import users.models.report
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("activities", "0004_emoji_post_emojis"),
+ ("users", "0004_identity_admin_notes_identity_restriction_and_more"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="Report",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("state_ready", models.BooleanField(default=True)),
+ ("state_changed", models.DateTimeField(auto_now_add=True)),
+ ("state_attempted", models.DateTimeField(blank=True, null=True)),
+ ("state_locked_until", models.DateTimeField(blank=True, null=True)),
+ (
+ "state",
+ stator.models.StateField(
+ choices=[("new", "new"), ("sent", "sent")],
+ default="new",
+ graph=users.models.report.ReportStates,
+ max_length=100,
+ ),
+ ),
+ (
+ "type",
+ models.CharField(
+ choices=[
+ ("spam", "Spam"),
+ ("hateful", "Hateful"),
+ ("illegal", "Illegal"),
+ ("remote", "Remote"),
+ ("other", "Other"),
+ ],
+ max_length=100,
+ ),
+ ),
+ ("complaint", models.TextField()),
+ ("forward", models.BooleanField(default=False)),
+ ("valid", models.BooleanField(null=True)),
+ ("seen", models.DateTimeField(blank=True, null=True)),
+ ("resolved", models.DateTimeField(blank=True, null=True)),
+ ("notes", models.TextField(blank=True, null=True)),
+ ("created", models.DateTimeField(auto_now_add=True)),
+ ("updated", models.DateTimeField(auto_now=True)),
+ (
+ "moderator",
+ models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ related_name="moderated_reports",
+ to="users.identity",
+ ),
+ ),
+ (
+ "source_domain",
+ models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="filed_reports",
+ to="users.domain",
+ ),
+ ),
+ (
+ "source_identity",
+ models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="filed_reports",
+ to="users.identity",
+ ),
+ ),
+ (
+ "subject_identity",
+ models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="reports",
+ to="users.identity",
+ ),
+ ),
+ (
+ "subject_post",
+ models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ related_name="reports",
+ to="activities.post",
+ ),
+ ),
+ ],
+ options={
+ "abstract": False,
+ },
+ ),
+ ]
diff --git a/users/models/__init__.py b/users/models/__init__.py
index 1c5f519..4e271ba 100644
--- a/users/models/__init__.py
+++ b/users/models/__init__.py
@@ -5,6 +5,7 @@ from .identity import Identity, IdentityStates # noqa
from .inbox_message import InboxMessage, InboxMessageStates # noqa
from .invite import Invite # noqa
from .password_reset import PasswordReset # noqa
+from .report import Report # noqa
from .system_actor import SystemActor # noqa
from .user import User # noqa
from .user_event import UserEvent # noqa
diff --git a/users/models/report.py b/users/models/report.py
new file mode 100644
index 0000000..7bafd0c
--- /dev/null
+++ b/users/models/report.py
@@ -0,0 +1,129 @@
+import httpx
+import urlman
+from django.db import models
+
+from core.ld import canonicalise
+from stator.models import State, StateField, StateGraph, StatorModel
+from users.models import SystemActor
+
+
+class ReportStates(StateGraph):
+ new = State(try_interval=600)
+ sent = State()
+
+ new.transitions_to(sent)
+
+ @classmethod
+ async def handle_new(cls, instance: "Report"):
+ """
+ Sends the report to the remote server if we need to
+ """
+ report = await instance.afetch_full()
+ if report.forward and not report.subject_identity.domain.local:
+ system_actor = SystemActor()
+ try:
+ await system_actor.signed_request(
+ method="post",
+ uri=report.subject_identity.inbox_uri,
+ body=canonicalise(report.to_ap()),
+ )
+ except httpx.RequestError:
+ return
+ return cls.sent
+
+
+class Report(StatorModel):
+ """
+ A complaint about a user or post.
+ """
+
+ class Types(models.TextChoices):
+ spam = "spam"
+ hateful = "hateful"
+ illegal = "illegal"
+ remote = "remote"
+ other = "other"
+
+ state = StateField(ReportStates)
+
+ subject_identity = models.ForeignKey(
+ "users.Identity",
+ on_delete=models.CASCADE,
+ blank=True,
+ null=True,
+ related_name="reports",
+ )
+ subject_post = models.ForeignKey(
+ "activities.Post",
+ on_delete=models.SET_NULL,
+ blank=True,
+ null=True,
+ related_name="reports",
+ )
+
+ source_identity = models.ForeignKey(
+ "users.Identity",
+ on_delete=models.CASCADE,
+ blank=True,
+ null=True,
+ related_name="filed_reports",
+ )
+ source_domain = models.ForeignKey(
+ "users.Domain",
+ on_delete=models.CASCADE,
+ blank=True,
+ null=True,
+ related_name="filed_reports",
+ )
+
+ moderator = models.ForeignKey(
+ "users.Identity",
+ on_delete=models.SET_NULL,
+ blank=True,
+ null=True,
+ related_name="moderated_reports",
+ )
+
+ type = models.CharField(max_length=100, choices=Types.choices)
+ complaint = models.TextField()
+ forward = models.BooleanField(default=False)
+ valid = models.BooleanField(null=True)
+
+ seen = models.DateTimeField(blank=True, null=True)
+ resolved = models.DateTimeField(blank=True, null=True)
+ notes = models.TextField(blank=True, null=True)
+
+ created = models.DateTimeField(auto_now_add=True)
+ updated = models.DateTimeField(auto_now=True)
+
+ class urls(urlman.Urls):
+ admin = "/admin/reports/"
+ admin_view = "{admin}{self.pk}/"
+
+ ### ActivityPub ###
+
+ async def afetch_full(self) -> "Report":
+ return await Report.objects.select_related(
+ "source_identity",
+ "source_domain",
+ "subject_identity__domain",
+ "subject_identity",
+ "subject_post",
+ ).aget(pk=self.pk)
+
+ def to_ap(self):
+ system_actor = SystemActor()
+ if self.subject_post:
+ objects = [
+ self.subject_post.object_uri,
+ self.subject_identity.actor_uri,
+ ]
+ else:
+ objects = self.subject_identity.actor_uri
+ return {
+ "id": f"https://{self.source_domain.uri_domain}/reports/{self.id}/",
+ "type": "Flag",
+ "actor": system_actor.actor_uri,
+ "object": objects,
+ "content": self.complaint,
+ }
diff --git a/users/views/admin/__init__.py b/users/views/admin/__init__.py
index d923f80..0e054d2 100644
--- a/users/views/admin/__init__.py
+++ b/users/views/admin/__init__.py
@@ -17,6 +17,7 @@ from users.views.admin.hashtags import ( # noqa
Hashtags,
)
from users.views.admin.identities import IdentitiesRoot, IdentityEdit # noqa
+from users.views.admin.reports import ReportsRoot, ReportView # noqa
from users.views.admin.settings import ( # noqa
BasicSettings,
PoliciesSettings,
diff --git a/users/views/admin/reports.py b/users/views/admin/reports.py
new file mode 100644
index 0000000..6187068
--- /dev/null
+++ b/users/views/admin/reports.py
@@ -0,0 +1,80 @@
+from django import forms
+from django.shortcuts import get_object_or_404, redirect
+from django.utils import timezone
+from django.utils.decorators import method_decorator
+from django.views.generic import FormView, ListView
+
+from users.decorators import admin_required
+from users.models import Identity, Report
+
+
+@method_decorator(admin_required, name="dispatch")
+class ReportsRoot(ListView):
+
+ template_name = "admin/reports.html"
+ paginate_by = 30
+
+ def get(self, request, *args, **kwargs):
+ self.query = request.GET.get("query")
+ self.all = request.GET.get("all")
+ self.extra_context = {
+ "section": "reports",
+ "all": self.all,
+ }
+ return super().get(request, *args, **kwargs)
+
+ def get_queryset(self):
+ reports = Report.objects.select_related(
+ "subject_post", "subject_identity"
+ ).order_by("created")
+ if not self.all:
+ reports = reports.filter(resolved__isnull=True)
+ return reports
+
+
+@method_decorator(admin_required, name="dispatch")
+class ReportView(FormView):
+
+ template_name = "admin/report_view.html"
+ extra_context = {
+ "section": "reports",
+ }
+
+ class form_class(forms.Form):
+ notes = forms.CharField(widget=forms.Textarea, required=False)
+
+ def dispatch(self, request, id, *args, **kwargs):
+ self.report = get_object_or_404(Report, id=id)
+ return super().dispatch(request, *args, **kwargs)
+
+ def post(self, request, *args, **kwargs):
+ if "limit" in request.POST:
+ self.report.subject_identity.restriction = Identity.Restriction.limited
+ self.report.subject_identity.save()
+ if "block" in request.POST:
+ self.report.subject_identity.restriction = Identity.Restriction.blocked
+ self.report.subject_identity.save()
+ if "valid" in request.POST:
+ self.report.resolved = timezone.now()
+ self.report.valid = True
+ self.report.moderator = self.request.identity
+ self.report.save()
+ if "invalid" in request.POST:
+ self.report.resolved = timezone.now()
+ self.report.valid = False
+ self.report.moderator = self.request.identity
+ self.report.save()
+ return super().post(request, *args, **kwargs)
+
+ def get_initial(self):
+ return {"notes": self.report.notes}
+
+ def form_valid(self, form):
+ self.report.notes = form.cleaned_data["notes"]
+ self.report.save()
+ return redirect(".")
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ context["report"] = self.report
+ return context
diff --git a/users/views/report.py b/users/views/report.py
new file mode 100644
index 0000000..f7e29ed
--- /dev/null
+++ b/users/views/report.py
@@ -0,0 +1,76 @@
+from django import forms
+from django.shortcuts import get_object_or_404, render
+from django.utils.decorators import method_decorator
+from django.views.generic import FormView
+
+from users.decorators import identity_required
+from users.models import Report
+from users.shortcuts import by_handle_or_404
+
+
+@method_decorator(identity_required, name="dispatch")
+class SubmitReport(FormView):
+ """
+ Submits a report on a user or a post
+ """
+
+ template_name = "users/report.html"
+
+ class form_class(forms.Form):
+ type = forms.ChoiceField(
+ choices=[
+ ("", "------"),
+ ("spam", "Spam or inappropriate advertising"),
+ ("hateful", "Hateful, abusive, or violent speech"),
+ ("other", "Something else"),
+ ],
+ label="Why are you reporting this?",
+ )
+
+ complaint = forms.CharField(
+ widget=forms.Textarea,
+ help_text="Please describe why you think this should be removed",
+ )
+
+ forward = forms.BooleanField(
+ widget=forms.Select(
+ choices=[
+ (False, "Do not send to other server"),
+ (True, "Send to other server"),
+ ]
+ ),
+ help_text="Should we also send an anonymous copy of this to their server?",
+ required=False,
+ )
+
+ def dispatch(self, request, handle, post_id=None):
+ self.identity = by_handle_or_404(self.request, handle, local=False)
+ if post_id:
+ self.post_obj = get_object_or_404(self.identity.posts, pk=post_id)
+ else:
+ self.post_obj = None
+ return super().dispatch(request)
+
+ def form_valid(self, form):
+ # Create the report
+ report = Report.objects.create(
+ type=form.cleaned_data["type"],
+ complaint=form.cleaned_data["complaint"],
+ subject_identity=self.identity,
+ subject_post=self.post_obj,
+ source_identity=self.request.identity,
+ source_domain=self.request.identity.domain,
+ forward=form.cleaned_data.get("forward", False),
+ )
+ # Show a thanks page
+ return render(
+ self.request,
+ "users/report_sent.html",
+ {"report": report},
+ )
+
+ def get_context_data(self, *args, **kwargs):
+ context = super().get_context_data(*args, **kwargs)
+ context["identity"] = self.identity
+ context["post"] = self.post_obj
+ return context