diff options
| author | Andrew Godwin | 2022-12-17 14:45:31 -0700 | 
|---|---|---|
| committer | Andrew Godwin | 2022-12-17 14:45:31 -0700 | 
| commit | e8d6dccbb27a8611311b5f94f593b69bcca99528 (patch) | |
| tree | 440901d8495f29e682d5fd08bb3ee4e44e983505 | |
| parent | b3b2c6effdd747db9076b3139963965f4718eee9 (diff) | |
| download | takahe-e8d6dccbb27a8611311b5f94f593b69bcca99528.tar.gz takahe-e8d6dccbb27a8611311b5f94f593b69bcca99528.tar.bz2 takahe-e8d6dccbb27a8611311b5f94f593b69bcca99528.zip | |
Report function and admin
| -rw-r--r-- | activities/models/post.py | 1 | ||||
| -rw-r--r-- | static/css/style.css | 18 | ||||
| -rw-r--r-- | takahe/urls.py | 14 | ||||
| -rw-r--r-- | templates/activities/_post.html | 3 | ||||
| -rw-r--r-- | templates/admin/report_view.html | 84 | ||||
| -rw-r--r-- | templates/admin/reports.html | 43 | ||||
| -rw-r--r-- | templates/settings/_menu.html | 4 | ||||
| -rw-r--r-- | templates/users/report.html | 27 | ||||
| -rw-r--r-- | templates/users/report_sent.html | 13 | ||||
| -rw-r--r-- | users/admin.py | 6 | ||||
| -rw-r--r-- | users/migrations/0005_report.py | 119 | ||||
| -rw-r--r-- | users/models/__init__.py | 1 | ||||
| -rw-r--r-- | users/models/report.py | 129 | ||||
| -rw-r--r-- | users/views/admin/__init__.py | 1 | ||||
| -rw-r--r-- | users/views/admin/reports.py | 80 | ||||
| -rw-r--r-- | users/views/report.py | 76 | 
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 & 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 %}&all=true{% endif %}">Previous Page</a> +            {% endif %} +            {% if page_obj.has_next %} +                <a class="button" href=".?page={{ page_obj.next_page_number }}{% if all %}&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 | 
