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:
parent
ff44d7a51c
commit
16814d6eb7
|
@ -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
|
||||||
|
|
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()
|
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'
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 */
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
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">
|
<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 %}
|
||||||
|
|
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)
|
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 ''
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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')
|
||||||
|
|
Loading…
Reference in a new issue