]> git.lizzy.rs Git - cheatdb.git/commitdiff
Add reviews
authorrubenwardy <rw@rubenwardy.com>
Thu, 9 Jul 2020 03:10:09 +0000 (04:10 +0100)
committerrubenwardy <rw@rubenwardy.com>
Thu, 9 Jul 2020 03:10:09 +0000 (04:10 +0100)
Fixes #173

app/blueprints/packages/__init__.py
app/blueprints/packages/reviews.py [new file with mode: 0644]
app/blueprints/threads/__init__.py
app/models.py
app/templates/macros/reviews.html [new file with mode: 0644]
app/templates/packages/review_create_edit.html [new file with mode: 0644]
app/templates/packages/view.html
app/templates/threads/view.html
migrations/versions/4f2e19bc2a27_.py [new file with mode: 0644]

index e4fc4f2f565589500e8b2c7b3d0a1a262b23cf7c..99616bf23c1411d2014df1a036a86489f1a31461 100644 (file)
@@ -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 (file)
index 0000000..4322e8d
--- /dev/null
@@ -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)
index a55d55e3364eae54f26cae9659f0acd4ffc43168..c0b878ccb329f15556b04e1febcb90b670db45eb 100644 (file)
@@ -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)
 
index 80f6fea76386bbd30613aea16aac1a8fbac85d5d..7b0730966c2b44ec5952c789c89a85d81de9c596 100644 (file)
@@ -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 (file)
index 0000000..fc6673e
--- /dev/null
@@ -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 (file)
index 0000000..2f475cd
--- /dev/null
@@ -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 %}
index cd7ba9dc5d2a13aabced2feaef270701796c7882..3a8084d0f916939b412f87a0a44ef8d1cf6d746b 100644 (file)
 
                <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>
index 13097feabc7a2f94d9fb38e031b4ffc5e6bf3c77..b91f866affc3037b4dbaeac0c73d70eb269766d1 100644 (file)
@@ -19,12 +19,21 @@ Threads
                {% endif %}
        {% endif %}
 
-       <h1>{% if thread.private %}&#x1f512; {% 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 %}&#x1f512; {% 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 (file)
index 0000000..0b760fc
--- /dev/null
@@ -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 ###