[DB Changes!] Merge branch 'nyaazi-reports' (#146)

Adds reporting functionality.
Alembic migration included.
This commit is contained in:
TheAMM 2017-05-29 17:09:12 +03:00
commit 7b2bfc57ee
14 changed files with 286 additions and 7 deletions

0
WSGI.py Executable file → Normal file
View File

0
migrations/README Executable file → Normal file
View File

0
migrations/env.py Executable file → Normal file
View File

0
migrations/script.py.mako Executable file → Normal file
View File

View File

@ -0,0 +1,56 @@
"""Add Report table
Revision ID: 7f064e009cab
Revises: 2bceb2cb4d7c
Create Date: 2017-05-29 16:50:28.720980
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '7f064e009cab'
down_revision = '2bceb2cb4d7c'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('nyaa_reports',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('created_time', sa.DateTime(), nullable=True),
sa.Column('reason', sa.String(length=255), nullable=False),
# sqlalchemy_utils.types.choice.ChoiceType()
sa.Column('status', sa.Integer(), nullable=False),
sa.Column('torrent_id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['torrent_id'], ['nyaa_torrents.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('sukebei_reports',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('created_time', sa.DateTime(), nullable=True),
sa.Column('reason', sa.String(length=255), nullable=False),
# sqlalchemy_utils.types.choice.ChoiceType()
sa.Column('status', sa.Integer(), nullable=False),
sa.Column('torrent_id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['torrent_id'], ['sukebei_torrents.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('sukebei_reports')
op.drop_table('nyaa_reports')
# ### end Alembic commands ###

View File

@ -7,7 +7,8 @@ import os
import re
from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileRequired
from wtforms import StringField, PasswordField, BooleanField, TextAreaField, SelectField
from wtforms import StringField, PasswordField, BooleanField, TextAreaField, SelectField,\
HiddenField
from wtforms.validators import DataRequired, Optional, Email, Length, EqualTo, ValidationError
from wtforms.validators import Regexp
@ -295,6 +296,21 @@ class TorrentFileData(object):
# https://wiki.theory.org/BitTorrentSpecification#Metainfo_File_Structure
class ReportForm(FlaskForm):
reason = TextAreaField('Report reason', [
Length(min=3, max=255,
message='Report reason must be at least %(min)d characters long '
'and %(max)d at most.'),
DataRequired('You must provide a valid report reason.')
])
class ReportActionForm(FlaskForm):
action = SelectField(choices=[('close', 'Close'), ('hide', 'Hide'), ('delete', 'Delete')])
torrent = HiddenField()
report = HiddenField()
def _validate_trackers(torrent_dict, tracker_to_check_for=None):
announce = torrent_dict.get('announce')
announce_string = _validate_bytes(announce, 'announce', test_decode='utf-8')

70
nyaa/models.py Executable file → Normal file
View File

@ -580,6 +580,65 @@ class User(db.Model):
return self.level >= UserLevelType.TRUSTED
class ReportStatus(IntEnum):
IN_REVIEW = 0
VALID = 1
INVALID = 2
class ReportBase(DeclarativeHelperBase):
__tablename_base__ = 'reports'
id = db.Column(db.Integer, primary_key=True)
created_time = db.Column(db.DateTime(timezone=False), default=datetime.utcnow)
reason = db.Column(db.String(length=255), nullable=False)
status = db.Column(ChoiceType(ReportStatus, impl=db.Integer()), nullable=False)
@declarative.declared_attr
def torrent_id(cls):
return db.Column(db.Integer, db.ForeignKey(
cls._table_prefix('torrents.id'), ondelete='CASCADE'), nullable=False)
@declarative.declared_attr
def user_id(cls):
return db.Column(db.Integer, db.ForeignKey('users.id'))
@declarative.declared_attr
def user(cls):
return db.relationship('User', uselist=False, lazy="joined")
@declarative.declared_attr
def torrent(cls):
return db.relationship(cls._flavor_prefix('Torrent'), uselist=False, lazy="joined")
def __init__(self, torrent_id, user_id, reason):
self.torrent_id = torrent_id
self.user_id = user_id
self.reason = reason
self.status = ReportStatus.IN_REVIEW
def __repr__(self):
return '<Report %r>' % self.id
@property
def created_utc_timestamp(self):
''' Returns a UTC POSIX timestamp, as seconds '''
return (self.created_time - UTC_EPOCH).total_seconds()
@classmethod
def by_id(cls, id):
return cls.query.get(id)
@classmethod
def not_reviewed(cls, page):
reports = cls.query.filter_by(status=0).paginate(page=page, per_page=20)
return reports
@classmethod
def remove_reviewed(cls, id):
return cls.query.filter(cls.torrent_id == id, cls.status == 0).delete()
# Actually declare our site-specific classes
# Torrent
@ -672,6 +731,15 @@ class SukebeiComment(CommentBase, db.Model):
__flavor__ = 'Sukebei'
# Report
class NyaaReport(ReportBase, db.Model):
__flavor__ = 'Nyaa'
class SukebeiReport(ReportBase, db.Model):
__flavor__ = 'Sukebei'
# Choose our defaults for models.Torrent etc
if app.config['SITE_FLAVOR'] == 'nyaa':
Torrent = NyaaTorrent
@ -682,6 +750,7 @@ if app.config['SITE_FLAVOR'] == 'nyaa':
MainCategory = NyaaMainCategory
SubCategory = NyaaSubCategory
Comment = NyaaComment
Report = NyaaReport
TorrentNameSearch = NyaaTorrentNameSearch
elif app.config['SITE_FLAVOR'] == 'sukebei':
@ -693,5 +762,6 @@ elif app.config['SITE_FLAVOR'] == 'sukebei':
MainCategory = SukebeiMainCategory
SubCategory = SukebeiSubCategory
Comment = SukebeiComment
Report = SukebeiReport
TorrentNameSearch = SukebeiTorrentNameSearch

View File

@ -29,7 +29,6 @@ from email.utils import formatdate
from flask_paginate import Pagination
DEBUG_API = False
DEFAULT_MAX_SEARCH_RESULT = 1000
DEFAULT_PER_PAGE = 75
@ -675,11 +674,13 @@ def view_torrent(torrent_id):
if torrent.filelist:
files = json.loads(torrent.filelist.filelist_blob.decode('utf-8'))
report_form = forms.ReportForm()
return flask.render_template('view.html', torrent=torrent,
files=files,
comment_form=comment_form,
comments=torrent.comments,
can_edit=can_edit)
can_edit=can_edit,
report_form=report_form)
@app.route('/view/<int:torrent_id>/comment/<int:comment_id>/delete', methods=['POST'])
@ -798,6 +799,66 @@ def download_torrent(torrent_id):
return resp
@app.route('/view/<int:torrent_id>/submit_report', methods=['POST'])
def submit_report(torrent_id):
if not flask.g.user:
flask.abort(403)
form = forms.ReportForm(flask.request.form)
if flask.request.method == 'POST' and form.validate():
report_reason = form.reason.data
current_user_id = flask.g.user.id
report = models.Report(
torrent_id=torrent_id,
user_id=current_user_id,
reason=report_reason)
db.session.add(report)
db.session.commit()
flask.flash('Successfully reported torrent!', 'success')
return flask.redirect(flask.url_for('view_torrent', torrent_id=torrent_id))
@app.route('/reports', methods=['GET', 'POST'])
def view_reports():
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)
reports = models.Report.not_reviewed(page)
report_action = forms.ReportActionForm(flask.request.form)
if flask.request.method == 'POST' and report_action.validate():
action = report_action.action.data
torrent_id = report_action.torrent.data
report_id = report_action.report.data
torrent = models.Torrent.by_id(torrent_id)
report = models.Report.by_id(report_id)
if not torrent or not report or report.status != 0:
flask.abort(404)
else:
if action == 'delete':
torrent.deleted = True
report.status = 1
elif action == 'hide':
torrent.hidden = True
report.status = 1
else:
report.status = 2
models.Report.remove_reviewed(torrent_id)
db.session.commit()
flask.flash('Closed report #{}'.format(report.id), 'success')
return flask.redirect(flask.url_for('view_reports'))
return flask.render_template('reports.html',
reports=reports,
report_action=report_action)
def _get_cached_torrent_file(torrent):
# Note: obviously temporary
cached_torrent = os.path.join(app.config['BASE_DIR'],

0
nyaa/static/css/main.css Executable file → Normal file
View File

0
nyaa/templates/edit.html Executable file → Normal file
View File

5
nyaa/templates/layout.html Executable file → Normal file
View File

@ -88,6 +88,9 @@
{% elif config.SITE_FLAVOR == 'sukebei' %}
<li><a href="https://nyaa.si/">Fun</a></li>
{% endif %}
{% if g.user.is_moderator %}
<li><a href="{{ url_for('view_reports') }}">Reports</a> </li>
{% endif %}
</ul>
<ul class="nav navbar-nav navbar-right">
@ -304,5 +307,3 @@
</footer>
</body>
</html>

View File

@ -0,0 +1,48 @@
{% extends "layout.html" %}
{% block title %}Reports :: {{ 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>Reported by</th>
<th>Torrent</th>
<th>Reason</th>
<th>Date</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{% for report in reports.items %}
<tr>
<td>{{ report.id }}</td>
<td>
<a href="{{ url_for('view_user', user_name=report.user.username) }}">{{ report.user.username }}</a>
</td>
<td>
<a href="{{ url_for('view_torrent', torrent_id=report.torrent.id) }}">{{ report.torrent.display_name }}</a>
</td>
<td>{{ report.reason }}</td>
<td>{{ report.created_time }}</td>
<td style="width: 15%">
<form method="post">
{{ report_action.csrf_token }}
{{ report_action.action }}
{{ report_action.torrent(value=report.torrent.id) }}
{{ report_action.report(value=report.id) }}
<button type="submit" class="btn btn-primary pull-right">Review</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class=pagination>
{% from "bootstrap/pagination.html" import render_pagination %}
{{ render_pagination(reports) }}
</div>
{% endblock %}

0
nyaa/templates/upload.html Executable file → Normal file
View File

View File

@ -71,8 +71,12 @@
<div class="col-md-5"><kbd>{{ torrent.info_hash_as_hex }}</kbd></div>
</div>
</div>
<div class="panel-footer">
<div class="panel-footer clearfix">
{% if torrent.has_torrent %}<a href="{{ url_for('download_torrent', torrent_id=torrent.id )}}"><i class="fa fa-download fa-fw"></i>Download Torrent</a> or {% endif %}<a href="{{ torrent.magnet_uri }}" class="card-footer-item"><i class="fa fa-magnet fa-fw"></i>Magnet</a>
<button type="button" class="btn btn-danger pull-right" data-toggle="modal" data-target="#reportModal">
Report
</button>
</div>
</div>
@ -180,6 +184,29 @@
{% endif %}
</div>
<div class="modal fade" id="reportModal" tabindex="-1" role="dialog" aria-labelledby="reportModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span
aria-hidden="true">&times;</span></button>
<h4 class="modal-title">Report torrent #{{ torrent.id }}</h4>
</div>
<div class="modal-body">
<form method="POST" action="{{ request.url }}/submit_report">
{{ report_form.csrf_token }}
{{ render_field(report_form.reason, class_='form-control', maxlength=255) }}
<div style="float: right;">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="submit" class="btn btn-danger">Report</button>
</div>
</form>
</div>
<div class="modal-footer" style="border-top: none;">
</div>
</div>
</div>
<script>
var target = document.getElementById('torrent-description');
var text = target.innerHTML;
@ -189,4 +216,4 @@
target.innerHTML = writer.render(parsed);
</script>
{% endblock %}
{% endblock %}