diff --git a/migrations/versions/d0eeb8049623_add_comments.py b/migrations/versions/d0eeb8049623_add_comments.py new file mode 100644 index 0000000..4b8599a --- /dev/null +++ b/migrations/versions/d0eeb8049623_add_comments.py @@ -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 ### diff --git a/nyaa/forms.py b/nyaa/forms.py index f6e35a8..be79f51 100644 --- a/nyaa/forms.py +++ b/nyaa/forms.py @@ -128,18 +128,16 @@ class DisabledSelectField(SelectField): class CommentForm(FlaskForm): 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() ]) - is_anonymous = BooleanField('Anonymous') - class EditForm(FlaskForm): display_name = StringField('Torrent display name', [ - Length(min=3, max=255, - message='Torrent display name must be at least %(min)d characters long ' - 'and %(max)d at most.') + Length(min=3, max=255, message='Torrent display name must be at least %(min)d characters ' + 'long and %(max)d at most.') ]) category = DisabledSelectField('Category') diff --git a/nyaa/models.py b/nyaa/models.py index 1e1e8dc..e623aa8 100644 --- a/nyaa/models.py +++ b/nyaa/models.py @@ -1,3 +1,4 @@ +import flask from enum import Enum, IntEnum from datetime import datetime, timezone from nyaa import app, db @@ -11,7 +12,8 @@ from ipaddress import ip_address import re import base64 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']: from sqlalchemy.dialects import mysql @@ -99,6 +101,8 @@ class Torrent(db.Model): cascade="all, delete-orphan", back_populates='torrent', lazy='joined') trackers = db.relationship('TorrentTrackers', uselist=True, cascade="all, delete-orphan", lazy='joined') + comments = db.relationship('Comment', uselist=True, + cascade="all, delete-orphan") def __repr__(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' id = db.Column(db.Integer, primary_key=True) - torrent = db.Column(db.Integer, db.ForeignKey( - DB_TABLE_PREFIX + 'torrents.id'), primary_key=True) - user_id = db.Column(db.Integer, db.ForeignKey( - 'users.id', ondelete='CASCADE')) + torrent_id = db.Column(db.Integer, db.ForeignKey( + DB_TABLE_PREFIX + 'torrents.id', ondelete='CASCADE'), nullable=False) + user_id = db.Column(db.Integer, db.ForeignKey('users.id', ondelete='CASCADE')) created_time = db.Column(db.DateTime(timezone=False), default=datetime.utcnow) 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): return '' % self.id @@ -396,11 +399,21 @@ class User(db.Model): # from http://en.gravatar.com/site/implement/images/python/ size = 120 # construct the url - gravatar_url = 'https://www.gravatar.com/avatar/' + \ - hashlib.md5(self.email.encode('utf-8').lower()).hexdigest() + '?' - gravatar_url += urllib.parse.urlencode({'d': config.DEFAULT_AVATAR_URL, 's': str(size)}) + default_avatar = flask.url_for('static', filename='img/avatar/default.png', _external=True) + gravatar_url = 'https://www.gravatar.com/avatar/{}?{}'.format( + md5(self.email.encode('utf-8').lower()).hexdigest(), + urlencode({'d': default_avatar, 's': str(size)})) 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 def ip_string(self): if self.last_login_ip: diff --git a/nyaa/routes.py b/nyaa/routes.py index 4df27a6..ecc3df9 100644 --- a/nyaa/routes.py +++ b/nyaa/routes.py @@ -7,6 +7,7 @@ from nyaa import torrents from nyaa import backend from nyaa import api_handler from nyaa.search import search_elastic, search_db +from sqlalchemy.orm import joinedload import config import json @@ -572,53 +573,52 @@ def upload(): @app.route('/view/') def view_torrent(torrent_id): - torrent = models.Torrent.by_id(torrent_id) - form = forms.CommentForm() - - viewer = flask.g.user - + torrent = models.Torrent.query \ + .options(joinedload('filelist'), + joinedload('comments')) \ + .filter_by(id=torrent_id) \ + .first() if not torrent: flask.abort(404) # 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) # 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 if torrent.filelist: files = json.loads(torrent.filelist.filelist_blob.decode('utf-8')) - comments = models.Comment.query.filter_by(torrent=torrent_id) - comment_count = comments.count() + comment_form = None + if flask.g.user: + comment_form = forms.CommentForm() return flask.render_template('view.html', torrent=torrent, files=files, - viewer=viewer, - form=form, - comments=comments, - comment_count=comment_count, + comment_form=comment_form, + comments=torrent.comments, can_edit=can_edit) -@app.route('/view//submit_comment', methods=['POST']) +@app.route('/view//comment', methods=['POST']) 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() - # 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( - torrent=torrent_id, - user_id=current_user_id, + torrent_id=torrent_id, + user_id=flask.g.user.id, text=comment_text) 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)) -@app.route('/view//delete_comment/') +@app.route('/view//comment//delete', methods=['POST']) def delete_comment(torrent_id, comment_id): - if flask.g.user is not None and flask.g.user.is_admin: - models.Comment.query.filter_by(id=comment_id).delete() - db.session.commit() - else: + if not flask.g.user: 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)) @@ -650,11 +659,11 @@ def edit_torrent(torrent_id): flask.abort(404) # 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) # 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) if flask.request.method == 'POST' and form.validate(): @@ -670,9 +679,9 @@ def edit_torrent(torrent_id): torrent.complete = form.is_complete.data torrent.anonymous = form.is_anonymous.data - if editor.is_trusted: + if flask.g.user.is_trusted: torrent.trusted = form.is_trusted.data - if editor.is_moderator: + if flask.g.user.is_moderator: torrent.deleted = form.is_deleted.data db.session.commit() @@ -700,8 +709,7 @@ def edit_torrent(torrent_id): return flask.render_template('edit.html', form=form, - torrent=torrent, - editor=editor) + torrent=torrent) @app.route('/view//magnet') @@ -793,7 +801,7 @@ def _create_user_class_choices(user): return default, choices -# Modified from: http://flask.pocoo.org/snippets/33/ + @app.template_filter() def timesince(dt, default='just now'): """ @@ -823,6 +831,8 @@ def timesince(dt, default='just now'): return default # #################################### STATIC PAGES #################################### + + @app.route('/rules', methods=['GET']) def site_rules(): return flask.render_template('rules.html') diff --git a/nyaa/static/css/main.css b/nyaa/static/css/main.css index 46390ff..fdca7db 100644 --- a/nyaa/static/css/main.css +++ b/nyaa/static/css/main.css @@ -233,7 +233,7 @@ ul.nav-tabs#profileTabs { margin-bottom:10px; } -.delete-btn { +.delete-comment-form { position: relative; float: right; } diff --git a/nyaa/templates/edit.html b/nyaa/templates/edit.html index b65ce0c..e608f8d 100644 --- a/nyaa/templates/edit.html +++ b/nyaa/templates/edit.html @@ -7,7 +7,7 @@ {% set torrent_url = url_for('view_torrent', torrent_id=torrent.id) %}

