from flask import Blueprint, render_template, redirect, url_for
-from flask_user import current_user, login_required
+from flask_user import current_user
from app.models import db, AuditLogEntry, UserRank
from app.utils import rank_required
from . import bp
+
@bp.route("/admin/audit/")
-@login_required
@rank_required(UserRank.MODERATOR)
def audit():
log = AuditLogEntry.query.order_by(db.desc(AuditLogEntry.created_at)).all()
return render_template("admin/audit.html", log=log)
+
+
+@bp.route("/admin/audit/<int:id>/")
+@rank_required(UserRank.MODERATOR)
+def audit_view(id):
+ entry = AuditLogEntry.query.get(id)
+ return render_template("admin/audit_view.html", entry=entry)
return redirect(thread.getViewURL())
+@bp.route("/threads/<int:id>/delete/", methods=["GET", "POST"])
+@login_required
+def delete_reply(id):
+ thread = Thread.query.get(id)
+ if thread is None:
+ abort(404)
+
+ reply_id = request.args.get("reply")
+ if reply_id is None:
+ abort(404)
+
+ reply = ThreadReply.query.get(reply_id)
+ if reply is None or reply.thread != thread:
+ abort(404)
+
+ if thread.replies[0] == reply:
+ flash("Cannot delete thread opening post!", "danger")
+ return redirect(thread.getViewURL())
+
+ if not thread.checkPerm(current_user, Permission.DELETE_REPLY):
+ abort(403)
+
+ if request.method == "GET":
+ return render_template("threads/delete_reply.html", thread=thread, reply=reply)
+
+ msg = "Deleted reply by {}".format(reply.author.display_name)
+ addAuditLog(AuditSeverity.MODERATION, current_user, msg, thread.getViewURL(), thread.package, reply.comment)
+
+ db.session.delete(reply)
+ db.session.commit()
+
+ return redirect(thread.getViewURL())
+
+
@bp.route("/threads/<int:id>/", methods=["GET", "POST"])
def view(id):
thread = Thread.query.get(id)
private = BooleanField("Private")
submit = SubmitField("Open Thread")
+
@bp.route("/threads/new/", methods=["GET", "POST"])
@login_required
def new():
CREATE_THREAD = "CREATE_THREAD"
COMMENT_THREAD = "COMMENT_THREAD"
LOCK_THREAD = "LOCK_THREAD"
+ DELETE_REPLY = "DELETE_REPLY"
UNAPPROVE_PACKAGE = "UNAPPROVE_PACKAGE"
TOPIC_DISCARD = "TOPIC_DISCARD"
CREATE_TOKEN = "CREATE_TOKEN"
elif perm == Permission.COMMENT_THREAD:
return canSee and (not self.locked or user.rank.atLeast(UserRank.MODERATOR))
- elif perm == Permission.LOCK_THREAD:
+ elif perm == Permission.LOCK_THREAD or perm == Permission.DELETE_REPLY:
return user.rank.atLeast(UserRank.MODERATOR)
else:
package_id = db.Column(db.Integer, db.ForeignKey("package.id"), nullable=True)
package = db.relationship("Package", foreign_keys=[package_id])
- def __init__(self, causer, severity, title, url, package=None):
+ description = db.Column(db.Text, nullable=True, default=None)
+
+ def __init__(self, causer, severity, title, url, package=None, description=None):
if len(title) > 100:
title = title[:99] + "…"
self.title = title
self.url = url
self.package = package
+ self.description = description
<div class="list-group mt-3">
{% for entry in log %}
- <a class="list-group-item list-group-item-action" href="{{ entry.url }}">
+ <a class="list-group-item list-group-item-action"
+ {% if entry.description %}
+ href="{{ url_for('admin.audit_view', id=entry.id) }}">
+ {% else %}
+ href="{{ entry.url }}">
+ {% endif %}
+
<div class="row {% if entry.severity == entry.severity.NORMAL %}text-muted{% endif %}">
<div class="col-sm-auto text-center" style="width: 50px;">
{% if entry.severity == entry.severity.MODERATION %}
<div class="col-sm">
{{ entry.title}}
+
+ {% if entry.description %}
+ <i class="fas fa-paperclip ml-3"></i>
+ {% endif %}
</div>
{% if entry.package %}
{% else %}
<p class="list-group-item"><i>No audit log entires.</i></p>
{% endfor %}
- </ul>
+ </div>
{% endblock %}
--- /dev/null
+{% extends "base.html" %}
+
+{% block title %}
+{{ entry.title }}
+{% endblock %}
+
+{% block content %}
+ {% if entry.url %}
+ <a class="float-right btn btn-primary" href="{{ entry.url }}">View</a>
+ {% endif %}
+
+ <h1>{{ entry.title }}</h1>
+ <p class="text-muted mb-4">
+ {{ _("Caused by %(author)s.", author=entry.causer.display_name) }}
+ </p>
+
+ <pre><code>{{ entry.description }}</code></pre>
+
+{% endblock %}
</div>
<div class="card-body">
+ {% if r != thread.replies[0] and thread.checkPerm(current_user, "DELETE_REPLY") %}
+ <a class="float-right btn btn-secondary btn-sm"
+ href="{{ url_for('threads.delete_reply', id=thread.id, reply=r.id) }}">
+ <i class="fas fa-trash"></i>
+ </a>
+ {% endif %}
+
{{ r.comment | markdown }}
</div>
</div>
--- /dev/null
+{% extends "base.html" %}
+
+{% block title %}
+ Delete reply in {{ thread.title }}
+{% endblock %}
+
+{% block content %}
+ <form method="POST" action="" class="card box_grey">
+ <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
+
+ <h3 class="card-header">Delete reply by {{ reply.author.display_name }}</h3>
+ <div class="card-body">
+ {{ reply.comment | markdown }}
+ </div>
+ <div class="card-body">
+ <p>Deleting is permanent</p>
+
+ <a class="btn btn-secondary mr-3" href="{{ thread.getViewURL() }}">Cancel</a>
+ <input type="submit" value="Delete" class="btn btn-danger" />
+ </div>
+ </form>
+{% endblock %}
db.session.add(notif)
-def addAuditLog(severity, causer, title, url, package=None):
- entry = AuditLogEntry(causer, severity, title, url, package)
+def addAuditLog(severity, causer, title, url, package=None, description=None):
+ entry = AuditLogEntry(causer, severity, title, url, package, description)
db.session.add(entry)
--- /dev/null
+"""empty message
+
+Revision ID: 86512692b770
+Revises: ba730ce1dc3e
+Create Date: 2020-07-11 01:56:28.634661
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '86512692b770'
+down_revision = 'ba730ce1dc3e'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.add_column('audit_log_entry', sa.Column('description', sa.Text, nullable=True))
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_column('audit_log_entry', 'description')
+ # ### end Alembic commands ###