mirror of
https://gitlab.com/SIGBUS/nyaa.git
synced 2024-12-22 19:00:07 +00:00
[DB Changes!] Merge branch 'nyaazi-reports' (#146)
Adds reporting functionality. Alembic migration included.
This commit is contained in:
commit
7b2bfc57ee
0
migrations/README
Executable file → Normal file
0
migrations/README
Executable file → Normal file
0
migrations/env.py
Executable file → Normal file
0
migrations/env.py
Executable file → Normal file
0
migrations/script.py.mako
Executable file → Normal file
0
migrations/script.py.mako
Executable file → Normal file
56
migrations/versions/7f064e009cab_add_report_table.py
Normal file
56
migrations/versions/7f064e009cab_add_report_table.py
Normal 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 ###
|
|
@ -7,7 +7,8 @@ import os
|
||||||
import re
|
import re
|
||||||
from flask_wtf import FlaskForm
|
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
|
||||||
from wtforms.validators import DataRequired, Optional, Email, Length, EqualTo, ValidationError
|
from wtforms.validators import DataRequired, Optional, Email, Length, EqualTo, ValidationError
|
||||||
from wtforms.validators import Regexp
|
from wtforms.validators import Regexp
|
||||||
|
|
||||||
|
@ -295,6 +296,21 @@ class TorrentFileData(object):
|
||||||
# https://wiki.theory.org/BitTorrentSpecification#Metainfo_File_Structure
|
# 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):
|
def _validate_trackers(torrent_dict, tracker_to_check_for=None):
|
||||||
announce = torrent_dict.get('announce')
|
announce = torrent_dict.get('announce')
|
||||||
announce_string = _validate_bytes(announce, 'announce', test_decode='utf-8')
|
announce_string = _validate_bytes(announce, 'announce', test_decode='utf-8')
|
||||||
|
|
70
nyaa/models.py
Executable file → Normal file
70
nyaa/models.py
Executable file → Normal file
|
@ -580,6 +580,65 @@ class User(db.Model):
|
||||||
return self.level >= UserLevelType.TRUSTED
|
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
|
# Actually declare our site-specific classes
|
||||||
|
|
||||||
# Torrent
|
# Torrent
|
||||||
|
@ -672,6 +731,15 @@ class SukebeiComment(CommentBase, db.Model):
|
||||||
__flavor__ = 'Sukebei'
|
__flavor__ = 'Sukebei'
|
||||||
|
|
||||||
|
|
||||||
|
# Report
|
||||||
|
class NyaaReport(ReportBase, db.Model):
|
||||||
|
__flavor__ = 'Nyaa'
|
||||||
|
|
||||||
|
|
||||||
|
class SukebeiReport(ReportBase, db.Model):
|
||||||
|
__flavor__ = 'Sukebei'
|
||||||
|
|
||||||
|
|
||||||
# Choose our defaults for models.Torrent etc
|
# Choose our defaults for models.Torrent etc
|
||||||
if app.config['SITE_FLAVOR'] == 'nyaa':
|
if app.config['SITE_FLAVOR'] == 'nyaa':
|
||||||
Torrent = NyaaTorrent
|
Torrent = NyaaTorrent
|
||||||
|
@ -682,6 +750,7 @@ if app.config['SITE_FLAVOR'] == 'nyaa':
|
||||||
MainCategory = NyaaMainCategory
|
MainCategory = NyaaMainCategory
|
||||||
SubCategory = NyaaSubCategory
|
SubCategory = NyaaSubCategory
|
||||||
Comment = NyaaComment
|
Comment = NyaaComment
|
||||||
|
Report = NyaaReport
|
||||||
|
|
||||||
TorrentNameSearch = NyaaTorrentNameSearch
|
TorrentNameSearch = NyaaTorrentNameSearch
|
||||||
elif app.config['SITE_FLAVOR'] == 'sukebei':
|
elif app.config['SITE_FLAVOR'] == 'sukebei':
|
||||||
|
@ -693,5 +762,6 @@ elif app.config['SITE_FLAVOR'] == 'sukebei':
|
||||||
MainCategory = SukebeiMainCategory
|
MainCategory = SukebeiMainCategory
|
||||||
SubCategory = SukebeiSubCategory
|
SubCategory = SukebeiSubCategory
|
||||||
Comment = SukebeiComment
|
Comment = SukebeiComment
|
||||||
|
Report = SukebeiReport
|
||||||
|
|
||||||
TorrentNameSearch = SukebeiTorrentNameSearch
|
TorrentNameSearch = SukebeiTorrentNameSearch
|
||||||
|
|
|
@ -29,7 +29,6 @@ from email.utils import formatdate
|
||||||
|
|
||||||
from flask_paginate import Pagination
|
from flask_paginate import Pagination
|
||||||
|
|
||||||
|
|
||||||
DEBUG_API = False
|
DEBUG_API = False
|
||||||
DEFAULT_MAX_SEARCH_RESULT = 1000
|
DEFAULT_MAX_SEARCH_RESULT = 1000
|
||||||
DEFAULT_PER_PAGE = 75
|
DEFAULT_PER_PAGE = 75
|
||||||
|
@ -675,11 +674,13 @@ def view_torrent(torrent_id):
|
||||||
if torrent.filelist:
|
if torrent.filelist:
|
||||||
files = json.loads(torrent.filelist.filelist_blob.decode('utf-8'))
|
files = json.loads(torrent.filelist.filelist_blob.decode('utf-8'))
|
||||||
|
|
||||||
|
report_form = forms.ReportForm()
|
||||||
return flask.render_template('view.html', torrent=torrent,
|
return flask.render_template('view.html', torrent=torrent,
|
||||||
files=files,
|
files=files,
|
||||||
comment_form=comment_form,
|
comment_form=comment_form,
|
||||||
comments=torrent.comments,
|
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'])
|
@app.route('/view/<int:torrent_id>/comment/<int:comment_id>/delete', methods=['POST'])
|
||||||
|
@ -798,6 +799,66 @@ def download_torrent(torrent_id):
|
||||||
return resp
|
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):
|
def _get_cached_torrent_file(torrent):
|
||||||
# Note: obviously temporary
|
# Note: obviously temporary
|
||||||
cached_torrent = os.path.join(app.config['BASE_DIR'],
|
cached_torrent = os.path.join(app.config['BASE_DIR'],
|
||||||
|
|
0
nyaa/static/css/main.css
Executable file → Normal file
0
nyaa/static/css/main.css
Executable file → Normal file
0
nyaa/templates/edit.html
Executable file → Normal file
0
nyaa/templates/edit.html
Executable file → Normal file
5
nyaa/templates/layout.html
Executable file → Normal file
5
nyaa/templates/layout.html
Executable file → Normal file
|
@ -88,6 +88,9 @@
|
||||||
{% elif config.SITE_FLAVOR == 'sukebei' %}
|
{% elif config.SITE_FLAVOR == 'sukebei' %}
|
||||||
<li><a href="https://nyaa.si/">Fun</a></li>
|
<li><a href="https://nyaa.si/">Fun</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if g.user.is_moderator %}
|
||||||
|
<li><a href="{{ url_for('view_reports') }}">Reports</a> </li>
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<ul class="nav navbar-nav navbar-right">
|
<ul class="nav navbar-nav navbar-right">
|
||||||
|
@ -304,5 +307,3 @@
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|
||||||
|
|
48
nyaa/templates/reports.html
Normal file
48
nyaa/templates/reports.html
Normal 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
0
nyaa/templates/upload.html
Executable file → Normal file
|
@ -71,8 +71,12 @@
|
||||||
<div class="col-md-5"><kbd>{{ torrent.info_hash_as_hex }}</kbd></div>
|
<div class="col-md-5"><kbd>{{ torrent.info_hash_as_hex }}</kbd></div>
|
||||||
</div>
|
</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>
|
{% 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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -180,6 +184,29 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</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">×</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>
|
<script>
|
||||||
var target = document.getElementById('torrent-description');
|
var target = document.getElementById('torrent-description');
|
||||||
var text = target.innerHTML;
|
var text = target.innerHTML;
|
||||||
|
|
Loading…
Reference in a new issue