Edit Torrent #{{torrent.id}} - {% if (torrent.user != None) and (torrent.user != editor) %} + {% if (torrent.user != None) and (torrent.user != g.user) %} (by {{ torrent.user.username }}) {% endif %}

@@ -31,7 +31,7 @@
- {% if editor.is_moderator %} + {% if g.user.is_moderator %} {% endif %} - {% if editor.is_trusted %} + {% if g.user.is_trusted %}
@@ -133,7 +133,7 @@

- Comments - {{ comment_count }} + Comments - {{ comments|length }}

{% for comment in comments %} @@ -148,15 +148,15 @@ {% endif %}

{{ comment.user.userlevel_str }}

-

- -

+

- {{comment.created_time | timesince}} + {{ comment.created_time | timesince }} {% if g.user.is_moderator or g.user.id == comment.user_id %} -
Delete
+
+ +
{% endif %}
@@ -175,10 +175,10 @@ target.innerHTML = writer.render(parsed); {% endfor %} - {% if g.user %} -
- {{ form.csrf_token }} - {{ render_field(form.comment, class_='form-control') }} + {% if comment_form %} + + {{ comment_form.csrf_token }} + {{ render_field(comment_form.comment, class_='form-control') }}
{% endif %} diff --git a/nyaa/torrents.py b/nyaa/torrents.py index 648683f..3a466a9 100644 --- a/nyaa/torrents.py +++ b/nyaa/torrents.py @@ -14,6 +14,7 @@ USED_TRACKERS = OrderedSet() # Limit the amount of trackers added into .torrent files MAX_TRACKERS = 5 + def read_trackers_from_file(file_object): USED_TRACKERS.clear()