]> git.lizzy.rs Git - cheatdb.git/commitdiff
Add comment system
authorrubenwardy <rw@rubenwardy.com>
Mon, 11 Jun 2018 21:49:25 +0000 (22:49 +0100)
committerrubenwardy <rw@rubenwardy.com>
Mon, 11 Jun 2018 21:52:37 +0000 (22:52 +0100)
app/models.py
app/templates/macros/threads.html [new file with mode: 0644]
app/templates/packages/view.html
app/templates/threads/list.html [new file with mode: 0644]
app/templates/threads/new.html [new file with mode: 0644]
app/templates/threads/view.html [new file with mode: 0644]
app/views/__init__.py
app/views/packages/__init__.py
app/views/threads.py [new file with mode: 0644]
migrations/versions/605b3d74ada1_.py [new file with mode: 0644]

index 7f0d8f311fd293c29528d9f0a02fddd8e30a6b18..c76cb258ec163efbd206c9643c04a748417ab356 100644 (file)
@@ -76,6 +76,7 @@ class Permission(enum.Enum):
        CHANGE_RANK        = "CHANGE_RANK"
        CHANGE_EMAIL       = "CHANGE_EMAIL"
        EDIT_EDITREQUEST   = "EDIT_EDITREQUEST"
+       SEE_THREAD         = "SEE_THREAD"
 
        # Only return true if the permission is valid for *all* contexts
        # See Package.checkPerm for package-specific contexts
@@ -120,6 +121,8 @@ class User(db.Model, UserMixin):
        # causednotifs  = db.relationship("Notification", backref="causer", lazy="dynamic")
        packages      = db.relationship("Package", backref="author", lazy="dynamic")
        requests      = db.relationship("EditRequest", backref="author", lazy="dynamic")
+       threads       = db.relationship("Thread", backref="author", lazy="dynamic")
+       replies       = db.relationship("ThreadReply", backref="author", lazy="dynamic")
 
        def __init__(self, username, active=False, email=None, password=None):
                import datetime
@@ -337,6 +340,9 @@ class Package(db.Model):
        approved     = db.Column(db.Boolean, nullable=False, default=False)
        soft_deleted = db.Column(db.Boolean, nullable=False, default=False)
 
+       review_thread_id = db.Column(db.Integer, db.ForeignKey("thread.id"), nullable=True, default=None)
+       review_thread    = db.relationship("Thread", foreign_keys=[review_thread_id])
+
        # Downloads
        repo         = db.Column(db.String(200), nullable=True)
        website      = db.Column(db.String(200), nullable=True)
@@ -659,6 +665,49 @@ class EditRequestChange(db.Model):
                        setattr(package, self.key.name, self.newValue)
 
 
+class Thread(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])
+
+       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")
+
+       created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
+
+       replies    = db.relationship("ThreadReply", backref="thread", lazy="dynamic")
+
+       def checkPerm(self, user, perm):
+               if not user.is_authenticated:
+                       return not self.private
+
+               if type(perm) == str:
+                       perm = Permission[perm]
+               elif type(perm) != Permission:
+                       raise Exception("Unknown permission given to Thread.checkPerm()")
+
+               isOwner = user == self.author
+
+               if perm == Permission.SEE_THREAD:
+                       return not self.private or isOwner or user.rank.atLeast(UserRank.EDITOR)
+
+               else:
+                       raise Exception("Permission {} is not related to threads".format(perm.name))
+
+class ThreadReply(db.Model):
+       id         = db.Column(db.Integer, primary_key=True)
+       thread_id  = db.Column(db.Integer, db.ForeignKey("thread.id"), nullable=False)
+       comment    = db.Column(db.String(500), nullable=False)
+       author_id  = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
+       created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
+
+
+
+
+
+
 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/threads.html b/app/templates/macros/threads.html
new file mode 100644 (file)
index 0000000..96e1107
--- /dev/null
@@ -0,0 +1,27 @@
+{% macro render_thread(thread, current_user) -%}
+       <ul>
+               {% for r in thread.replies %}
+                       <li>
+                               &lt;<a href="{{ url_for('user_profile_page', username=r.author.username) }}">{{ r.author.display_name }}</a>&gt;
+                               {{ r.comment }}
+                       </li>
+               {% endfor %}
+       </ul>
+
+       {% if current_user.is_authenticated %}
+               <form method="post" action="{{ url_for('thread_page', id=thread.id)}}">
+                       <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
+                       <textarea required maxlength=500 name="comment"></textarea><br />
+                       <input type="submit" value="Comment" />
+               </form>
+       {% endif %}
+{% endmacro %}
+
+{% macro render_threadlist(threads) -%}
+       <ul>
+               {% for t in threads %}
+                       <li><a href="{{ url_for('thread_page', id=t.id) }}">{{ t.title }}</a> by {{ t.author.display_name }}</li>
+               {% endfor %}
+       </ul>
+
+{% endmacro %}
index 56cfd62182f74fe94f06105c78504011133d3867..3a83995907b4566d4cb723291f478b3926b59a8b 100644 (file)
                        {% endif %}
                        <div style="clear: both;"></div>
                </div>
