diff options
-rw-r--r-- | app/blueprints/packages/__init__.py | 2 | ||||
-rw-r--r-- | app/blueprints/packages/reviews.py | 98 | ||||
-rw-r--r-- | app/blueprints/threads/__init__.py | 3 | ||||
-rw-r--r-- | app/models.py | 36 | ||||
-rw-r--r-- | app/templates/macros/reviews.html | 117 | ||||
-rw-r--r-- | app/templates/packages/review_create_edit.html | 45 | ||||
-rw-r--r-- | app/templates/packages/view.html | 10 | ||||
-rw-r--r-- | app/templates/threads/view.html | 19 | ||||
-rw-r--r-- | migrations/versions/4f2e19bc2a27_.py | 40 |
9 files changed, 361 insertions, 9 deletions
diff --git a/app/blueprints/packages/__init__.py b/app/blueprints/packages/__init__.py index e4fc4f2..99616bf 100644 --- a/app/blueprints/packages/__init__.py +++ b/app/blueprints/packages/__init__.py @@ -18,4 +18,4 @@ from flask import Blueprint bp = Blueprint("packages", __name__) -from . import packages, screenshots, releases +from . import packages, screenshots, releases, reviews diff --git a/app/blueprints/packages/reviews.py b/app/blueprints/packages/reviews.py new file mode 100644 index 0000000..4322e8d --- /dev/null +++ b/app/blueprints/packages/reviews.py @@ -0,0 +1,98 @@ +# Content DB +# Copyright (C) 2020 rubenwardy +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +from . import bp + +from flask import * +from flask_user import * +from flask_wtf import FlaskForm +from wtforms import * +from wtforms.validators import * +from app.models import db, PackageReview, Thread, ThreadReply +from app.utils import is_package_page, triggerNotif + +class ReviewForm(FlaskForm): + title = StringField("Title", [InputRequired(), Length(3,100)]) + comment = TextAreaField("Comment", [InputRequired(), Length(10, 500)]) + recommends = RadioField("Private", [InputRequired()], choices=[("yes", "Yes"), ("no", "No")]) + submit = SubmitField("Save") + +@bp.route("/packages/<author>/<name>/review/", methods=["GET", "POST"]) +@login_required +@is_package_page +def review(package): + review = PackageReview.query.filter_by(package=package, author=current_user).first() + + form = ReviewForm(formdata=request.form, obj=review) + + # Set default values + if request.method == "GET" and review: + form.title.data = review.thread.title + form.recommends.data = "yes" if review.recommends else "no" + form.comment.data = review.thread.replies[0].comment + + # Validate and submit + elif request.method == "POST" and form.validate(): + was_new = False + if not review: + was_new = True + review = PackageReview() + review.package = package + review.author = current_user + db.session.add(review) + + review.recommends = form.recommends.data == "yes" + + thread = review.thread + if not thread: + thread = Thread() + thread.author = current_user + thread.private = False + thread.package = package + thread.review = review + db.session.add(thread) + + thread.watchers.append(current_user) + + reply = ThreadReply() + reply.thread = thread + reply.author = current_user + reply.comment = form.comment.data + db.session.add(reply) + + thread.replies.append(reply) + else: + reply = thread.replies[0] + reply.comment = form.comment.data + + thread.title = form.title.data + + db.session.commit() + + notif_msg = None + if was_new: + notif_msg = "New review '{}' on package {}".format(form.title.data, package.title) + else: + notif_msg = "Updated review '{}' on package {}".format(form.title.data, package.title) + + for maintainer in package.maintainers: + triggerNotif(maintainer, current_user, notif_msg, url_for("threads.view", id=thread.id)) + + db.session.commit() + + return redirect(package.getDetailsURL()) + + return render_template("packages/review_create_edit.html", form=form, package=package) diff --git a/app/blueprints/threads/__init__.py b/app/blueprints/threads/__init__.py index a55d55e..c0b878c 100644 --- a/app/blueprints/threads/__init__.py +++ b/app/blueprints/threads/__init__.py @@ -206,7 +206,8 @@ def new(): notif_msg = None if package is not None: notif_msg = "New thread '{}' on package {}".format(thread.title, package.title) - triggerNotif(package.author, current_user, notif_msg, url_for("threads.view", id=thread.id)) + for maintainer in package.maintainers: + triggerNotif(maintainer, current_user, notif_msg, url_for("threads.view", id=thread.id)) else: notif_msg = "New thread '{}'".format(thread.title) diff --git a/app/models.py b/app/models.py index 80f6fea..7b07309 100644 --- a/app/models.py +++ b/app/models.py @@ -205,9 +205,17 @@ class User(db.Model, UserMixin): raise Exception("Permission {} is not related to users".format(perm.name)) def canCommentRL(self): + one_min_ago = datetime.datetime.utcnow() - datetime.timedelta(minutes=1) + if ThreadReply.query.filter_by(author=self) \ + .filter(ThreadReply.created_at > one_min_ago).count() >= 3: + return False + hour_ago = datetime.datetime.utcnow() - datetime.timedelta(hours=1) - return ThreadReply.query.filter_by(author=self) \ - .filter(ThreadReply.created_at > hour_ago).count() < 4 + if ThreadReply.query.filter_by(author=self) \ + .filter(ThreadReply.created_at > hour_ago).count() >= 20: + return False + + return True def canOpenThreadRL(self): hour_ago = datetime.datetime.utcnow() - datetime.timedelta(hours=1) @@ -1063,6 +1071,9 @@ class Thread(db.Model): package_id = db.Column(db.Integer, db.ForeignKey("package.id"), nullable=True) package = db.relationship("Package", foreign_keys=[package_id]) + review_id = db.Column(db.Integer, db.ForeignKey("package_review.id"), nullable=True) + review = db.relationship("PackageReview", foreign_keys=[review_id]) + author_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False) title = db.Column(db.String(100), nullable=False) private = db.Column(db.Boolean, server_default="0") @@ -1110,6 +1121,27 @@ class ThreadReply(db.Model): created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow) +class PackageReview(db.Model): + id = db.Column(db.Integer, primary_key=True) + + package_id = db.Column(db.Integer, db.ForeignKey("package.id"), nullable=True) + package = db.relationship("Package", foreign_keys=[package_id], backref=db.backref("reviews", lazy=True)) + + author_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False) + author = db.relationship("User", foreign_keys=[author_id], backref=db.backref("reviews", lazy=True)) + + recommends = db.Column(db.Boolean, nullable=False) + + thread = db.relationship("Thread", uselist=False, back_populates="review") + + def getEditURL(self): + return url_for("packages.edit_review", + author=self.package.author.username, + name=self.package.name, + id=self.id) + + + REPO_BLACKLIST = [".zip", "mediafire.com", "dropbox.com", "weebly.com", \ "minetest.net", "dropboxusercontent.com", "4shared.com", \ "digitalaudioconcepts.com", "hg.intevation.org", "www.wtfpl.net", \ diff --git a/app/templates/macros/reviews.html b/app/templates/macros/reviews.html new file mode 100644 index 0000000..fc6673e --- /dev/null +++ b/app/templates/macros/reviews.html @@ -0,0 +1,117 @@ +{% macro render_reviews(reviews) -%} +<ul class="comments mt-4 mb-0"> + {% for review in reviews %} + <li class="row my-2 mx-0"> + <div class="col-md-1 p-1"> + <a href="{{ url_for('users.profile', username=review.author.username) }}"> + <img class="img-responsive user-photo img-thumbnail img-thumbnail-1" src="{{ review.author.getProfilePicURL() }}"> + </a> + </div> + <div class="col-md-auto pl-1 pr-3 pt-2 text-center" style=" font-size: 200%;"> + {% if review.recommends %} + <i class="fas fa-thumbs-up" style="color:#6f6;"></i> + {% else %} + <i class="fas fa-thumbs-down" style="color:#f66;"></i> + {% endif %} + </div> + {% if review.thread %} + {% set reply = review.thread.replies[0] %} + <div class="col pr-0"> + <div class="card"> + <div class="card-header"> + <a class="author {{ reply.author.rank.name }}" + href="{{ url_for('users.profile', username=reply.author.username) }}"> + {{ reply.author.display_name }} + </a> + + <a name="reply-{{ reply.id }}" class="text-muted float-right" + href="{{ url_for('threads.view', id=review.thread.id) }}#reply-{{ reply.id }}"> + {{ reply.created_at | datetime }} + </a> + </div> + + <div class="card-body"> + <p> + <strong>{{ review.thread.title }}</strong> + </p> + + {{ reply.comment | markdown }} + + <a class="btn btn-primary" href="{{ url_for('threads.view', id=review.thread.id) }}"> + <i class="fas fa-comments mr-2"></i> + {{ _("%(num)d comments", num=review.thread.replies.count() - 1) }} + </a> + </div> + </div> + </div> + {% endif %} + </li> + {% endfor %} +</ul> +{% endmacro %} + + +{% macro render_review_form(package, current_user) -%} + <div class="card mt-0 mb-4 "> + <div class="card-header"> + {{ _("Review") }} + </div> + <form method="post" action="{{ url_for('packages.review', author=package.author.username, name=package.name) }}" class="card-body"> + <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" /> + <p> + {{ _("Do you recommend this %(type)s?", type=package.type.value | lower) }} + </p> + + <div class="btn-group btn-group-toggle" data-toggle="buttons"> + <label class="btn btn-primary"> + <i class="fas fa-thumbs-up mr-2"></i> + <input type="radio" name="recommends" id="yes" autocomplete="off"> {{ _("Yes") }} + </label> + <label class="btn btn-primary"> + <i class="fas fa-thumbs-down mr-2"></i> + <input type="radio" name="recommends" id="no" autocomplete="off"> {{ _("No") }} + </label> + </div> + + <p class="mt-4 mb-3"> + {{ _("Why or why not? Try to be constructive") }} + </p> + + <div class="form-group"> + <label for="title">{{ _("Title") }}</label> + <input class="form-control" id="title" name="title" required="" type="text"> + </div> + + <textarea class="form-control markdown" required maxlength=500 name="comment"></textarea><br /> + <input class="btn btn-primary" type="submit" value="{{ _('Post Review') }}" /> + </form> + </div> +{% endmacro %} + + +{% macro render_review_preview(package, current_user) -%} + <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" /> + <div class="card mt-0 mb-4 "> + <div class="card-header"> + {{ _("Review") }} + </div> + <div class="card-body"> + <p> + {{ _("Do you recommend this %(type)s?", type=package.type.value | lower) }} + </p> + + {% set review_url = url_for('packages.review', author=package.author.username, name=package.name) %} + + <div class="btn-group"> + <a class="btn btn-primary" href="{{ url_for('user.login', r=review_url) }}"> + <i class="fas fa-thumbs-up mr-2"></i> + {{ _("Yes") }} + </a> + <a class="btn btn-primary" href="{{ url_for('user.login', r=review_url) }}"> + <i class="fas fa-thumbs-down mr-2"></i> + {{ _("No") }} + </a> + </div> + </div> + </div> +{% endmacro %} diff --git a/app/templates/packages/review_create_edit.html b/app/templates/packages/review_create_edit.html new file mode 100644 index 0000000..2f475cd --- /dev/null +++ b/app/templates/packages/review_create_edit.html @@ -0,0 +1,45 @@ +{% extends "base.html" %} + +{% block title %} + {{ _("Review") }} +{% endblock %} + +{% block content %} + +<h1>{{ _("Review") }}</h1> + +{% from "macros/forms.html" import render_field, render_submit_field, render_radio_field %} +<form method="POST" action="" enctype="multipart/form-data"> + {{ form.hidden_tag() }} + <div class="row mt-0 mb-4 comments mx-0"> + <div class="col-md-1 p-1"> + <img class="img-responsive user-photo img-thumbnail img-thumbnail-1" src="{{ current_user.getProfilePicURL() }}"> + </div> + <div class="col"> + <div class="card"> + <div class="card-header {{ current_user.rank.name }}"> + {{ current_user.display_name }} + <a name="reply"></a> + </div> + <div class="card-body"> + <p> + {{ _("Do you recommend this %(type)s?", type=package.type.value | lower) }} + </p> + {{ render_radio_field(form.recommends) }} + + <p class="mt-4 mb-3"> + {{ _("Why or why not? Try to be constructive") }} + </p> + + {{ render_field(form.title) }} + {{ render_field(form.comment, label="", class_="m-0", fieldclass="form-control markdown") }} <br /> + {{ render_submit_field(form.submit) }} + </div> + </div> + </div> + </div> + +</form> + + +{% endblock %} diff --git a/app/templates/packages/view.html b/app/templates/packages/view.html index cd7ba9d..3a8084d 100644 --- a/app/templates/packages/view.html +++ b/app/templates/packages/view.html @@ -459,6 +459,16 @@ <div style="clear: both;"></div> + <h3>Ratings and Reviews</h3> + + {% from "macros/reviews.html" import render_reviews, render_review_form, render_review_preview %} + {% if current_user.is_authenticated %} + {{ render_review_form(package, current_user) }} + {% else %} + {{ render_review_preview(package) }} + {% endif %} + {{ render_reviews(package.reviews) }} + {# {% if current_user.is_authenticated or requests %} <h3>Edit Requests</h3> diff --git a/app/templates/threads/view.html b/app/templates/threads/view.html index 13097fe..b91f866 100644 --- a/app/templates/threads/view.html +++ b/app/templates/threads/view.html @@ -19,12 +19,21 @@ Threads {% endif %} {% endif %} - <h1>{% if thread.private %}🔒 {% endif %}{{ thread.title }}</h1> - - {% if thread.package or current_user.is_authenticated %} - {% if thread.package %} - <p>Package: <a href="{{ thread.package.getDetailsURL() }}">{{ thread.package.title }}</a></p> + <h1> + {% if thread.review %} + {% if thread.review.recommends %} + <i class="fas fa-thumbs-up mr-2" style="color:#6f6;"></i> + {% else %} + <i class="fas fa-thumbs-down mr-2" style="color:#f66;"></i> + {% endif %} {% endif %} + {% if thread.private %}🔒 {% endif %}{{ thread.title }} + </h1> + + {% if thread.package %} + <p> + Package: <a href="{{ thread.package.getDetailsURL() }}">{{ thread.package.title }}</a> + </p> {% endif %} {% if thread.private %} diff --git a/migrations/versions/4f2e19bc2a27_.py b/migrations/versions/4f2e19bc2a27_.py new file mode 100644 index 0000000..0b760fc --- /dev/null +++ b/migrations/versions/4f2e19bc2a27_.py @@ -0,0 +1,40 @@ +"""empty message + +Revision ID: 4f2e19bc2a27 +Revises: dd27f1311a90 +Create Date: 2020-07-09 00:35:35.066719 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '4f2e19bc2a27' +down_revision = 'dd27f1311a90' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('package_review', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('package_id', sa.Integer(), nullable=True), + sa.Column('author_id', sa.Integer(), nullable=False), + sa.Column('recommends', sa.Boolean(), nullable=False), + sa.ForeignKeyConstraint(['author_id'], ['user.id'], ), + sa.ForeignKeyConstraint(['package_id'], ['package.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.add_column('thread', sa.Column('review_id', sa.Integer(), nullable=True)) + op.create_foreign_key(None, 'thread', 'package_review', ['review_id'], ['id']) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'thread', type_='foreignkey') + op.drop_column('thread', 'review_id') + op.drop_table('package_review') + # ### end Alembic commands ### |