mirror of
https://gitlab.com/SIGBUS/nyaa.git
synced 2024-12-22 03:00:01 +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:
parent
ff44d7a51c
commit
16814d6eb7
|
@ -176,3 +176,15 @@ EDITING_TIME_LIMIT = 0
|
|||
# Whether to use Gravatar or just always use the default avatar
|
||||
# (Useful if run as development instance behind NAT/firewall)
|
||||
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
|
||||
|
|
47
migrations/versions/5cbcee17bece_add_trusted_applications.py
Normal file
47
migrations/versions/5cbcee17bece_add_trusted_applications.py
Normal 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')
|
|
@ -485,6 +485,33 @@ class ReportActionForm(FlaskForm):
|
|||
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):
|
||||
announce = torrent_dict.get('announce')
|
||||
assert announce is not None, 'no tracker in torrent'
|
||||
|
|
|
@ -11,8 +11,9 @@ from urllib.parse import urlencode
|
|||
import flask
|
||||
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.hybrid import hybrid_property
|
||||
from sqlalchemy_fulltext import FullText
|
||||
from sqlalchemy_utils import ChoiceType, EmailType, PasswordType
|
||||
|
||||
|
@ -642,6 +643,24 @@ class User(db.Model):
|
|||
''' Returns a UTC POSIX timestamp, as 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):
|
||||
__tablename__ = 'user_preferences'
|
||||
|
@ -845,6 +864,77 @@ class RangeBan(db.Model):
|
|||
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
|
||||
|
||||
# Torrent
|
||||
|
|
|
@ -448,6 +448,9 @@ h6:hover .header-anchor {
|
|||
visibility: visible;
|
||||
display: inline-block;
|
||||
}
|
||||
.trusted-form textarea {
|
||||
height:12em;
|
||||
}
|
||||
|
||||
/* Dark theme */
|
||||
|
||||
|
|
|
@ -251,6 +251,11 @@ document.addEventListener("DOMContentLoaded", function() {
|
|||
var target = markdownTargets[i];
|
||||
var rendered;
|
||||
var markdownSource = htmlDecode(target.innerHTML);
|
||||
if (target.attributes["markdown-no-images"]) {
|
||||
markdown.disable('image');
|
||||
} else {
|
||||
markdown.enable('image');
|
||||
}
|
||||
if (target.attributes["markdown-text-inline"]) {
|
||||
rendered = markdown.renderInline(markdownSource);
|
||||
} else {
|
||||
|
|
|
@ -113,7 +113,7 @@
|
|||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro render_menu_with_button(field) %}
|
||||
{% macro render_menu_with_button(field, button_label='Apply') %}
|
||||
{% if field.errors %}
|
||||
<div class="form-group has-error">
|
||||
{% else %}
|
||||
|
@ -123,7 +123,7 @@
|
|||
<div class="input-group input-group-sm">
|
||||
{{ field(title=field.description, class_="form-control",**kwargs) | safe }}
|
||||
<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>
|
||||
{% if field.errors %}
|
||||
|
|
54
nyaa/templates/admin_trusted.html
Normal file
54
nyaa/templates/admin_trusted.html
Normal 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 %}
|
114
nyaa/templates/admin_trusted_view.html
Normal file
114
nyaa/templates/admin_trusted_view.html
Normal 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', ' '|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', ' '|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', ' '|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 %}
|
14
nyaa/templates/email/trusted.html
Normal file
14
nyaa/templates/email/trusted.html
Normal 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>
|
8
nyaa/templates/email/trusted.txt
Normal file
8
nyaa/templates/email/trusted.txt
Normal 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
|
|
@ -90,6 +90,7 @@
|
|||
<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.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>
|
||||
</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.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.trusted') %}class="active"{% endif %}><a href="{{ url_for('admin.trusted') }}">Trusted</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
|
17
nyaa/templates/trusted.html
Normal file
17
nyaa/templates/trusted.html
Normal 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 %}
|
51
nyaa/templates/trusted_form.html
Normal file
51
nyaa/templates/trusted_form.html
Normal 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 %}
|
1
nyaa/templates/trusted_rules.html
Normal file
1
nyaa/templates/trusted_rules.html
Normal file
|
@ -0,0 +1 @@
|
|||
<h1>Trusted rules go here</h1>
|
|
@ -233,6 +233,49 @@ def profile():
|
|||
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():
|
||||
next_url = flask.request.args.get('next', '')
|
||||
referrer = flask.request.referrer or ''
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
from datetime import datetime
|
||||
from ipaddress import ip_address
|
||||
|
||||
import flask
|
||||
|
||||
from nyaa import forms, models
|
||||
from nyaa import email, forms, models
|
||||
from nyaa.extensions import db
|
||||
|
||||
app = flask.current_app
|
||||
bp = flask.Blueprint('admin', __name__, url_prefix='/admin')
|
||||
|
||||
|
||||
|
@ -114,3 +116,84 @@ def view_reports():
|
|||
return flask.render_template('reports.html',
|
||||
reports=reports,
|
||||
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)
|
||||
|
|
|
@ -21,3 +21,8 @@ def help():
|
|||
@bp.route('/xmlns/nyaa', methods=['GET'])
|
||||
def xmlns_nyaa():
|
||||
return flask.render_template('xmlns.html')
|
||||
|
||||
|
||||
@bp.route('/trusted', methods=['GET'])
|
||||
def trusted():
|
||||
return flask.render_template('trusted.html')
|
||||
|
|
Loading…
Reference in a new issue