Make comments great again.

This commit is contained in:
nyaadev 2017-05-22 23:01:23 +02:00
parent aab3eaccaa
commit b7144f80f9
8 changed files with 137 additions and 67 deletions

View File

@ -0,0 +1,48 @@
"""Add comments table.
Revision ID: d0eeb8049623
Revises: 3001f79b7722
Create Date: 2017-05-22 22:58:12.039149
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'd0eeb8049623'
down_revision = '3001f79b7722'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('nyaa_comments',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('torrent_id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('created_time', sa.DateTime(), nullable=True),
sa.Column('text', sa.String(length=255), nullable=False),
sa.ForeignKeyConstraint(['torrent_id'], ['nyaa_torrents.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
op.create_table('sukebei_comments',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('torrent_id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('created_time', sa.DateTime(), nullable=True),
sa.Column('text', sa.String(length=255), nullable=False),
sa.ForeignKeyConstraint(['torrent_id'], ['sukebei_torrents.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('nyaa_comments')
op.drop_table('sukebei_comments')
# ### end Alembic commands ###

View File

@ -128,18 +128,16 @@ class DisabledSelectField(SelectField):
class CommentForm(FlaskForm): class CommentForm(FlaskForm):
comment = TextAreaField('Make a comment', [ comment = TextAreaField('Make a comment', [
Length(max=255, message='Comment must be at most %(max)d characters long.'), Length(min=3, max=255, message='Comment must be at least %(min)d characters '
'long and %(max)d at most.'),
DataRequired() DataRequired()
]) ])
is_anonymous = BooleanField('Anonymous')
class EditForm(FlaskForm): class EditForm(FlaskForm):
display_name = StringField('Torrent display name', [ display_name = StringField('Torrent display name', [
Length(min=3, max=255, Length(min=3, max=255, message='Torrent display name must be at least %(min)d characters '
message='Torrent display name must be at least %(min)d characters long ' 'long and %(max)d at most.')
'and %(max)d at most.')
]) ])
category = DisabledSelectField('Category') category = DisabledSelectField('Category')

View File

@ -1,3 +1,4 @@
import flask
from enum import Enum, IntEnum from enum import Enum, IntEnum
from datetime import datetime, timezone from datetime import datetime, timezone
from nyaa import app, db from nyaa import app, db
@ -11,7 +12,8 @@ from ipaddress import ip_address
import re import re
import base64 import base64
from markupsafe import escape as escape_markup from markupsafe import escape as escape_markup
from urllib.parse import unquote as unquote_url from urllib.parse import urlencode, unquote as unquote_url
from hashlib import md5
if app.config['USE_MYSQL']: if app.config['USE_MYSQL']:
from sqlalchemy.dialects import mysql from sqlalchemy.dialects import mysql
@ -99,6 +101,8 @@ class Torrent(db.Model):
cascade="all, delete-orphan", back_populates='torrent', lazy='joined') cascade="all, delete-orphan", back_populates='torrent', lazy='joined')
trackers = db.relationship('TorrentTrackers', uselist=True, trackers = db.relationship('TorrentTrackers', uselist=True,
cascade="all, delete-orphan", lazy='joined') cascade="all, delete-orphan", lazy='joined')
comments = db.relationship('Comment', uselist=True,
cascade="all, delete-orphan")
def __repr__(self): def __repr__(self):
return '<{0} #{1.id} \'{1.display_name}\' {1.filesize}b>'.format(type(self).__name__, self) return '<{0} #{1.id} \'{1.display_name}\' {1.filesize}b>'.format(type(self).__name__, self)
@ -321,14 +325,13 @@ class Comment(db.Model):
__tablename__ = DB_TABLE_PREFIX + 'comments' __tablename__ = DB_TABLE_PREFIX + 'comments'
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
torrent = db.Column(db.Integer, db.ForeignKey( torrent_id = db.Column(db.Integer, db.ForeignKey(
DB_TABLE_PREFIX + 'torrents.id'), primary_key=True) DB_TABLE_PREFIX + 'torrents.id', ondelete='CASCADE'), nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey( user_id = db.Column(db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'))
'users.id', ondelete='CASCADE'))
created_time = db.Column(db.DateTime(timezone=False), default=datetime.utcnow) created_time = db.Column(db.DateTime(timezone=False), default=datetime.utcnow)
text = db.Column(db.String(length=255), nullable=False) text = db.Column(db.String(length=255), nullable=False)
user = db.relationship('User', uselist=False, back_populates='comments') user = db.relationship('User', uselist=False, back_populates='comments', lazy="joined")
def __repr__(self): def __repr__(self):
return '<Comment %r>' % self.id return '<Comment %r>' % self.id
@ -396,11 +399,21 @@ class User(db.Model):
# from http://en.gravatar.com/site/implement/images/python/ # from http://en.gravatar.com/site/implement/images/python/
size = 120 size = 120
# construct the url # construct the url
gravatar_url = 'https://www.gravatar.com/avatar/' + \ default_avatar = flask.url_for('static', filename='img/avatar/default.png', _external=True)
hashlib.md5(self.email.encode('utf-8').lower()).hexdigest() + '?' gravatar_url = 'https://www.gravatar.com/avatar/{}?{}'.format(
gravatar_url += urllib.parse.urlencode({'d': config.DEFAULT_AVATAR_URL, 's': str(size)}) md5(self.email.encode('utf-8').lower()).hexdigest(),
urlencode({'d': default_avatar, 's': str(size)}))
return gravatar_url return gravatar_url
@property
def userlevel_str(self):
if self.level == UserLevelType.REGULAR:
return 'User'
elif self.level == UserLevelType.TRUSTED:
return 'Trusted'
elif self.level >= UserLevelType.MODERATOR:
return 'Moderator'
@property @property
def ip_string(self): def ip_string(self):
if self.last_login_ip: if self.last_login_ip:

View File

@ -7,6 +7,7 @@ from nyaa import torrents
from nyaa import backend from nyaa import backend
from nyaa import api_handler from nyaa import api_handler
from nyaa.search import search_elastic, search_db from nyaa.search import search_elastic, search_db
from sqlalchemy.orm import joinedload
import config import config
import json import json
@ -572,53 +573,52 @@ def upload():
@app.route('/view/<int:torrent_id>') @app.route('/view/<int:torrent_id>')
def view_torrent(torrent_id): def view_torrent(torrent_id):
torrent = models.Torrent.by_id(torrent_id) torrent = models.Torrent.query \
form = forms.CommentForm() .options(joinedload('filelist'),
joinedload('comments')) \
viewer = flask.g.user .filter_by(id=torrent_id) \
.first()
if not torrent: if not torrent:
flask.abort(404) flask.abort(404)
# Only allow admins see deleted torrents # Only allow admins see deleted torrents
if torrent.deleted and not (viewer and viewer.is_moderator): if torrent.deleted and not (flask.g.user and flask.g.user.is_moderator):
flask.abort(404) flask.abort(404)
# Only allow owners and admins to edit torrents # Only allow owners and admins to edit torrents
can_edit = viewer and (viewer is torrent.user or viewer.is_moderator) can_edit = flask.g.user and (flask.g.user is torrent.user or flask.g.user.is_moderator)
files = None files = None
if torrent.filelist: if torrent.filelist:
files = json.loads(torrent.filelist.filelist_blob.decode('utf-8')) files = json.loads(torrent.filelist.filelist_blob.decode('utf-8'))
comments = models.Comment.query.filter_by(torrent=torrent_id) comment_form = None
comment_count = comments.count() if flask.g.user:
comment_form = forms.CommentForm()
return flask.render_template('view.html', torrent=torrent, return flask.render_template('view.html', torrent=torrent,
files=files, files=files,
viewer=viewer, comment_form=comment_form,
form=form, comments=torrent.comments,
comments=comments,
comment_count=comment_count,
can_edit=can_edit) can_edit=can_edit)
@app.route('/view/<int:torrent_id>/submit_comment', methods=['POST']) @app.route('/view/<int:torrent_id>/comment', methods=['POST'])
def submit_comment(torrent_id): def submit_comment(torrent_id):
form = forms.CommentForm(flask.request.form) if not flask.g.user:
flask.abort(403)
if flask.request.method == 'POST' and form.validate(): torrent = models.Torrent.by_id(torrent_id)
if not torrent:
flask.abort(404)
form = forms.CommentForm(flask.request.form)
if form.validate():
comment_text = (form.comment.data or '').strip() comment_text = (form.comment.data or '').strip()
# Null entry for User just means Anonymous
if flask.g.user is None or form.is_anonymous.data:
current_user_id = None
else:
current_user_id = flask.g.user.id
comment = models.Comment( comment = models.Comment(
torrent=torrent_id, torrent_id=torrent_id,
user_id=current_user_id, user_id=flask.g.user.id,
text=comment_text) text=comment_text)
db.session.add(comment) db.session.add(comment)
@ -627,14 +627,23 @@ def submit_comment(torrent_id):
return flask.redirect(flask.url_for('view_torrent', torrent_id=torrent_id)) return flask.redirect(flask.url_for('view_torrent', torrent_id=torrent_id))
@app.route('/view/<int:torrent_id>/delete_comment/<int:comment_id>') @app.route('/view/<int:torrent_id>/comment/<int:comment_id>/delete', methods=['POST'])
def delete_comment(torrent_id, comment_id): def delete_comment(torrent_id, comment_id):
if flask.g.user is not None and flask.g.user.is_admin: if not flask.g.user:
models.Comment.query.filter_by(id=comment_id).delete()
db.session.commit()
else:
flask.abort(403) flask.abort(403)
comment = models.Comment.query.filter_by(id=comment_id).first()
if not comment:
flask.abort(404)
if not (comment.user.id == flask.g.user.id or flask.g.user.is_moderator):
flask.abort(403)
db.session.delete(comment)
db.session.commit()
flask.flash('Comment successfully deleted.', 'success')
return flask.redirect(flask.url_for('view_torrent', torrent_id=torrent_id)) return flask.redirect(flask.url_for('view_torrent', torrent_id=torrent_id))
@ -650,11 +659,11 @@ def edit_torrent(torrent_id):
flask.abort(404) flask.abort(404)
# Only allow admins edit deleted torrents # Only allow admins edit deleted torrents
if torrent.deleted and not (editor and editor.is_moderator): if torrent.deleted and not (flask.g.user and flask.g.user.is_moderator):
flask.abort(404) flask.abort(404)
# Only allow torrent owners or admins edit torrents # Only allow torrent owners or admins edit torrents
if not editor or not (editor is torrent.user or editor.is_moderator): if not flask.g.user or not (flask.g.user is torrent.user or flask.g.user.is_moderator):
flask.abort(403) flask.abort(403)
if flask.request.method == 'POST' and form.validate(): if flask.request.method == 'POST' and form.validate():
@ -670,9 +679,9 @@ def edit_torrent(torrent_id):
torrent.complete = form.is_complete.data torrent.complete = form.is_complete.data
torrent.anonymous = form.is_anonymous.data torrent.anonymous = form.is_anonymous.data
if editor.is_trusted: if flask.g.user.is_trusted:
torrent.trusted = form.is_trusted.data torrent.trusted = form.is_trusted.data
if editor.is_moderator: if flask.g.user.is_moderator:
torrent.deleted = form.is_deleted.data torrent.deleted = form.is_deleted.data
db.session.commit() db.session.commit()
@ -700,8 +709,7 @@ def edit_torrent(torrent_id):
return flask.render_template('edit.html', return flask.render_template('edit.html',
form=form, form=form,
torrent=torrent, torrent=torrent)
editor=editor)
@app.route('/view/<int:torrent_id>/magnet') @app.route('/view/<int:torrent_id>/magnet')
@ -793,7 +801,7 @@ def _create_user_class_choices(user):
return default, choices return default, choices
# Modified from: http://flask.pocoo.org/snippets/33/
@app.template_filter() @app.template_filter()
def timesince(dt, default='just now'): def timesince(dt, default='just now'):
""" """
@ -823,6 +831,8 @@ def timesince(dt, default='just now'):
return default return default
# #################################### STATIC PAGES #################################### # #################################### STATIC PAGES ####################################
@app.route('/rules', methods=['GET']) @app.route('/rules', methods=['GET'])
def site_rules(): def site_rules():
return flask.render_template('rules.html') return flask.render_template('rules.html')

View File

@ -233,7 +233,7 @@ ul.nav-tabs#profileTabs {
margin-bottom:10px; margin-bottom:10px;
} }
.delete-btn { .delete-comment-form {
position: relative; position: relative;
float: right; float: right;
} }

View File

@ -7,7 +7,7 @@
{% set torrent_url = url_for('view_torrent', torrent_id=torrent.id) %} {% set torrent_url = url_for('view_torrent', torrent_id=torrent.id) %}
<h1> <h1>
Edit Torrent <a href="{{ torrent_url }}">#{{torrent.id}}</a> Edit Torrent <a href="{{ torrent_url }}">#{{torrent.id}}</a>
{% if (torrent.user != None) and (torrent.user != editor) %} {% if (torrent.user != None) and (torrent.user != g.user) %}
(by <a href="{{ url_for('view_user', user_name=torrent.user.username) }}">{{ torrent.user.username }}</a>) (by <a href="{{ url_for('view_user', user_name=torrent.user.username) }}">{{ torrent.user.username }}</a>)
{% endif %} {% endif %}
</h1> </h1>
@ -31,7 +31,7 @@
<div class="col-md-6"> <div class="col-md-6">
<label class="control-label">Torrent flags</label> <label class="control-label">Torrent flags</label>
<div> <div>
{% if editor.is_moderator %} {% if g.user.is_moderator %}
<label class="btn btn-primary"> <label class="btn btn-primary">
{{ form.is_deleted }} {{ form.is_deleted }}
Deleted Deleted
@ -58,7 +58,7 @@
Anonymous Anonymous
</label> </label>
{% endif %} {% endif %}
{% if editor.is_trusted %} {% if g.user.is_trusted %}
<label class="btn btn-success" title="Mark torrent trusted"> <label class="btn btn-success" title="Mark torrent trusted">
{{ form.is_trusted }} {{ form.is_trusted }}
Trusted Trusted

View File

@ -29,9 +29,9 @@
{%- if not torrent.anonymous and torrent.user -%} {%- if not torrent.anonymous and torrent.user -%}
<a href="{{ user_url }}">{{ torrent.user.username }}</a> <a href="{{ user_url }}">{{ torrent.user.username }}</a>
{%- else -%} {%- else -%}
Anonymous {% if torrent.user and (viewer == torrent.user or viewer.is_moderator) %}(<a href="{{ user_url }}">{{ torrent.user.username }}</a>){% endif %} Anonymous {% if torrent.user and (g.user == torrent.user or g.user.is_moderator) %}(<a href="{{ user_url }}">{{ torrent.user.username }}</a>){% endif %}
{%- endif -%} {%- endif -%}
{%- if viewer and viewer.is_superadmin and torrent.uploader_ip -%} {%- if g.user and g.user.is_superadmin and torrent.uploader_ip -%}
({{ torrent.uploader_ip_string }}) ({{ torrent.uploader_ip_string }})
{%- endif -%} {%- endif -%}
</div> </div>
@ -133,7 +133,7 @@
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<h3 class="panel-title"> <h3 class="panel-title">
Comments - {{ comment_count }} Comments - {{ comments|length }}
</h3> </h3>
</div> </div>
{% for comment in comments %} {% for comment in comments %}
@ -148,15 +148,15 @@
{% endif %} {% endif %}
</p> </p>
<p>{{ comment.user.userlevel_str }}</p> <p>{{ comment.user.userlevel_str }}</p>
<p> <p><img class="avatar" src="{{ comment.user.gravatar_url() }}"></p>
<img class="avatar" src="{{ comment.user.gravatar_url() }}">
</p>
</div> </div>
<div class="col-md-10"> <div class="col-md-10">
<div class="row"> <div class="row">
<small>{{comment.created_time | timesince}}</small> <small>{{ comment.created_time | timesince }}</small>
{% if g.user.is_moderator or g.user.id == comment.user_id %} {% if g.user.is_moderator or g.user.id == comment.user_id %}
<div class="btn btn-danger btn-sm delete-btn" title="Delete" onclick='location.href = "{{ url_for('delete_comment', comment_id=comment.id, torrent_id=torrent.id)}}";'>Delete</div> <form class="delete-comment-form" action="{{ url_for('delete_comment', torrent_id=torrent.id, comment_id=comment.id) }}" method="POST">
<button name="submit" type="submit" class="btn btn-danger btn-sm" title="Delete">Delete</button>
</form>
{% endif %} {% endif %}
</div> </div>
<div class="row"> <div class="row">
@ -175,10 +175,10 @@
target.innerHTML = writer.render(parsed); target.innerHTML = writer.render(parsed);
</script> </script>
{% endfor %} {% endfor %}
{% if g.user %} {% if comment_form %}
<form method="POST" class="comment-box"> <form action="{{ url_for('submit_comment', torrent_id=torrent.id) }}" method="POST" class="comment-box">
{{ form.csrf_token }} {{ comment_form.csrf_token }}
{{ render_field(form.comment, class_='form-control') }} {{ render_field(comment_form.comment, class_='form-control') }}
<input type="submit" value="Submit" class="btn btn-success btn-sm"> <input type="submit" value="Submit" class="btn btn-success btn-sm">
</form> </form>
{% endif %} {% endif %}

View File

@ -14,6 +14,7 @@ USED_TRACKERS = OrderedSet()
# Limit the amount of trackers added into .torrent files # Limit the amount of trackers added into .torrent files
MAX_TRACKERS = 5 MAX_TRACKERS = 5
def read_trackers_from_file(file_object): def read_trackers_from_file(file_object):
USED_TRACKERS.clear() USED_TRACKERS.clear()