diff --git a/app/blueprints/packages/__init__.py b/app/blueprints/packages/__init__.py index e4fc4f2f..99616bf2 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 00000000..4322e8d4 --- /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 . + +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///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 a55d55e3..c0b878cc 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 80f6fea7..7b073096 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 00000000..fc6673e5 --- /dev/null +++ b/app/templates/macros/reviews.html @@ -0,0 +1,117 @@ +{% macro render_reviews(reviews) -%} + +{% endmacro %} + + +{% macro render_review_form(package, current_user) -%} +
+
+ {{ _("Review") }} +
+
+ +

+ {{ _("Do you recommend this %(type)s?", type=package.type.value | lower) }} +

+ +
+ + +
+ +

+ {{ _("Why or why not? Try to be constructive") }} +

+ +
+ + +
+ +
+ +
+
+{% endmacro %} + + +{% macro render_review_preview(package, current_user) -%} + +
+
+ {{ _("Review") }} +
+
+

+ {{ _("Do you recommend this %(type)s?", type=package.type.value | lower) }} +

+ + {% set review_url = url_for('packages.review', author=package.author.username, name=package.name) %} + + +
+
+{% endmacro %} diff --git a/app/templates/packages/review_create_edit.html b/app/templates/packages/review_create_edit.html new file mode 100644 index 00000000..2f475cd2 --- /dev/null +++ b/app/templates/packages/review_create_edit.html @@ -0,0 +1,45 @@ +{% extends "base.html" %} + +{% block title %} + {{ _("Review") }} +{% endblock %} + +{% block content %} + +

{{ _("Review") }}

+ +{% from "macros/forms.html" import render_field, render_submit_field, render_radio_field %} +
+ {{ form.hidden_tag() }} +
+
+ +
+
+
+
+ {{ current_user.display_name }} + +
+
+

+ {{ _("Do you recommend this %(type)s?", type=package.type.value | lower) }} +

+ {{ render_radio_field(form.recommends) }} + +

+ {{ _("Why or why not? Try to be constructive") }} +

+ + {{ render_field(form.title) }} + {{ render_field(form.comment, label="", class_="m-0", fieldclass="form-control markdown") }}
+ {{ render_submit_field(form.submit) }} +
+
+
+
+ +
+ + +{% endblock %} diff --git a/app/templates/packages/view.html b/app/templates/packages/view.html index cd7ba9dc..3a8084d0 100644 --- a/app/templates/packages/view.html +++ b/app/templates/packages/view.html @@ -459,6 +459,16 @@
+

Ratings and Reviews

+ + {% 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 %}

Edit Requests

diff --git a/app/templates/threads/view.html b/app/templates/threads/view.html index 13097fea..b91f866a 100644 --- a/app/templates/threads/view.html +++ b/app/templates/threads/view.html @@ -19,12 +19,21 @@ Threads {% endif %} {% endif %} -

{% if thread.private %}🔒 {% endif %}{{ thread.title }}

- - {% if thread.package or current_user.is_authenticated %} - {% if thread.package %} -

Package: {{ thread.package.title }}

+

+ {% if thread.review %} + {% if thread.review.recommends %} + + {% else %} + + {% endif %} {% endif %} + {% if thread.private %}🔒 {% endif %}{{ thread.title }} +

+ + {% if thread.package %} +

+ Package: {{ thread.package.title }} +

{% endif %} {% if thread.private %} diff --git a/migrations/versions/4f2e19bc2a27_.py b/migrations/versions/4f2e19bc2a27_.py new file mode 100644 index 00000000..0b760fcb --- /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 ###