1
0
Fork 0
mirror of https://gitlab.com/SIGBUS/nyaa.git synced 2024-12-22 14:00: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:
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 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_-)')

View file

@ -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

View file

@ -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:

View file

@ -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;
}
}

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> <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>