Add reports to database
This commit is contained in:
@@ -19,19 +19,21 @@ 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
|
||||
from wtforms.validators import InputRequired, Length
|
||||
from wtforms import TextAreaField, SubmitField, URLField, StringField, RadioField
|
||||
from wtforms.validators import InputRequired, Length, Optional
|
||||
|
||||
from app.models import User, UserRank
|
||||
from app.tasks.emails import send_user_email
|
||||
from app.models import User, UserRank, Report, db, AuditSeverity, Thread
|
||||
from app.tasks.webhooktasks import post_discord_webhook
|
||||
from app.utils import is_no, abs_url_samesite, normalize_line_endings
|
||||
from app.utils import is_no, abs_url_samesite, normalize_line_endings, rank_required, add_audit_log, abs_url_for, \
|
||||
add_replies
|
||||
|
||||
bp = Blueprint("report", __name__)
|
||||
|
||||
|
||||
class ReportForm(FlaskForm):
|
||||
message = TextAreaField(lazy_gettext("Message"), [InputRequired(), Length(10, 10000)], filters=[normalize_line_endings])
|
||||
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])
|
||||
submit = SubmitField(lazy_gettext("Report"))
|
||||
|
||||
|
||||
@@ -48,23 +50,68 @@ def report():
|
||||
|
||||
form = ReportForm(formdata=request.form) if current_user.is_authenticated else None
|
||||
if form and request.method == "GET":
|
||||
form.url.data = url
|
||||
form.message.data = request.args.get("message", "")
|
||||
|
||||
if form and form.validate_on_submit():
|
||||
report = Report()
|
||||
report.user = current_user if current_user.is_authenticated else None
|
||||
form.populate_obj(report)
|
||||
|
||||
if not current_user.is_authenticated:
|
||||
ip_addr = request.headers.get("X-Forwarded-For") or request.remote_addr
|
||||
report.message = ip_addr + "\n\n" + report.message
|
||||
|
||||
db.session.add(report)
|
||||
db.session.flush()
|
||||
|
||||
if current_user.is_authenticated:
|
||||
user_info = f"{current_user.username}"
|
||||
add_audit_log(AuditSeverity.USER, current_user, f"New report: {report.title}",
|
||||
url_for("report.view", rid=report.id))
|
||||
|
||||
db.session.commit()
|
||||
|
||||
abs_url = abs_url_for("report.view", rid=report.id)
|
||||
msg = f"**New Report**\nReport on `{report.url}`\n\n{report.title}\n\nView: {abs_url}"
|
||||
post_discord_webhook.delay(None if is_anon else current_user.username, msg, True)
|
||||
|
||||
return redirect(url_for("report.view", rid=report.id))
|
||||
|
||||
return render_template("report/report.html", form=form, url=url, is_anon=is_anon, noindex=url is not None)
|
||||
|
||||
|
||||
@bp.route("/admin/reports/")
|
||||
@rank_required(UserRank.MODERATOR)
|
||||
def list_all():
|
||||
reports = Report.query.order_by(db.desc(Report.is_resolved), db.desc(Report.created_at)).all()
|
||||
return render_template("report/list.html", reports=reports)
|
||||
|
||||
|
||||
class ResolveForm(FlaskForm):
|
||||
completed = SubmitField(lazy_gettext("Completed / resolved"))
|
||||
removed = SubmitField(lazy_gettext("Content removed"))
|
||||
invalid = SubmitField(lazy_gettext("Invalid / No action taken"))
|
||||
|
||||
|
||||
@bp.route("/admin/reports/<int:rid>/", methods=["GET", "POST"])
|
||||
@rank_required(UserRank.MODERATOR)
|
||||
def view(rid: int):
|
||||
report = Report.query.get_or_404(rid)
|
||||
|
||||
resolve_form = ResolveForm(request.form)
|
||||
if resolve_form.validate_on_submit():
|
||||
if resolve_form.completed.data:
|
||||
outcome = "completed"
|
||||
elif resolve_form.removed.data:
|
||||
outcome = "removed"
|
||||
elif resolve_form.invalid.data:
|
||||
outcome = "invalid"
|
||||
else:
|
||||
user_info = request.headers.get("X-Forwarded-For") or request.remote_addr
|
||||
abort(400)
|
||||
|
||||
text = f"{url}\n\n{form.message.data}"
|
||||
report.is_resolved = True
|
||||
url = url_for("report.view", rid=report.id)
|
||||
add_audit_log(AuditSeverity.MODERATION, current_user, f"Resolved report as {outcome} \"{report.title}\"", url)
|
||||
db.session.commit()
|
||||
|
||||
task = None
|
||||
for admin in User.query.filter_by(rank=UserRank.ADMIN).all():
|
||||
task = send_user_email.delay(admin.email, admin.locale or "en",
|
||||
f"User report from {user_info}", text)
|
||||
|
||||
post_discord_webhook.delay(None if is_anon else current_user.username, f"**New Report**\n{url}\n\n{form.message.data}", True)
|
||||
|
||||
return redirect(url_for("tasks.check", id=task.id, r=url_for("homepage.home")))
|
||||
|
||||
return render_template("report/index.html", form=form, url=url, is_anon=is_anon, noindex=url is not None)
|
||||
return render_template("report/view.html", report=report, resolve_form=resolve_form)
|
||||
|
||||
@@ -130,6 +130,24 @@ class AuditLogEntry(db.Model):
|
||||
raise Exception("Permission {} is not related to audit log entries".format(perm.name))
|
||||
|
||||
|
||||
class Report(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
|
||||
|
||||
user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=True)
|
||||
user = db.relationship("User", foreign_keys=[user_id], back_populates="reports")
|
||||
|
||||
thread_id = db.Column(db.Integer, db.ForeignKey("thread.id"), nullable=True)
|
||||
thread = db.relationship("Thread", foreign_keys=[thread_id])
|
||||
|
||||
url = db.Column(db.String, nullable=True)
|
||||
title = db.Column(db.Unicode(300), nullable=False)
|
||||
message = db.Column(db.UnicodeText, nullable=False)
|
||||
|
||||
is_resolved = db.Column(db.Boolean, nullable=False, default=False)
|
||||
|
||||
|
||||
REPO_BLACKLIST = [".zip", "mediafire.com", "dropbox.com", "weebly.com",
|
||||
"minetest.net", "luanti.org", "dropboxusercontent.com", "4shared.com",
|
||||
"digitalaudioconcepts.com", "hg.intevation.org", "www.wtfpl.net",
|
||||
|
||||
@@ -210,6 +210,7 @@ class User(db.Model, UserMixin):
|
||||
forum_topics = db.relationship("ForumTopic", back_populates="author", lazy="dynamic", cascade="all, delete, delete-orphan")
|
||||
collections = db.relationship("Collection", back_populates="author", lazy="dynamic", cascade="all, delete, delete-orphan", order_by=db.asc("title"))
|
||||
clients = db.relationship("OAuthClient", back_populates="owner", lazy="dynamic", cascade="all, delete, delete-orphan")
|
||||
reports = db.relationship("Report", back_populates="user", lazy="dynamic", cascade="all")
|
||||
|
||||
ban = db.relationship("UserBan", foreign_keys="UserBan.user_id", back_populates="user", uselist=False)
|
||||
|
||||
|
||||
33
app/templates/report/list.html
Normal file
33
app/templates/report/list.html
Normal file
@@ -0,0 +1,33 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title -%}
|
||||
{{ _("Reports") }}
|
||||
{%- endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1>{{ self.title() }}</h1>
|
||||
|
||||
<nav class="list-group">
|
||||
{% for report in reports %}
|
||||
<a class="list-group-item list-group-item-action" href="{{ url_for('report.view', rid=report.id) }}">
|
||||
{% if report.is_resolved %}
|
||||
<span class="badge bg-secondary me-3">
|
||||
Closed
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="badge bg-warning me-3">
|
||||
Open
|
||||
</span>
|
||||
{% endif %}
|
||||
{{ report.title }}
|
||||
</a>
|
||||
{% else %}
|
||||
<span>
|
||||
No reports.
|
||||
</span>
|
||||
{% endfor %}
|
||||
</nav>
|
||||
|
||||
{% endblock %}
|
||||
@@ -4,6 +4,10 @@
|
||||
{{ _("Report") }}
|
||||
{%- endblock %}
|
||||
|
||||
{% from "macros/forms.html" import render_field, render_submit_field, render_checkbox_field, easymde_scripts %}
|
||||
{% block scriptextra %}
|
||||
{{ easymde_scripts() }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
@@ -22,23 +26,17 @@
|
||||
|
||||
{% else %}
|
||||
|
||||
{% from "macros/forms.html" import render_field, render_submit_field, render_checkbox_field %}
|
||||
|
||||
<form method="POST" action="" enctype="multipart/form-data">
|
||||
{{ form.hidden_tag() }}
|
||||
{% if url %}
|
||||
<p>
|
||||
URL: <code>{{ url }}</code>
|
||||
</p>
|
||||
{% endif %}
|
||||
{{ render_field(form.message, hint=_("What are you reporting? Why are you reporting it?")) }}
|
||||
{{ render_field(form.url) }}
|
||||
{{ render_field(form.title) }}
|
||||
{{ render_field(form.message, class_="m-0", fieldclass="form-control markdown", data_enter_submit="1") }}
|
||||
{{ render_submit_field(form.submit) }}
|
||||
|
||||
<p class="mt-5 text-muted">
|
||||
{{ _("Reports will be shared with ContentDB staff.") }}
|
||||
{% if is_anon %}
|
||||
{{ _("Only the admin will be able to see who made the report.") }}
|
||||
{% endif %}
|
||||
{{ _("The full report will be visible to all ContentDB staff members, including editors and moderators.") }}
|
||||
{{ _("If you are reporting content posted by another user, we may discuss the report with them but will not disclose who reported it.") }}
|
||||
{{ _("If you are reporting content by an editor or moderator, then you should email the admin or a moderator directly to hide your identity.") }}
|
||||
</p>
|
||||
<p class="alert alert-info">
|
||||
{{ _("Found a bug? Please report on the package's issue tracker or in a thread instead.") }}
|
||||
80
app/templates/report/view.html
Normal file
80
app/templates/report/view.html
Normal file
@@ -0,0 +1,80 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title -%}
|
||||
{{ report.title }}
|
||||
{%- endblock %}
|
||||
|
||||
{% from "macros/forms.html" import render_field, render_submit_field, render_radio_field, easymde_scripts %}
|
||||
{% block scriptextra %}
|
||||
{{ easymde_scripts() }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<p>
|
||||
<a href="{{ url_for('report.list_all') }}">Back to reports</a>
|
||||
</p>
|
||||
|
||||
<h1>
|
||||
{% if report.is_resolved %}
|
||||
<span class="badge bg-secondary me-3">
|
||||
Closed
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="badge bg-warning me-3">
|
||||
Open
|
||||
</span>
|
||||
{% endif %}
|
||||
{{ self.title() }}
|
||||
</h1>
|
||||
|
||||
<article class="row">
|
||||
<div class="col-md-9">
|
||||
<div class="card">
|
||||
<div class="card-body markdown">
|
||||
{{ report.message | markdown }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="col-md-3 info-sidebar">
|
||||
<dl>
|
||||
<dt>URL</dt>
|
||||
<dd>
|
||||
<a href="{{ report.url }}">{{ report.url }}</a>
|
||||
</dd>
|
||||
</dl>
|
||||
<dl>
|
||||
<dt>Created At</dt>
|
||||
<dd>
|
||||
{{ report.created_at | full_datetime }}
|
||||
</dd>
|
||||
</dl>
|
||||
<dl>
|
||||
<dt>Reporter</dt>
|
||||
<dd>
|
||||
{% if report.user %}
|
||||
<a href="{{ url_for('users.profile', username=report.user.username) }}">
|
||||
{{ report.user.username }}
|
||||
</a>
|
||||
{% else %}
|
||||
Anonymous
|
||||
{% endif %}
|
||||
</dd>
|
||||
</dl>
|
||||
</aside>
|
||||
</article>
|
||||
|
||||
{% if not report.is_resolved %}
|
||||
<article>
|
||||
<h2>Quick resolve</h2>
|
||||
<form method="POST" action="">
|
||||
{{ resolve_form.hidden_tag() }}
|
||||
{{ render_submit_field(resolve_form.completed) }}
|
||||
{{ render_submit_field(resolve_form.removed) }}
|
||||
{{ render_submit_field(resolve_form.invalid) }}
|
||||
</form>
|
||||
</article>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
30
migrations/versions/3052712496e4_.py
Normal file
30
migrations/versions/3052712496e4_.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '3052712496e4'
|
||||
down_revision = '663521dfe86d'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('report',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||
sa.Column('thread_id', sa.Integer(), nullable=True),
|
||||
sa.Column('url', sa.String(), nullable=True),
|
||||
sa.Column('title', sa.Unicode(length=300), nullable=False),
|
||||
sa.Column('message', sa.UnicodeText(), nullable=False),
|
||||
sa.Column('is_resolved', sa.Boolean(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
||||
sa.ForeignKeyConstraint(['thread_id'], ['thread.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_table('report')
|
||||
Reference in New Issue
Block a user