The harassment app owns the public harassment report form, stored submissions, recipients, and notification email flow.
The old public route and reverse name stay available as /social/harassment/ and social:harassment through social.urls.
Harassment captures anonymous or attributed reports with optional email plus a free-text message.HarassmentEmailRecipient stores the notification recipient list.Harassment.__str__ returns a content-free label from harassment.redaction.report_label, for example Trakasserianmälan #12. Never make it return self.message.django_admin_log: Django stores str(obj)[:200] in LogEntry.object_repr for every add, change, and delete, and the admin log page (Admin › Log entries) is readable by staff who are not report recipients. The same value reaches the admin success message, which the messages framework keeps in the session and the cookie.harassment/migrations/0002_redact_admin_log_reports.py rewrites report text that earlier releases stored in object_repr. Rows keep the report id, so the audit trail still shows which report was touched. It covers harassment.harassment and the pre-split social.harassment content type; both are listed in harassment.redaction.REPORT_CONTENT_TYPES.python manage.py redact_harassment_logs applies the same rewrite on demand and is idempotent. Migrations run before the application rolls, and a blue-green standby shares the database with the live release, so a pod running the older image can write one more content-bearing row after the migration has passed. Run the command once the old pods are gone (it is a release step, see operations.md), and again after any release that touched report logging. Pre-deploy database dumps still contain the text; redacting them needs the operator’s backup policy, not application code.message_preview on HarassmentAdmin as the place where authorised staff read a report, and keep the full message out of list_display, admin actions, and logging.HarassmentForm is a simple ModelForm that adds Bootstrap classes. The captcha token is read directly from request.POST['cf-turnstile-response'].harassment.views.harassment_form handles the PRG flow:
harass_submitted session flag on GET only. A GET with the flag set renders the success page once; a POST with the flag still set (the redirect GET never landed: back button, dropped connection, a second tab) is validated and saved instead of being answered with the success page and silently dropped. social.tests.HarassmentViewTests covers both.templates/social/harassment_admin_email.txt receives harassment and harassment_url.harassment_url points at /admin/harassment/harassment/<id>/..txt templates so djlint does not rewrite meaningful email whitespace. Django template variables and tags work normally in .txt files; use .html only for an actual HTML message body.social.Harassment and social.HarassmentEmailRecipient.social harassment and recipient permissions continue to grant equivalent admin access while stale content types are being migrated.is_reviewed or assigned_to here if case management becomes more formal.