From 8d1268bd1994c07e8f76e1060e35352966bdf086 Mon Sep 17 00:00:00 2001
From: rubenwardy
Date: Tue, 26 Aug 2025 15:46:41 +0100
Subject: [PATCH] Add report categories
---
app/blueprints/report/__init__.py | 12 ++++--
app/models/__init__.py | 56 +++++++++++++++++++++++++++-
app/templates/report/report.html | 1 +
app/templates/report/view.html | 6 +++
app/templates/users/account.html | 2 +-
migrations/versions/1e08d7e4c15d_.py | 28 ++++++++++++++
6 files changed, 99 insertions(+), 6 deletions(-)
create mode 100644 migrations/versions/1e08d7e4c15d_.py
diff --git a/app/blueprints/report/__init__.py b/app/blueprints/report/__init__.py
index e2765c51..f3634430 100644
--- a/app/blueprints/report/__init__.py
+++ b/app/blueprints/report/__init__.py
@@ -19,10 +19,10 @@ from flask_babel import lazy_gettext
from flask_login import current_user
from flask_wtf import FlaskForm
from werkzeug.utils import redirect
-from wtforms import TextAreaField, SubmitField, URLField, StringField, RadioField
+from wtforms import TextAreaField, SubmitField, URLField, StringField, RadioField, SelectField
from wtforms.validators import InputRequired, Length, Optional
-from app.models import User, UserRank, Report, db, AuditSeverity, Thread
+from app.models import User, UserRank, Report, db, AuditSeverity, Thread, ReportCategory
from app.tasks.webhooktasks import post_discord_webhook
from app.utils import is_no, abs_url_samesite, normalize_line_endings, rank_required, add_audit_log, abs_url_for, \
add_replies, random_string
@@ -31,6 +31,8 @@ bp = Blueprint("report", __name__)
class ReportForm(FlaskForm):
+ category = SelectField(lazy_gettext("Category"), [InputRequired()], choices=ReportCategory.choices(with_none=True), coerce=ReportCategory.coerce)
+
url = URLField(lazy_gettext("URL"), [Optional()])
title = StringField(lazy_gettext("Subject / Title"), [InputRequired(), Length(10, 300)])
message = TextAreaField(lazy_gettext("Message"), [Optional(), Length(0, 10000)], filters=[normalize_line_endings])
@@ -50,8 +52,12 @@ def report():
form = ReportForm(formdata=request.form) if current_user.is_authenticated else None
if form and request.method == "GET":
+ try:
+ form.category.data = ReportCategory.coerce(request.args.get("category"))
+ except KeyError:
+ pass
form.url.data = url
- form.message.data = request.args.get("message", "")
+ form.title.data = request.args.get("title", "")
if form and form.validate_on_submit():
report = Report()
diff --git a/app/models/__init__.py b/app/models/__init__.py
index 0946ef63..cb4596e4 100644
--- a/app/models/__init__.py
+++ b/app/models/__init__.py
@@ -13,8 +13,7 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-
-
+from flask_babel import LazyString
from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy_searchable import make_searchable
@@ -130,6 +129,58 @@ class AuditLogEntry(db.Model):
raise Exception("Permission {} is not related to audit log entries".format(perm.name))
+class ReportCategory(enum.Enum):
+ ACCOUNT_DELETION = "account_deletion"
+ COPYRIGHT = "copyright"
+ USER_CONDUCT = "user_conduct"
+ ILLEGAL_HARMFUL = "illegal_harmful"
+ APPEAL = "appeal"
+ OTHER = "other"
+
+ def __str__(self):
+ return self.name
+
+ @property
+ def title(self) -> LazyString:
+ if self == ReportCategory.ACCOUNT_DELETION:
+ return lazy_gettext("Account deletion")
+ elif self == ReportCategory.COPYRIGHT:
+ return lazy_gettext("Copyright infringement / DMCA")
+ elif self == ReportCategory.USER_CONDUCT:
+ return lazy_gettext("User behaviour, bullying, or abuse")
+ elif self == ReportCategory.ILLEGAL_HARMFUL:
+ return lazy_gettext("Illegal or harmful content")
+ elif self == ReportCategory.APPEAL:
+ return lazy_gettext("Appeal")
+ elif self == ReportCategory.OTHER:
+ return lazy_gettext("Other")
+ else:
+ raise Exception("Unknown report category")
+
+ @classmethod
+ def get(cls, name):
+ try:
+ return ReportCategory[name.upper()]
+ except KeyError:
+ return None
+
+ @classmethod
+ def choices(cls, with_none):
+ ret = [(choice, choice.title) for choice in cls]
+
+ if with_none:
+ ret.insert(0, (None, ""))
+
+ return ret
+
+ @classmethod
+ def coerce(cls, item):
+ if item is None or (isinstance(item, str) and item.upper() == "NONE"):
+ return None
+ return item if type(item) == ReportCategory else ReportCategory[item.upper()]
+
+
+
class Report(db.Model):
id = db.Column(db.String(24), primary_key=True)
@@ -141,6 +192,7 @@ class Report(db.Model):
thread_id = db.Column(db.Integer, db.ForeignKey("thread.id"), nullable=True)
thread = db.relationship("Thread", foreign_keys=[thread_id])
+ category = db.Column(db.Enum(ReportCategory), nullable=False)
url = db.Column(db.String, nullable=True)
title = db.Column(db.Unicode(300), nullable=False)
message = db.Column(db.UnicodeText, nullable=False)
diff --git a/app/templates/report/report.html b/app/templates/report/report.html
index 1e627e0a..5bd2e64a 100644
--- a/app/templates/report/report.html
+++ b/app/templates/report/report.html
@@ -28,6 +28,7 @@
- {{ _("Report") }}
+ {{ _("Report") }}
{% endif %}
diff --git a/migrations/versions/1e08d7e4c15d_.py b/migrations/versions/1e08d7e4c15d_.py
new file mode 100644
index 00000000..f2f810d5
--- /dev/null
+++ b/migrations/versions/1e08d7e4c15d_.py
@@ -0,0 +1,28 @@
+"""empty message
+
+Revision ID: 1e08d7e4c15d
+Revises: 9689a71efe88
+Create Date: 2025-08-26 14:43:30.501823
+
+"""
+from alembic import op
+import sqlalchemy as sa
+from sqlalchemy.dialects import postgresql
+
+# revision identifiers, used by Alembic.
+revision = '1e08d7e4c15d'
+down_revision = '9689a71efe88'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ status = postgresql.ENUM('ACCOUNT_DELETION', 'COPYRIGHT', 'USER_CONDUCT', 'ILLEGAL_HARMFUL', 'APPEAL', 'OTHER', name='reportcategory')
+ status.create(op.get_bind())
+ with op.batch_alter_table('report', schema=None) as batch_op:
+ batch_op.add_column(sa.Column('category', sa.Enum('ACCOUNT_DELETION', 'COPYRIGHT', 'USER_CONDUCT', 'ILLEGAL_HARMFUL', 'APPEAL', 'OTHER', name='reportcategory'), nullable=False, server_default="OTHER"))
+
+
+def downgrade():
+ with op.batch_alter_table('report', schema=None) as batch_op:
+ batch_op.drop_column('category')