+
+               {% if package.author == current_user or package.checkPerm(current_user, "APPROVE_NEW") %}
+                       {% if review_thread %}
+                               {% from "macros/threads.html" import render_thread %}
+                               {{ render_thread(review_thread, current_user) }}
+                       {% else %}
+                               <div class="box box_grey alert alert-info">
+                                       Privately ask a question or give feedback
+
+                                       <a class="alert_right button" href="{{ url_for('new_thread_page', pid=package.id, title='Package approval comments') }}">Open Thread</a>
+                               </div>
+                       {% endif %}
+               {% endif %}
        {% endif %}
 
        <h1>{{ package.title }} by {{ package.author.display_name }}</h1>
diff --git a/app/templates/threads/list.html b/app/templates/threads/list.html
new file mode 100644 (file)
index 0000000..7e27325
--- /dev/null
@@ -0,0 +1,12 @@
+{% extends "base.html" %}
+
+{% block title %}
+Threads
+{% endblock %}
+
+{% block content %}
+       <h1>Threads</h1>
+
+       {% from "macros/threads.html" import render_threadlist %}
+       {{ render_threadlist(threads) }}
+{% endblock %}
diff --git a/app/templates/threads/new.html b/app/templates/threads/new.html
new file mode 100644 (file)
index 0000000..22f5b72
--- /dev/null
@@ -0,0 +1,19 @@
+{% extends "base.html" %}
+
+{% block title %}
+       New Thread
+{% endblock %}
+
+{% block content %}
+       {% from "macros/forms.html" import render_field, render_submit_field %}
+       <form method="POST" action="" enctype="multipart/form-data">
+               {{ form.hidden_tag() }}
+
+               {{ render_field(form.title) }}
+               {{ render_field(form.comment) }}
+               {{ render_field(form.private) }}
+               {{ render_submit_field(form.submit) }}
+
+               <p>Only the you, the package author, and users of Editor rank and above can read private threads.</p>
+       </form>
+{% endblock %}
diff --git a/app/templates/threads/view.html b/app/templates/threads/view.html
new file mode 100644 (file)
index 0000000..397fba3
--- /dev/null
@@ -0,0 +1,25 @@
+{% extends "base.html" %}
+
+{% block title %}
+Threads
+{% endblock %}
+
+{% block content %}
+       <h1>{% 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 %}
+               <i>
+                       This thread is only visible to its creator, the package owner, and users of
+                       Editor rank or above.
+               </i>
+       {% endif %}
+
+       {% from "macros/threads.html" import render_thread %}
+       {{ render_thread(thread, current_user) }}
+{% endblock %}
index 09c7be81688fc2a5331be09c30b882310b7d96b9..5257675c8d861027b7349c6ffa927acc5e475947 100644 (file)
@@ -51,7 +51,7 @@ def home_page():
        packages = query.order_by(db.desc(Package.created_at)).limit(15).all()
        return render_template("index.html", packages=packages, count=count)
 
-from . import users, githublogin, packages, sass, tasks, admin, notifications, tagseditor, meta, thumbnails
+from . import users, githublogin, packages, sass, tasks, admin, notifications, tagseditor, meta, thumbnails, threads
 
 @menu.register_menu(app, ".help", "Help", order=19, endpoint_arguments_constructor=lambda: { 'path': 'help' })
 @app.route('/<path:path>/')
index 07f529bf0278876c62cef2bfd05b115b7e1b118c..0c22a7012c4e89d4b75b02a6d443e5031f57b0ac 100644 (file)
@@ -110,9 +110,15 @@ def package_page(package):
 
                releases = getReleases(package)
                requests = [r for r in package.requests if r.status == 0]
+
+               review_thread = Thread.query.filter_by(package_id=package.id, private=True).first()
+               if review_thread is not None and not review_thread.checkPerm(current_user, Permission.SEE_THREAD):
+                       review_thread = None
+
                return render_template("packages/view.html", \
                                package=package, releases=releases, requests=requests, \
-                               alternatives=alternatives, similar_topics=similar_topics)
+                               alternatives=alternatives, similar_topics=similar_topics, \
+                               review_thread=review_thread)
 
 
 @app.route("/packages/<author>/<name>/download/")
