mirror of
https://gitlab.com/SIGBUS/nyaa.git
synced 2024-12-22 14:40:00 +00:00
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:
parent
1cee6cb647
commit
dd8cb4757e
44
migrations/versions/1add911660a6_admin_log_added.py
Normal file
44
migrations/versions/1add911660a6_admin_log_added.py
Normal 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 ###
|
|
@ -10,7 +10,8 @@ from flask_wtf import FlaskForm
|
||||||
from flask_wtf.file import FileField, FileRequired
|
from flask_wtf.file import FileField, FileRequired
|
||||||
from wtforms import StringField, PasswordField, BooleanField, TextAreaField, SelectField,\
|
from wtforms import StringField, PasswordField, BooleanField, TextAreaField, SelectField,\
|
||||||
HiddenField
|
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
|
from wtforms.validators import Regexp
|
||||||
|
|
||||||
# For DisabledSelectField
|
# For DisabledSelectField
|
||||||
|
@ -49,6 +50,7 @@ def stop_on_validation_error(f):
|
||||||
raise StopValidation(*e.args) from e
|
raise StopValidation(*e.args) from e
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
_username_validator = Regexp(
|
_username_validator = Regexp(
|
||||||
r'^[a-zA-Z0-9_\-]+$',
|
r'^[a-zA-Z0-9_\-]+$',
|
||||||
message='Your username must only consist of alphanumerics and _- (a-zA-Z0-9_-)')
|
message='Your username must only consist of alphanumerics and _- (a-zA-Z0-9_-)')
|
||||||
|
|
|
@ -563,6 +563,38 @@ class User(db.Model):
|
||||||
return self.level >= UserLevelType.TRUSTED
|
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):
|
class ReportStatus(IntEnum):
|
||||||
IN_REVIEW = 0
|
IN_REVIEW = 0
|
||||||
VALID = 1
|
VALID = 1
|
||||||
|
@ -714,6 +746,15 @@ class SukebeiComment(CommentBase, db.Model):
|
||||||
__flavor__ = 'Sukebei'
|
__flavor__ = 'Sukebei'
|
||||||
|
|
||||||
|
|
||||||
|
# AdminLog
|
||||||
|
class NyaaAdminLog(AdminLogBase, db.Model):
|
||||||
|
__flavor__ = 'Nyaa'
|
||||||
|
|
||||||
|
|
||||||
|
class SukebeiAdminLog(AdminLogBase, db.Model):
|
||||||
|
__flavor__ = 'Sukebei'
|
||||||
|
|
||||||
|
|
||||||
# Report
|
# Report
|
||||||
class NyaaReport(ReportBase, db.Model):
|
class NyaaReport(ReportBase, db.Model):
|
||||||
__flavor__ = 'Nyaa'
|
__flavor__ = 'Nyaa'
|
||||||
|
@ -733,9 +774,10 @@ if app.config['SITE_FLAVOR'] == 'nyaa':
|
||||||
MainCategory = NyaaMainCategory
|
MainCategory = NyaaMainCategory
|
||||||
SubCategory = NyaaSubCategory
|
SubCategory = NyaaSubCategory
|
||||||
Comment = NyaaComment
|
Comment = NyaaComment
|
||||||
|
AdminLog = NyaaAdminLog
|
||||||
Report = NyaaReport
|
Report = NyaaReport
|
||||||
|
|
||||||
TorrentNameSearch = NyaaTorrentNameSearch
|
TorrentNameSearch = NyaaTorrentNameSearch
|
||||||
|
|
||||||
elif app.config['SITE_FLAVOR'] == 'sukebei':
|
elif app.config['SITE_FLAVOR'] == 'sukebei':
|
||||||
Torrent = SukebeiTorrent
|
Torrent = SukebeiTorrent
|
||||||
TorrentFilelist = SukebeiTorrentFilelist
|
TorrentFilelist = SukebeiTorrentFilelist
|
||||||
|
@ -745,6 +787,6 @@ elif app.config['SITE_FLAVOR'] == 'sukebei':
|
||||||
MainCategory = SukebeiMainCategory
|
MainCategory = SukebeiMainCategory
|
||||||
SubCategory = SukebeiSubCategory
|
SubCategory = SukebeiSubCategory
|
||||||
Comment = SukebeiComment
|
Comment = SukebeiComment
|
||||||
|
AdminLog = SukebeiAdminLog
|
||||||
Report = SukebeiReport
|
Report = SukebeiReport
|
||||||
|
|
||||||
TorrentNameSearch = SukebeiTorrentNameSearch
|
TorrentNameSearch = SukebeiTorrentNameSearch
|
||||||
|
|
|
@ -325,20 +325,26 @@ def view_user(user_name):
|
||||||
if flask.request.method == 'GET':
|
if flask.request.method == 'GET':
|
||||||
admin_form.user_class.data = default
|
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():
|
if flask.request.method == 'POST' and admin_form and admin_form.validate():
|
||||||
selection = admin_form.user_class.data
|
selection = admin_form.user_class.data
|
||||||
|
log = None
|
||||||
if selection == 'regular':
|
if selection == 'regular':
|
||||||
user.level = models.UserLevelType.REGULAR
|
user.level = models.UserLevelType.REGULAR
|
||||||
|
log = "[{}]({}) changed to regular user".format(user_name, url)
|
||||||
elif selection == 'trusted':
|
elif selection == 'trusted':
|
||||||
user.level = models.UserLevelType.TRUSTED
|
user.level = models.UserLevelType.TRUSTED
|
||||||
|
log = "[{}]({}) changed to trusted user".format(user_name, url)
|
||||||
elif selection == 'moderator':
|
elif selection == 'moderator':
|
||||||
user.level = models.UserLevelType.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(user)
|
||||||
|
db.session.add(adminlog)
|
||||||
db.session.commit()
|
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]
|
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.delete(comment)
|
||||||
db.session.flush()
|
db.session.flush()
|
||||||
torrent.update_comment_count()
|
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()
|
db.session.commit()
|
||||||
|
|
||||||
flask.flash('Comment successfully deleted.', 'success')
|
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'])
|
@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:
|
if flask.g.user.is_moderator:
|
||||||
torrent.deleted = form.is_deleted.data
|
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()
|
db.session.commit()
|
||||||
|
|
||||||
flask.flash(flask.Markup(
|
flask.flash(flask.Markup(
|
||||||
'Torrent has been successfully edited! Changes might take a few minutes to show up.'),
|
'Torrent has been successfully edited! Changes might take a few minutes to show up.'),
|
||||||
'info')
|
'info')
|
||||||
|
|
||||||
return flask.redirect(flask.url_for('view_torrent', torrent_id=torrent.id))
|
return flask.redirect(url)
|
||||||
else:
|
else:
|
||||||
if flask.request.method != 'POST':
|
if flask.request.method != 'POST':
|
||||||
# Fill form data only if the POST didn't fail
|
# 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))
|
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'])
|
@app.route('/reports', methods=['GET', 'POST'])
|
||||||
def view_reports():
|
def view_reports():
|
||||||
if not flask.g.user or not flask.g.user.is_moderator:
|
if not flask.g.user or not flask.g.user.is_moderator:
|
||||||
|
|
|
@ -160,7 +160,7 @@ table.torrent-list tbody .comments i {
|
||||||
-ms-flex-order: 3;
|
-ms-flex-order: 3;
|
||||||
order: 3;
|
order: 3;
|
||||||
width: 99%;
|
width: 99%;
|
||||||
padding-right: 4em;
|
padding-right: 1em;
|
||||||
border-left: 0;
|
border-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -380,7 +380,7 @@ h6:hover .header-anchor {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 991px){
|
@media (max-width: 991px) {
|
||||||
.panel-body .col-md-5 {
|
.panel-body .col-md-5 {
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
@ -397,7 +397,7 @@ h6:hover .header-anchor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 992px){
|
@media (min-width: 992px) {
|
||||||
.panel-body .col-md-5 {
|
.panel-body .col-md-5 {
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
@ -419,3 +419,10 @@ table.table > tbody > tr.reports-row > td {
|
||||||
td.report-action-column {
|
td.report-action-column {
|
||||||
min-width: 150px;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
34
nyaa/templates/adminlog.html
Normal file
34
nyaa/templates/adminlog.html
Normal 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 %}
|
|
@ -89,7 +89,16 @@
|
||||||
<li><a href="//{{ config.EXTERNAL_URLS['main'] }}">Fun</a></li>
|
<li><a href="//{{ config.EXTERNAL_URLS['main'] }}">Fun</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if g.user.is_moderator %}
|
{% 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 %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue