From e728ca18182cef8f677e7f06d39f4a60043afd35 Mon Sep 17 00:00:00 2001 From: A nyaa developer Date: Sat, 5 Aug 2017 21:41:59 +0200 Subject: [PATCH] Expose soft delete to users and allow reuploading of deleted torrents. (#331) Add banning torrents for moderators which disallows reuploading. New delete UI. --- nyaa/backend.py | 8 +++- nyaa/forms.py | 19 ++++++++-- nyaa/models.py | 2 + nyaa/templates/edit.html | 80 +++++++++++++++++++++++++++++++++++----- nyaa/views/torrents.py | 65 ++++++++++++++++++++++++++++++++ 5 files changed, 160 insertions(+), 14 deletions(-) diff --git a/nyaa/backend.py b/nyaa/backend.py index 0c20ecf..e777e23 100644 --- a/nyaa/backend.py +++ b/nyaa/backend.py @@ -45,6 +45,11 @@ def _replace_utf8_values(dict_or_list): def handle_torrent_upload(upload_form, uploading_user=None, fromAPI=False): torrent_data = upload_form.torrent_file.parsed_data + # Delete exisiting torrent which is marked as deleted + if torrent_data.db_id is not None: + models.Torrent.query.filter_by(id=torrent_data.db_id).delete() + db.session.commit() + # The torrent has been validated and is safe to access with ['foo'] etc - all relevant # keys and values have been checked for (see UploadForm in forms.py for details) info_dict = torrent_data.torrent_dict['info'] @@ -62,7 +67,8 @@ def handle_torrent_upload(upload_form, uploading_user=None, fromAPI=False): # In case no encoding, assume UTF-8. torrent_encoding = torrent_data.torrent_dict.get('encoding', b'utf-8').decode('utf-8') - torrent = models.Torrent(info_hash=torrent_data.info_hash, + torrent = models.Torrent(id=torrent_data.db_id, + info_hash=torrent_data.info_hash, display_name=display_name, torrent_name=torrent_data.filename, information=information, diff --git a/nyaa/forms.py b/nyaa/forms.py index 528f303..879dbd8 100644 --- a/nyaa/forms.py +++ b/nyaa/forms.py @@ -8,7 +8,7 @@ from flask_wtf.file import FileField, FileRequired from flask_wtf.recaptcha import RecaptchaField from flask_wtf.recaptcha.validators import Recaptcha as RecaptchaValidator from wtforms import (BooleanField, HiddenField, PasswordField, SelectField, StringField, - TextAreaField) + SubmitField, TextAreaField) from wtforms.validators import (DataRequired, Email, EqualTo, Length, Optional, Regexp, StopValidation, ValidationError) from wtforms.widgets import Select as SelectWidget # For DisabledSelectField @@ -186,6 +186,13 @@ class EditForm(FlaskForm): ]) +class DeleteForm(FlaskForm): + delete = SubmitField("Delete") + ban = SubmitField("Delete & Ban") + undelete = SubmitField("Undelete") + unban = SubmitField("Unban") + + class UploadForm(FlaskForm): torrent_file = FileField('Torrent file', [ FileRequired() @@ -281,14 +288,18 @@ class UploadForm(FlaskForm): # Check if the info_hash exists already in the database existing_torrent = models.Torrent.by_info_hash(info_hash) - if existing_torrent: - raise ValidationError('That torrent already exists (#{})'.format(existing_torrent.id)) + existing_torrent_id = existing_torrent.id if existing_torrent else None + if existing_torrent and not existing_torrent.deleted: + raise ValidationError('This torrent already exists (#{})'.format(existing_torrent.id)) + if existing_torrent and existing_torrent.banned: + raise ValidationError('This torrent is banned'.format(existing_torrent.id)) # Torrent is legit, pass original filename and dict along field.parsed_data = TorrentFileData(filename=os.path.basename(field.data.filename), torrent_dict=torrent_dict, info_hash=info_hash, - bencoded_info_dict=bencoded_info_dict) + bencoded_info_dict=bencoded_info_dict, + db_id=existing_torrent_id) class UserForm(FlaskForm): diff --git a/nyaa/models.py b/nyaa/models.py index 1f26cbc..bd14aed 100644 --- a/nyaa/models.py +++ b/nyaa/models.py @@ -98,6 +98,7 @@ class TorrentFlags(IntEnum): REMAKE = 8 COMPLETE = 16 DELETED = 32 + BANNED = 64 class TorrentBase(DeclarativeHelperBase): @@ -250,6 +251,7 @@ class TorrentBase(DeclarativeHelperBase): anonymous = FlagProperty(TorrentFlags.ANONYMOUS) hidden = FlagProperty(TorrentFlags.HIDDEN) deleted = FlagProperty(TorrentFlags.DELETED) + banned = FlagProperty(TorrentFlags.BANNED) trusted = FlagProperty(TorrentFlags.TRUSTED) remake = FlagProperty(TorrentFlags.REMAKE) complete = FlagProperty(TorrentFlags.COMPLETE) diff --git a/nyaa/templates/edit.html b/nyaa/templates/edit.html index adce139..3232499 100644 --- a/nyaa/templates/edit.html +++ b/nyaa/templates/edit.html @@ -15,7 +15,6 @@
{{ form.csrf_token }} -
{{ render_field(form.display_name, class_='form-control', placeholder='Display name') }} @@ -46,14 +45,6 @@ Hidden - {% if g.user.is_moderator %} - - {% endif %}
@@ -93,4 +84,75 @@
+
+ +
+
+
+
+

Danger Zone

+
+
+
+ {{ delete_form.csrf_token }} + + {% if not torrent.deleted %} +

+ Delete torrent. + {{ delete_form.delete(class="btn btn-danger pull-right") }} +

+

+ Deleted torrents are retained for backup purposes.
+ You (or someone else) will be able to reupload the torrent. +

+ + {% if g.user.is_moderator %} +
+

+ Delete and ban torrent. + {{ delete_form.ban(class="btn btn-danger pull-right") }} +

+

+ Soft deletes the torrent and disallows reuploading it. +

+ {% endif %} + {% else %} +

This torrent is deleted{% if torrent.banned %} and banned{% endif %}.

+

+ Undelete{% if torrent.banned %} and unban{% endif %} torrent. + {{ delete_form.undelete(class="btn btn-info pull-right") }} +

+

+ Undeletes{% if torrent.banned %} and unbans{% endif %} this torrent. +

+ + {% if torrent.banned %} +
+

+ Unban torrent. + {{ delete_form.unban(class="btn btn-info pull-right") }} +

+

+ Unbans torrent without undeleting it.
+ Allows reuploading this torrent again. +

+ {% else%} +
+

+ Ban torrent. + {{ delete_form.ban(value="Ban", class="btn btn-danger pull-right") }} +

+

+ Bans the torrent.
+ Disallows reuploading this torrent. +

+ {% endif %} + + {% endif %} +
+
+
+
+
+ {% endblock %} diff --git a/nyaa/views/torrents.py b/nyaa/views/torrents.py index f24d7a1..365ad05 100644 --- a/nyaa/views/torrents.py +++ b/nyaa/views/torrents.py @@ -80,6 +80,7 @@ def view_torrent(torrent_id): def edit_torrent(torrent_id): torrent = models.Torrent.by_id(torrent_id) form = forms.EditForm(flask.request.form) + delete_form = forms.DeleteForm() form.category.choices = _create_upload_category_choices() editor = flask.g.user @@ -147,9 +148,73 @@ def edit_torrent(torrent_id): return flask.render_template('edit.html', form=form, + delete_form=delete_form, torrent=torrent) +@bp.route('/view//delete', endpoint='delete', methods=['POST']) +def delete_torrent(torrent_id): + torrent = models.Torrent.by_id(torrent_id) + form = forms.DeleteForm(flask.request.form) + + editor = flask.g.user + + if not torrent: + flask.abort(404) + + # Only allow admins edit deleted torrents + if torrent.deleted and not (editor and editor.is_moderator): + flask.abort(404) + + # Only allow torrent owners or admins edit torrents + if not editor or not (editor is torrent.user or editor.is_moderator): + flask.abort(403) + + action = None + url = flask.url_for('main.home') + + if form.delete.data and not torrent.deleted: + action = 'deleted' + torrent.deleted = True + db.session.add(torrent) + + elif form.ban.data and not torrent.banned and editor.is_moderator: + torrent.banned = True + if not torrent.deleted: + torrent.deleted = True + action = 'deleted and banned' + else: + action = 'banned' + db.session.add(torrent) + + elif form.undelete.data and torrent.deleted: + action = 'undeleted' + torrent.deleted = False + torrent.banned = False + db.session.add(torrent) + + elif form.unban.data and torrent.banned: + action = 'unbanned' + torrent.banned = False + db.session.add(torrent) + + if not action: + flask.flash(flask.Markup('What the fuck are you doing?'), 'danger') + return flask.redirect(flask.url_for('torrents.edit', torrent_id=torrent.id)) + + if editor.is_moderator: + url = flask.url_for('torrents.view', torrent_id=torrent.id) + if editor is not torrent.user: + log = "Torrent [#{0}]({1}) has been {2}".format(torrent.id, url, action) + adminlog = models.AdminLog(log=log, admin_id=editor.id) + db.session.add(adminlog) + + db.session.commit() + + flask.flash(flask.Markup('Torrent has been successfully {0}.'.format(action)), 'info') + return flask.redirect(url) + + @bp.route('/view//magnet') def redirect_magnet(torrent_id): torrent = models.Torrent.by_id(torrent_id)