1
0
Fork 0
mirror of https://gitlab.com/SIGBUS/nyaa.git synced 2024-12-22 08:49:59 +00:00

Add trusted application functionality (#533)

* Add trusted application functionality

This lets users apply for trusted status, given certain minimum
requirements. Moderators can then review the applications, giving
a recommendation, and administrators can accept or reject them.

If an application is accepted or rejected, the user receives an
e-mail about it.

Markdown images are not rendered in applications to prevent browsers
from sending automatic requests to untrusted webservers.

Users who have had their application rejected cannot re-apply for a set
amount of days.

* minor fixes
This commit is contained in:
Nicolas F 2019-08-11 03:18:44 +02:00 committed by Arylide
parent ff44d7a51c
commit 16814d6eb7
18 changed files with 580 additions and 4 deletions

View file

@ -176,3 +176,15 @@ EDITING_TIME_LIMIT = 0
# Whether to use Gravatar or just always use the default avatar # Whether to use Gravatar or just always use the default avatar
# (Useful if run as development instance behind NAT/firewall) # (Useful if run as development instance behind NAT/firewall)
ENABLE_GRAVATAR = True ENABLE_GRAVATAR = True
##########################
## Trusted Requirements ##
##########################
# Minimum number of uploads the user needs to have in order to apply for trusted
TRUSTED_MIN_UPLOADS = 10
# Minimum number of cumulative downloads the user needs to have across their
# torrents in order to apply for trusted
TRUSTED_MIN_DOWNLOADS = 10000
# Number of days an applicant needs to wait before re-applying
TRUSTED_REAPPLY_COOLDOWN = 90

View file

@ -0,0 +1,47 @@
"""Add trusted applications
Revision ID: 5cbcee17bece
Revises: 8a6a7662eb37
Create Date: 2018-11-05 15:16:07.497898
"""
from alembic import op
import sqlalchemy as sa
import sqlalchemy_utils
# revision identifiers, used by Alembic.
revision = '5cbcee17bece'
down_revision = '8a6a7662eb37'
branch_labels = None
depends_on = None
def upgrade():
op.create_table('trusted_applications',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('submitter_id', sa.Integer(), nullable=False, index=True),
sa.Column('created_time', sa.DateTime(), nullable=True),
sa.Column('closed_time', sa.DateTime(), nullable=True),
sa.Column('why_want', sa.String(length=4000), nullable=False),
sa.Column('why_give', sa.String(length=4000), nullable=False),
sa.Column('status', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['submitter_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('trusted_reviews',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('reviewer_id', sa.Integer(), nullable=False),
sa.Column('app_id', sa.Integer(), nullable=False),
sa.Column('created_time', sa.DateTime(), nullable=True),
sa.Column('comment', sa.String(length=4000), nullable=False),
sa.Column('recommendation', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['app_id'], ['trusted_applications.id'], ),
sa.ForeignKeyConstraint(['reviewer_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
def downgrade():
op.drop_table('trusted_reviews')
op.drop_table('trusted_applications')

View file

@ -485,6 +485,33 @@ class ReportActionForm(FlaskForm):
report = HiddenField() report = HiddenField()
class TrustedForm(FlaskForm):
why_give_trusted = TextAreaField('Why do you think you should be given trusted status?', [
Length(min=32, max=4000,
message='Please explain why you think you should be given trusted status in at '
'least %(min)d but less than %(max)d characters.'),
DataRequired('Please fill out all of the fields in the form.')
])
why_want_trusted = TextAreaField('Why do you want to become a trusted user?', [
Length(min=32, max=4000,
message='Please explain why you want to become a trusted user in at least %(min)d '
'but less than %(max)d characters.'),
DataRequired('Please fill out all of the fields in the form.')
])
class TrustedReviewForm(FlaskForm):
comment = TextAreaField('Comment',
[Length(min=8, max=4000, message='Please provide a comment')])
recommendation = SelectField(choices=[('abstain', 'Abstain'), ('reject', 'Reject'),
('accept', 'Accept')])
class TrustedDecisionForm(FlaskForm):
accept = SubmitField('Accept')
reject = SubmitField('Reject')
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')
assert announce is not None, 'no tracker in torrent' assert announce is not None, 'no tracker in torrent'

View file

@ -11,8 +11,9 @@ from urllib.parse import urlencode
import flask import flask
from markupsafe import escape as escape_markup from markupsafe import escape as escape_markup
from sqlalchemy import ForeignKeyConstraint, Index from sqlalchemy import ForeignKeyConstraint, Index, func
from sqlalchemy.ext import declarative from sqlalchemy.ext import declarative
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy_fulltext import FullText from sqlalchemy_fulltext import FullText
from sqlalchemy_utils import ChoiceType, EmailType, PasswordType from sqlalchemy_utils import ChoiceType, EmailType, PasswordType
@ -642,6 +643,24 @@ class User(db.Model):
''' Returns a UTC POSIX timestamp, as seconds ''' ''' Returns a UTC POSIX timestamp, as seconds '''
return (self.created_time - UTC_EPOCH).total_seconds() return (self.created_time - UTC_EPOCH).total_seconds()
@property
def satisfies_trusted_reqs(self):
num_total = 0
downloads_total = 0
for ts_flavor, t_flavor in ((NyaaStatistic, NyaaTorrent),
(SukebeiStatistic, SukebeiTorrent)):
uploads = db.session.query(func.count(t_flavor.id)).\
filter(t_flavor.user == self).\
filter(t_flavor.flags.op('&')(int(TorrentFlags.REMAKE)).is_(False)).scalar()
dls = db.session.query(func.sum(ts_flavor.download_count)).\
join(t_flavor).\
filter(t_flavor.user == self).\
filter(t_flavor.flags.op('&')(int(TorrentFlags.REMAKE)).is_(False)).scalar()
num_total += uploads or 0
downloads_total += dls or 0
return (num_total >= config['TRUSTED_MIN_UPLOADS'] and
downloads_total >= config['TRUSTED_MIN_DOWNLOADS'])
class UserPreferences(db.Model): class UserPreferences(db.Model):
__tablename__ = 'user_preferences' __tablename__ = 'user_preferences'
@ -845,6 +864,77 @@ class RangeBan(db.Model):
return q.count() > 0 return q.count() > 0
class TrustedApplicationStatus(IntEnum):
# If you change these, don't forget to change is_closed in TrustedApplication
NEW = 0
REVIEWED = 1
ACCEPTED = 2
REJECTED = 3
class TrustedApplication(db.Model):
__tablename__ = 'trusted_applications'
id = db.Column(db.Integer, primary_key=True)
submitter_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False, index=True)
created_time = db.Column(db.DateTime(timezone=False), default=datetime.utcnow)
closed_time = db.Column(db.DateTime(timezone=False))
why_want = db.Column(db.String(length=4000), nullable=False)
why_give = db.Column(db.String(length=4000), nullable=False)
status = db.Column(ChoiceType(TrustedApplicationStatus, impl=db.Integer()), nullable=False,
default=TrustedApplicationStatus.NEW)
reviews = db.relationship('TrustedReview', backref='trusted_applications')
submitter = db.relationship('User', uselist=False, lazy='joined', foreign_keys=[submitter_id])
@hybrid_property
def is_closed(self):
# We can't use the attribute names from TrustedApplicationStatus in an or here because of
# SQLAlchemy jank. It'll generate the wrong query.
return self.status > 1
@hybrid_property
def is_new(self):
return self.status == TrustedApplicationStatus.NEW
@hybrid_property
def is_reviewed(self):
return self.status == TrustedApplicationStatus.REVIEWED
@hybrid_property
def is_rejected(self):
return self.status == TrustedApplicationStatus.REJECTED
@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)
class TrustedRecommendation(IntEnum):
ACCEPT = 0
REJECT = 1
ABSTAIN = 2
class TrustedReview(db.Model):
__tablename__ = 'trusted_reviews'
id = db.Column(db.Integer, primary_key=True)
reviewer_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
app_id = db.Column(db.Integer, db.ForeignKey('trusted_applications.id'), nullable=False)
created_time = db.Column(db.DateTime(timezone=False), default=datetime.utcnow)
comment = db.Column(db.String(length=4000), nullable=False)
recommendation = db.Column(ChoiceType(TrustedRecommendation, impl=db.Integer()),
nullable=False)
reviewer = db.relationship('User', uselist=False, lazy='joined', foreign_keys=[reviewer_id])
application = db.relationship('TrustedApplication', uselist=False, lazy='joined',
foreign_keys=[app_id])
# Actually declare our site-specific classes # Actually declare our site-specific classes
# Torrent # Torrent

View file

@ -448,6 +448,9 @@ h6:hover .header-anchor {
visibility: visible; visibility: visible;
display: inline-block; display: inline-block;
} }
.trusted-form textarea {
height:12em;
}
/* Dark theme */ /* Dark theme */

View file

@ -251,6 +251,11 @@ document.addEventListener("DOMContentLoaded", function() {
var target = markdownTargets[i]; var target = markdownTargets[i];
var rendered; var rendered;
var markdownSource = htmlDecode(target.innerHTML); var markdownSource = htmlDecode(target.innerHTML);
if (target.attributes["markdown-no-images"]) {
markdown.disable('image');
} else {
markdown.enable('image');
}
if (target.attributes["markdown-text-inline"]) { if (target.attributes["markdown-text-inline"]) {
rendered = markdown.renderInline(markdownSource); rendered = markdown.renderInline(markdownSource);
} else { } else {

View file

@ -113,7 +113,7 @@
</div> </div>
{% endmacro %} {% endmacro %}
{% macro render_menu_with_button(field) %} {% macro render_menu_with_button(field, button_label='Apply') %}
{% if field.errors %} {% if field.errors %}
<div class="form-group has-error"> <div class="form-group has-error">
{% else %} {% else %}
@ -123,7 +123,7 @@
<div class="input-group input-group-sm"> <div class="input-group input-group-sm">
{{ field(title=field.description, class_="form-control",**kwargs) | safe }} {{ field(title=field.description, class_="form-control",**kwargs) | safe }}
<div class="input-group-btn"> <div class="input-group-btn">
<button type="submit" class="btn btn-primary">Apply</button> <button type="submit" class="btn btn-primary">{{ button_label }}</button>
</div> </div>
</div> </div>
{% if field.errors %} {% if field.errors %}

View file

@ -0,0 +1,54 @@
{% extends "layout.html" %}
{% macro render_filter_tab(name) %}
<li class="nav-item{% if list_filter == name %} active{% endif %}">
<a class="nav-link{% if list_filter == name %} active{% endif %}" href="{{ url_for('admin.trusted', list_filter=name) }}">
{% if name %}
{{ name.capitalize() }}
{% else %}
Open
{% endif %}
</a>
</li>
{% endmacro %}
{% block title %}Trusted Applications :: {{ config.SITE_NAME }}{% endblock %}
{% block body %}
<ul class="nav nav-tabs" role="tablist">
{{ render_filter_tab(None) }}
{{ render_filter_tab('new') }}
{{ render_filter_tab('reviewed') }}
{{ render_filter_tab('closed') }}
</ul>
<div class="table">
<table class="table table-bordered table-hover table-striped table-condensed">
<caption>List of {{ list_filter or 'open' }} applications</caption>
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Submitter</th>
<th scope="col">Submitted on</th>
<th scope="col">Status</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
{% for app in apps.items %}
<tr class="reports-row">
<td>{{ app.id }}</td>
<td>
<a href="{{ url_for('users.view_user', user_name=app.submitter.username) }}">
{{ app.submitter.username }}
</a>
</td>
<td data-timestamp="{{ app.created_utc_timestamp | int }}">{{ app.created_time.strftime('%Y-%m-%d %H:%M') }}</td>
<td>{{ app.status.name.capitalize() }}</td>
<td><a class="btn btn-primary btn-sm" style="width:100%" href="{{ url_for('admin.trusted_application', app_id=app.id) }}" role="button">View</a></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class=pagination>
{% from "bootstrap/pagination.html" import render_pagination %}
{{ render_pagination(apps) }}
</div>
{% endblock %}

View file

@ -0,0 +1,114 @@
{% extends "layout.html" %}
{% from "_formhelpers.html" import render_field, render_menu_with_button %}
{%- macro review_class(rec) -%}
{%- if rec.name == 'ACCEPT' -%}
{{ 'panel-success' -}}
{%- elif rec.name == 'REJECT' -%}
{{ 'panel-danger' -}}
{%- elif rec.name == 'ABSTAIN' -%}
{{ 'panel-default' -}}
{%- endif -%}
{%- endmacro -%}
{% block title %}{{ app.submitter.username }}'s Application :: {{ config.SITE_NAME }}{% endblock %}
{% block body %}
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">{{ app.submitter.username }}'s Application</h3>
</div>
<div class="panel-body">
<div class="row">
<dl>
<div class="col-xs-4 col-sm-2 col-md-2">
<dt>Submitter</dt>
<dd>
<a href="{{ url_for('users.view_user', user_name=app.submitter.username) }}">
{{ app.submitter.username }}
</a>
</dd>
</div>
<div class="col-xs-4 col-sm-2 col-md-2">
<dt>Submitted on</dt>
<dd data-timestamp="{{ app.created_utc_timestamp | int }}">
{{ app.created_time.strftime('%Y-%m-%d %H:%M') }}
</dd>
</div>
<div class="col-xs-4 col-sm-2 col-md-2">
<dt>Status</dt>
<dd>{{ app.status.name.capitalize() }}</dd>
</div>
</dl>
</div>
<hr>
<div class="row">
<div class="col-md-12">
<h4>Why do you think you should be given trusted status?</h4>
<div class="panel panel-default">
<div class="panel-body" markdown-text markdown-no-images>
{{- app.why_give | escape | replace('\r\n', '\n') | replace('\n', '&#10;'|safe) -}}
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<h4>Why do you want to become a trusted user?</h4>
<div class="panel panel-default">
<div class="panel-body" markdown-text markdown-no-images>
{{- app.why_want | escape | replace('\r\n', '\n') | replace('\n', '&#10;'|safe) -}}
</div>
</div>
</div>
</div>
</div>
{%- if decision_form -%}
<div class="panel-footer">
<form method="POST">
{{ decision_form.csrf_token }}
<div class="btn-group" role="group" aria-label="Decision">
{{ decision_form.reject(class="btn btn-danger") }}
{{ decision_form.accept(class="btn btn-success") }}
</div>
</form>
</div>
{%- endif -%}
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Reviews - {{ app.reviews | length }}</h3>
</div>
<div class="panel-body">
{% for rev in app.reviews %}
<div class="panel {{ review_class(rev.recommendation) -}}">
<div class="panel-heading">
<h3 class="panel-title">{{ rev.reviewer.username }}'s Review</h3>
</div>
<div class="panel-body">
<div markdown-text>
{{- rev.comment | escape | replace('\r\n', '\n') | replace('\n', '&#10;'|safe) -}}
</div>
</div>
<div class="panel-footer">
{%- if rev.recommendation.name == 'ABSTAIN' -%}
{{ rev.reviewer.username }} does not give an explicit recommendation.
{%- else -%}
{{ rev.reviewer.username }} recommends to <strong>{{ rev.recommendation.name.lower() }}</strong> this application.
{%- endif -%}
</div>
</div>
{% endfor %}
<form method="POST">
{{ review_form.csrf_token }}
<div class="row">
<div class="col-xs-12 col-md-8 col-sm-10">
{{ render_field(review_form.comment, class_="form-control") }}
</div>
</div>
<div class="row">
<div class="col-xs-3">
{{ render_menu_with_button(review_form.recommendation, 'Submit') }}
</div>
</div>
</form>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,14 @@
<html>
<head>
<title>Your {{ config.GLOBAL_SITE_NAME }} Trusted Application was {{ 'accepted' if is_accepted else 'rejected' }}</title>
</head>
<body>
{% if is_accepted %}
<p>Congratulations! Your Trusted status application on {{ config.GLOBAL_SITE_NAME }} was accepted. You can now edit your torrents and set the Trusted flag on them.</p>
{% else %}
<p>We're sorry to inform you that we've rejected your Trusted status application on {{ config.GLOBAL_SITE_NAME }}. You can re-apply for Trusted status in {{ config.TRUSTED_REAPPLY_COOLDOWN }} days if you wish to do so.</p>
{% endif %}
<p>Regards<br/>
The {{ config.GLOBAL_SITE_NAME }} Moderation Team</p>
</body>
</html>

View file

@ -0,0 +1,8 @@
{% if is_accepted %}
Congratulations! Your Trusted status application on {{ config.GLOBAL_SITE_NAME }} was accepted. You can now edit your torrents and set the Trusted flag on them.
{% else %}
We're sorry to inform you that we've rejected your Trusted status application on {{ config.GLOBAL_SITE_NAME }}. You can re-apply for Trusted status in {{ config.TRUSTED_REAPPLY_COOLDOWN }} days if you wish to do so.
{% endif %}
Regards
The {{ config.GLOBAL_SITE_NAME }} Moderation Team

View file

@ -90,6 +90,7 @@
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li {% if request.path == url_for('site.rules') %}class="active"{% endif %}><a href="{{ url_for('site.rules') }}">Rules</a></li> <li {% if request.path == url_for('site.rules') %}class="active"{% endif %}><a href="{{ url_for('site.rules') }}">Rules</a></li>
<li {% if request.path == url_for('site.help') %}class="active"{% endif %}><a href="{{ url_for('site.help') }}">Help</a></li> <li {% if request.path == url_for('site.help') %}class="active"{% endif %}><a href="{{ url_for('site.help') }}">Help</a></li>
<li {% if request.path == url_for('site.trusted') %}class="active"{% endif %}><a href="{{ url_for('site.trusted') }}">Trusted</a></li>
</ul> </ul>
</li> </li>
<li><a href="{% if rss_filter %}{{ url_for('main.home', page='rss', **rss_filter) }}{% else %}{{ url_for('main.home', page='rss') }}{% endif %}">RSS</a></li> <li><a href="{% if rss_filter %}{{ url_for('main.home', page='rss', **rss_filter) }}{% else %}{{ url_for('main.home', page='rss') }}{% endif %}">RSS</a></li>
@ -108,6 +109,7 @@
<li {% if request.path == url_for('admin.reports') %}class="active"{% endif %}><a href="{{ url_for('admin.reports') }}">Reports</a></li> <li {% if request.path == url_for('admin.reports') %}class="active"{% endif %}><a href="{{ url_for('admin.reports') }}">Reports</a></li>
<li {% if request.path == url_for('admin.log') %}class="active"{% endif %}><a href="{{ url_for('admin.log') }}">Log</a></li> <li {% if request.path == url_for('admin.log') %}class="active"{% endif %}><a href="{{ url_for('admin.log') }}">Log</a></li>
<li {% if request.path == url_for('admin.bans') %}class="active"{% endif %}><a href="{{ url_for('admin.bans') }}">Bans</a></li> <li {% if request.path == url_for('admin.bans') %}class="active"{% endif %}><a href="{{ url_for('admin.bans') }}">Bans</a></li>
<li {% if request.path == url_for('admin.trusted') %}class="active"{% endif %}><a href="{{ url_for('admin.trusted') }}">Trusted</a></li>
</ul> </ul>
</li> </li>
{% endif %} {% endif %}

View file

@ -0,0 +1,17 @@
{% extends "layout.html" %}
{% block title %}Trusted :: {{ config.SITE_NAME }}{% endblock %}
{% block body %}
<div class="content">
<div class="row">
<div class="col-md-12">
{% include "trusted_rules.html" %}
</div>
</div>
<div class="row">
<div class="col-md-12">
<a href="{{ url_for('account.request_trusted') }}" class="btn btn-success btn-lg">Request Trusted Status</a>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,51 @@
{% extends "layout.html" %}
{% from "_formhelpers.html" import render_field %}
{% block title %}Apply for Trusted :: {{ config.SITE_NAME }}{% endblock %}
{% block body %}
<div class="content">
{% if trusted_form %}
<div class="row">
<div class="col-md-12">
<h1>You are eligible to apply for trusted status</h1>
</div>
</div>
<form class="trusted-form" method="POST">
{{ trusted_form.csrf_token }}
<div class="row">
<div class="col-md-6">
{{ render_field(trusted_form.why_give_trusted, class_='form-control') }}
</div>
</div>
<div class="row">
<div class="col-md-6">
{{ render_field(trusted_form.why_want_trusted, class_='form-control') }}
</div>
</div>
<div class="row">
<div class="col-md-2">
<input type="submit" value="Submit" class="btn btn-success btn-sm">
</div>
</div>
</form>
{% else %}
<div class="row">
<div class="col-md-12">
<h1>You are currently not eligible to apply for trusted status</h1>
</div>
</div>
<div class="row">
<div class="col-md-12">
<p>
You currently are not eligible to apply for trusted status for the following
reason{% if deny_reasons|length > 1 %}s{% endif %}:
</p>
<ul>
{% for reason in deny_reasons %}
<li>{{ reason }}</li>
{% endfor %}
</ul>
</div>
</div>
{% endif %}
</div>
{% endblock %}

View file

@ -0,0 +1 @@
<h1>Trusted rules go here</h1>

View file

@ -233,6 +233,49 @@ def profile():
return flask.render_template('profile.html', form=form) return flask.render_template('profile.html', form=form)
@bp.route('/trusted/request', methods=['GET', 'POST'])
def request_trusted():
if not flask.g.user:
return flask.redirect(flask.url_for('account.login'))
trusted_form = None
deny_reasons = []
if flask.g.user.is_trusted:
deny_reasons.append('You are already trusted.')
if not flask.g.user.satisfies_trusted_reqs:
deny_reasons.append('You do not satisfy the minimum requirements.')
if (models.TrustedApplication.query.
filter(models.TrustedApplication.submitter_id == flask.g.user.id).
filter_by(is_closed=False).first()):
deny_reasons.append('You already have an open application.')
last_app = models.TrustedApplication.query \
.filter(models.TrustedApplication.submitter_id == flask.g.user.id) \
.filter_by(is_rejected=True) \
.order_by(models.TrustedApplication.closed_time.desc()) \
.first()
if last_app:
if ((datetime.utcnow() - last_app.closed_time).days <
app.config['TRUSTED_REAPPLY_COOLDOWN']):
deny_reasons.append('Your last application was rejected less than {} days ago.'
.format(app.config['TRUSTED_REAPPLY_COOLDOWN']))
if flask.request.method == 'POST':
trusted_form = forms.TrustedForm(flask.request.form)
if trusted_form.validate() and not deny_reasons:
ta = models.TrustedApplication()
ta.submitter_id = flask.g.user.id
ta.why_want = trusted_form.why_want_trusted.data.rstrip()
ta.why_give = trusted_form.why_give_trusted.data.rstrip()
db.session.add(ta)
db.session.commit()
flask.flash('Your trusted application has been submitted. '
'You will receive an email when a decision has been made.', 'success')
return flask.redirect(flask.url_for('site.trusted'))
else:
if len(deny_reasons) == 0:
trusted_form = forms.TrustedForm()
return flask.render_template('trusted_form.html', trusted_form=trusted_form,
deny_reasons=deny_reasons)
def redirect_url(): def redirect_url():
next_url = flask.request.args.get('next', '') next_url = flask.request.args.get('next', '')
referrer = flask.request.referrer or '' referrer = flask.request.referrer or ''

View file

@ -1,10 +1,12 @@
from datetime import datetime
from ipaddress import ip_address from ipaddress import ip_address
import flask import flask
from nyaa import forms, models from nyaa import email, forms, models
from nyaa.extensions import db from nyaa.extensions import db
app = flask.current_app
bp = flask.Blueprint('admin', __name__, url_prefix='/admin') bp = flask.Blueprint('admin', __name__, url_prefix='/admin')
@ -114,3 +116,84 @@ def view_reports():
return flask.render_template('reports.html', return flask.render_template('reports.html',
reports=reports, reports=reports,
report_action=report_action) report_action=report_action)
@bp.route('/trusted/<list_filter>', endpoint='trusted', methods=['GET'])
@bp.route('/trusted', endpoint='trusted', methods=['GET'])
def view_trusted(list_filter=None):
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)
q = db.session.query(models.TrustedApplication)
if list_filter == 'closed':
q = q.filter_by(is_closed=True)
else:
q = q.filter_by(is_closed=False)
if list_filter == 'new':
q = q.filter_by(is_new=True)
elif list_filter == 'reviewed':
q = q.filter_by(is_reviewed=True)
elif list_filter is not None:
flask.abort(404)
apps = q.order_by(models.TrustedApplication.created_time.desc()) \
.paginate(page=page, per_page=20)
return flask.render_template('admin_trusted.html', apps=apps,
list_filter=list_filter)
@bp.route('/trusted/application/<int:app_id>', endpoint='trusted_application',
methods=['GET', 'POST'])
def view_trusted_application(app_id):
if not flask.g.user or not flask.g.user.is_moderator:
flask.abort(403)
app = models.TrustedApplication.by_id(app_id)
if not app:
flask.abort(404)
decision_form = None
review_form = forms.TrustedReviewForm(flask.request.form)
if flask.g.user.is_superadmin and not app.is_closed:
decision_form = forms.TrustedDecisionForm()
if flask.request.method == 'POST':
do_decide = decision_form and (decision_form.accept.data or decision_form.reject.data)
if do_decide and decision_form.validate():
app.closed_time = datetime.utcnow()
if decision_form.accept.data:
app.status = models.TrustedApplicationStatus.ACCEPTED
app.submitter.level = models.UserLevelType.TRUSTED
flask.flash(flask.Markup('Application has been <b>accepted</b>.'), 'success')
elif decision_form.reject.data:
app.status = models.TrustedApplicationStatus.REJECTED
flask.flash(flask.Markup('Application has been <b>rejected</b>.'), 'success')
_send_trusted_decision_email(app.submitter, bool(decision_form.accept.data))
db.session.commit()
return flask.redirect(flask.url_for('admin.trusted_application', app_id=app_id))
elif review_form.comment.data and review_form.validate():
tr = models.TrustedReview()
tr.reviewer_id = flask.g.user.id
tr.app_id = app_id
tr.comment = review_form.comment.data
tr.recommendation = getattr(models.TrustedRecommendation,
review_form.recommendation.data.upper())
if app.status == models.TrustedApplicationStatus.NEW:
app.status = models.TrustedApplicationStatus.REVIEWED
db.session.add(tr)
db.session.commit()
flask.flash('Review successfully posted.', 'success')
return flask.redirect(flask.url_for('admin.trusted_application', app_id=app_id))
return flask.render_template('admin_trusted_view.html', app=app, review_form=review_form,
decision_form=decision_form)
def _send_trusted_decision_email(user, is_accepted):
email_msg = email.EmailHolder(
subject='Your {} Trusted Application was {}.'.format(app.config['GLOBAL_SITE_NAME'],
('rejected', 'accepted')[is_accepted]),
recipient=user,
text=flask.render_template('email/trusted.txt', is_accepted=is_accepted),
html=flask.render_template('email/trusted.html', is_accepted=is_accepted),
)
email.send_email(email_msg)

View file

@ -21,3 +21,8 @@ def help():
@bp.route('/xmlns/nyaa', methods=['GET']) @bp.route('/xmlns/nyaa', methods=['GET'])
def xmlns_nyaa(): def xmlns_nyaa():
return flask.render_template('xmlns.html') return flask.render_template('xmlns.html')
@bp.route('/trusted', methods=['GET'])
def trusted():
return flask.render_template('trusted.html')