diff --git a/app/views/threads.py b/app/views/threads.py
new file mode 100644 (file)
index 0000000..e4973a5
--- /dev/null
@@ -0,0 +1,136 @@
+# Content DB
+# Copyright (C) 2018  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 flask import *
+from flask_user import *
+from app import app
+from app.models import *
+from app.utils import triggerNotif, clearNotifications
+
+from flask_wtf import FlaskForm
+from wtforms import *
+from wtforms.validators import *
+
+@app.route("/threads/")
+def threads_page():
+       threads = Thread.query.filter_by(private=False).all()
+       return render_template("threads/list.html", threads=threads)
+
+@app.route("/threads/<int:id>/", methods=["GET", "POST"])
+def thread_page(id):
+       clearNotifications(url_for("thread_page", id=id))
+
+       thread = Thread.query.get(id)
+       if thread is None or not thread.checkPerm(current_user, Permission.SEE_THREAD):
+               abort(404)
+
+       if current_user.is_authenticated and request.method == "POST":
+               comment = request.form["comment"]
+
+               if len(comment) <= 500 and len(comment) > 3:
+                       reply = ThreadReply()
+                       reply.author = current_user
+                       reply.comment = comment
+                       db.session.add(reply)
+
+                       thread.replies.append(reply)
+                       db.session.commit()
+
+                       return redirect(url_for("thread_page", id=id))
+
+               else:
+                       flash("Comment needs to be between 3 and 500 characters.")
+
+       return render_template("threads/view.html", thread=thread)
+
+
+class ThreadForm(FlaskForm):
+       title   = StringField("Title", [InputRequired(), Length(3,100)])
+       comment = TextAreaField("Comment", [InputRequired(), Length(10, 500)])
+       private = BooleanField("Private")
+       submit  = SubmitField("Open Thread")
+
+@app.route("/threads/new/", methods=["GET", "POST"])
+@login_required
+def new_thread_page():
+       form = ThreadForm(formdata=request.form)
+
+       package = None
+       if "pid" in request.args:
+               package = Package.query.get(int(request.args.get("pid")))
+               if package is None:
+                       flash("Unable to find that package!", "error")
+
+       if package is None:
+               abort(403)
+
+       def_is_private   = request.args.get("private") or False
+       if not package.approved:
+               def_is_private = True
+       allow_change     = package.approved
+       is_review_thread = package is not None and not package.approved
+
+       # Check that user can make the thread
+       if is_review_thread and not (package.author == current_user or \
+                       package.checkPerm(current_user, Permission.APPROVE_NEW)):
+               flash("Unable to create thread!", "error")
+               return redirect(url_for("home_page"))
+
+       # Only allow creating one thread when not approved
+       elif is_review_thread and package.review_thread is not None:
+               flash("A review thread already exists!", "error")
+               if request.method == "GET":
+                       return redirect(url_for("thread_page", id=package.review_thread.id))
+
+       # Set default values
+       elif request.method == "GET":
+               form.private.data = def_is_private
+               form.title.data   = request.args.get("title") or ""
+
+       # Validate and submit
+       elif request.method == "POST" and form.validate():
+               thread = Thread()
+               thread.author  = current_user
+               thread.title   = form.title.data
+               thread.private = form.private.data if allow_change else def_is_private
+               thread.package = package
+               db.session.add(thread)
+
+               reply = ThreadReply()
+               reply.thread  = thread
+               reply.author  = current_user
+               reply.comment = form.comment.data
+               db.session.add(reply)
+
+               thread.replies.append(reply)
+
+               db.session.commit()
+
+               if is_review_thread:
+                       package.review_thread = thread
+
+               if package is not None:
+                       triggerNotif(package.author, current_user,
+                                       "New thread '{}' on package {}".format(thread.title, package.title), url_for("thread_page", id=thread.id))
+                       db.session.commit()
+
+               db.session.commit()
+
+               return redirect(url_for("thread_page", id=thread.id))
+
+
+       return render_template("threads/new.html", form=form, allow_private_change=allow_change)
diff --git a/migrations/versions/605b3d74ada1_.py b/migrations/versions/605b3d74ada1_.py
new file mode 100644 (file)
index 0000000..acd0f2c
--- /dev/null
@@ -0,0 +1,55 @@
+"""empty message
+
+Revision ID: 605b3d74ada1
+Revises: 28a427cbd4cf
+Create Date: 2018-06-11 22:50:36.828818
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '605b3d74ada1'
+down_revision = '28a427cbd4cf'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.create_table('thread',
+    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('title', sa.String(length=100), nullable=False),
+    sa.Column('private', sa.Boolean(), server_default='0', nullable=True),
+    sa.Column('created_at', sa.DateTime(), nullable=False),
+    sa.ForeignKeyConstraint(['author_id'], ['user.id'], ),
+    sa.ForeignKeyConstraint(['package_id'], ['package.id'], ),
+    sa.PrimaryKeyConstraint('id')
+    )
+    op.create_table('thread_reply',
+    sa.Column('id', sa.Integer(), nullable=False),
+    sa.Column('thread_id', sa.Integer(), nullable=False),
+    sa.Column('comment', sa.String(length=500), nullable=False),
+    sa.Column('author_id', sa.Integer(), nullable=False),
+    sa.Column('created_at', sa.DateTime(), nullable=False),
+    sa.ForeignKeyConstraint(['author_id'], ['user.id'], ),
+    sa.ForeignKeyConstraint(['thread_id'], ['thread.id'], ),
+    sa.PrimaryKeyConstraint('id')
+    )
+
+    op.add_column('package', sa.Column('review_thread_id', sa.Integer(), nullable=True))
+    op.create_foreign_key(None, 'package', 'thread', ['review_thread_id'], ['id'])
+    # ### end Alembic commands ###
+
+
+def downgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.drop_constraint(None, 'package', type_='foreignkey')
+    op.drop_constraint(None, 'package', type_='foreignkey')
+    op.drop_column('package', 'review_thread_id')
+    op.drop_table('thread_reply')
+    op.drop_table('thread')
+    # ### end Alembic commands ###