Admin Log 2.0 (#283)

* Admin log added

* Add admin log to top bar

* Fixed some admin log bugs

* Remove comment_id column because comments die when they are killed

* Fix tabs in admin log template

* Fixed sort of admin logs to be created_time desc

* Fix navbar wrapping to a new line

when 992px <= width <= 1200px

* Put reports and admin log in "Admin" dropdown

Applied ./lint.sh fixes

Fixed long lines

* Updated log to be text instead of id based to account for future deletions

* Small fix in log message formatting
This commit is contained in:
mreweilk 2017-07-04 23:13:59 -05:00 committed by GitHub
parent 1cee6cb647
commit dd8cb4757e
7 changed files with 185 additions and 12 deletions

View File

@ -0,0 +1,44 @@
"""Admin log added
Revision ID: 1add911660a6
Revises: 7f064e009cab
Create Date: 2017-06-29 02:57:39.715965
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '1add911660a6'
down_revision = '7f064e009cab'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('nyaa_adminlog',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('created_time', sa.DateTime(), nullable=True),
sa.Column('log', sa.String(length=1024), nullable=False),
sa.Column('admin_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['admin_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('sukebei_adminlog',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('created_time', sa.DateTime(), nullable=True),
sa.Column('log', sa.String(length=1024), nullable=False),
sa.Column('admin_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['admin_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('sukebei_adminlog')
op.drop_table('nyaa_adminlog')
# ### end Alembic commands ###

View File

@ -10,7 +10,8 @@ from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileRequired
from wtforms import StringField, PasswordField, BooleanField, TextAreaField, SelectField,\
HiddenField
from wtforms.validators import DataRequired, Optional, Email, Length, EqualTo, ValidationError, StopValidation
from wtforms.validators import DataRequired, Optional, Email, Length, EqualTo, ValidationError,\
StopValidation
from wtforms.validators import Regexp
# For DisabledSelectField
@ -49,6 +50,7 @@ def stop_on_validation_error(f):
raise StopValidation(*e.args) from e
return decorator
_username_validator = Regexp(
r'^[a-zA-Z0-9_\-]+$',
message='Your username must only consist of alphanumerics and _- (a-zA-Z0-9_-)')

View File

@ -563,6 +563,38 @@ class User(db.Model):
return self.level >= UserLevelType.TRUSTED
class AdminLogBase(DeclarativeHelperBase):
__tablename_base__ = 'adminlog'
id = db.Column(db.Integer, primary_key=True)
created_time = db.Column(db.DateTime(timezone=False), default=datetime.utcnow)
log = db.Column(db.String(length=1024), nullable=False)
@declarative.declared_attr
def admin_id(cls):
return db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
def __init__(self, log, admin_id):
self.log = log
self.admin_id = admin_id
def __repr__(self):
return '<AdminLog %r>' % self.id
@property
def created_utc_timestamp(self):
''' Returns a UTC POSIX timestamp, as seconds '''
return (self.created_time - UTC_EPOCH).total_seconds()
@declarative.declared_attr
def admin(cls):
return db.relationship('User', uselist=False, lazy="joined")
@classmethod
def all_logs(cls):
return cls.query
class ReportStatus(IntEnum):
IN_REVIEW = 0
VALID = 1
@ -714,6 +746,15 @@ class SukebeiComment(CommentBase, db.Model):
__flavor__ = 'Sukebei'
# AdminLog
class NyaaAdminLog(AdminLogBase, db.Model):
__flavor__ = 'Nyaa'
class SukebeiAdminLog(AdminLogBase, db.Model):
__flavor__ = 'Sukebei'
# Report
class NyaaReport(ReportBase, db.Model):
__flavor__ = 'Nyaa'
@ -733,9 +774,10 @@ if app.config['SITE_FLAVOR'] == 'nyaa':
MainCategory = NyaaMainCategory
SubCategory = NyaaSubCategory
Comment = NyaaComment
AdminLog = NyaaAdminLog
Report = NyaaReport
TorrentNameSearch = NyaaTorrentNameSearch
elif app.config['SITE_FLAVOR'] == 'sukebei':
Torrent = SukebeiTorrent
TorrentFilelist = SukebeiTorrentFilelist
@ -745,6 +787,6 @@ elif app.config['SITE_FLAVOR'] == 'sukebei':
MainCategory = SukebeiMainCategory
SubCategory = SukebeiSubCategory
Comment = SukebeiComment
AdminLog = SukebeiAdminLog
Report = SukebeiReport
TorrentNameSearch = SukebeiTorrentNameSearch

View File

@ -325,20 +325,26 @@ def view_user(user_name):
if flask.request.method == 'GET':
admin_form.user_class.data = default
url = flask.url_for('view_user', user_name=user.username)
if flask.request.method == 'POST' and admin_form and admin_form.validate():
selection = admin_form.user_class.data
log = None
if selection == 'regular':
user.level = models.UserLevelType.REGULAR
log = "[{}]({}) changed to regular user".format(user_name, url)
elif selection == 'trusted':
user.level = models.UserLevelType.TRUSTED
log = "[{}]({}) changed to trusted user".format(user_name, url)
elif selection == 'moderator':
user.level = models.UserLevelType.MODERATOR
log = "[{}]({}) changed to moderator user".format(user_name, url)
adminlog = models.AdminLog(log=log, admin_id=flask.g.user.id)
db.session.add(user)
db.session.add(adminlog)
db.session.commit()
return flask.redirect(flask.url_for('view_user', user_name=user.username))
return flask.redirect(url)
user_level = ['Regular', 'Trusted', 'Moderator', 'Administrator'][user.level]
@ -701,11 +707,17 @@ def delete_comment(torrent_id, comment_id):
db.session.delete(comment)
db.session.flush()
torrent.update_comment_count()
url = flask.url_for('view_torrent', torrent_id=torrent.id)
if flask.g.user.is_moderator:
log = "Comment deleted on torrent [#{}]({})".format(torrent.id, url)
adminlog = models.AdminLog(log=log, admin_id=flask.g.user.id)
db.session.add(adminlog)
db.session.commit()
flask.flash('Comment successfully deleted.', 'success')
return flask.redirect(flask.url_for('view_torrent', torrent_id=torrent_id))
return flask.redirect(url)
@app.route('/view/<int:torrent_id>/edit', methods=['GET', 'POST'])
@ -745,13 +757,22 @@ def edit_torrent(torrent_id):
if flask.g.user.is_moderator:
torrent.deleted = form.is_deleted.data
if flask.g.user.is_moderator:
log = "Torrent [#{}]({}) marked as ".format(torrent.id, url)
url = flask.url_for('view_torrent', torrent_id=torrent.id)
adminlog = models.AdminLog(log=(log+"deleted"
if torrent.deleted else
log+"undeleted"),
admin_id=flask.g.user.id)
db.session.add(adminlog)
db.session.commit()
flask.flash(flask.Markup(
'Torrent has been successfully edited! Changes might take a few minutes to show up.'),
'info')
return flask.redirect(flask.url_for('view_torrent', torrent_id=torrent.id))
return flask.redirect(url)
else:
if flask.request.method != 'POST':
# Fill form data only if the POST didn't fail
@ -821,6 +842,20 @@ def submit_report(torrent_id):
return flask.redirect(flask.url_for('view_torrent', torrent_id=torrent_id))
@app.route('/adminlog', methods=['GET'])
def view_adminlog():
if not flask.g.user or not flask.g.user.is_moderator:
flask.abort(403)
page = flask.request.args.get('p', flask.request.args.get('offset', 1, int), int)
logs = models.AdminLog.all_logs() \
.order_by(models.AdminLog.created_time.desc()) \
.paginate(page=page, per_page=20)
return flask.render_template('adminlog.html',
adminlog=logs)
@app.route('/reports', methods=['GET', 'POST'])
def view_reports():
if not flask.g.user or not flask.g.user.is_moderator:

View File

@ -160,7 +160,7 @@ table.torrent-list tbody .comments i {
-ms-flex-order: 3;
order: 3;
width: 99%;
padding-right: 4em;
padding-right: 1em;
border-left: 0;
}
@ -380,7 +380,7 @@ h6:hover .header-anchor {
display: inline-block;
}
@media (max-width: 991px){
@media (max-width: 991px) {
.panel-body .col-md-5 {
margin-left: 20px;
margin-bottom: 10px;
@ -397,11 +397,11 @@ h6:hover .header-anchor {
}
}
@media (min-width: 992px){
@media (min-width: 992px) {
.panel-body .col-md-5 {
word-wrap: break-word;
}
.search-btn {
top: 0;
width: auto;
@ -419,3 +419,10 @@ table.table > tbody > tr.reports-row > td {
td.report-action-column {
min-width: 150px;
}
@media (min-width: 992px) and (max-width: 1199px) {
/* Collapse navbar search form so it doesn't wrap to a new line */
.search-container {
width: 400px;
}
}

View File

@ -0,0 +1,34 @@
{% extends "layout.html" %}
{% block title %}Admin Log :: {{ config.SITE_NAME }}{% endblock %}
{% block body %}
{% from "_formhelpers.html" import render_field %}
<div class="table-responsive">
<table class="table table-bordered table-hover table-striped">
<thead>
<tr>
<th>#</th>
<th>Moderator/Admin</th>
<th>Log</th>
<th>Date</th>
</tr>
</thead>
<tbody>
{% for log in adminlog.items %}
<tr>
<td>{{ log.id }}</td>
<td>
<a href="{{ url_for('view_user', user_name=log.admin.username) }}">{{ log.admin.username }}</a>
</td>
<td><div markdown-text>{{ log.log }}</div></td>
<td>{{ log.created_time }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class=pagination>
{% from "bootstrap/pagination.html" import render_pagination %}
{{ render_pagination(adminlog) }}
</div>
{% endblock %}

View File

@ -89,7 +89,16 @@
<li><a href="//{{ config.EXTERNAL_URLS['main'] }}">Fun</a></li>
{% endif %}
{% if g.user.is_moderator %}
<li><a href="{{ url_for('view_reports') }}">Reports</a> </li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
Admin
<span class="caret"></span>
</a>
<ul class="dropdown-menu">
<li {% if request.path == url_for('view_reports') %}class="active"{% endif %}><a href="{{ url_for('view_reports') }}">Reports</a></li>
<li {% if request.path == url_for('view_adminlog') %}class="active"{% endif %}><a href="{{ url_for('view_adminlog') }}">Log</a></li>
</ul>
</li>
{% endif %}
</ul>