2020-07-12 17:34:25 +02:00
|
|
|
# ContentDB
|
2018-06-11 23:49:25 +02:00
|
|
|
# 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 *
|
2019-11-16 00:51:42 +01:00
|
|
|
|
|
|
|
bp = Blueprint("threads", __name__)
|
|
|
|
|
2018-06-11 23:49:25 +02:00
|
|
|
from flask_user import *
|
|
|
|
from app.models import *
|
2020-07-11 03:32:17 +02:00
|
|
|
from app.utils import addNotification, clearNotifications, isYes, addAuditLog
|
2018-06-11 23:49:25 +02:00
|
|
|
|
2019-01-28 20:01:37 +01:00
|
|
|
import datetime
|
|
|
|
|
2018-06-11 23:49:25 +02:00
|
|
|
from flask_wtf import FlaskForm
|
|
|
|
from wtforms import *
|
|
|
|
from wtforms.validators import *
|
2020-01-21 23:40:51 +01:00
|
|
|
from app.utils import get_int_or_abort
|
2018-06-11 23:49:25 +02:00
|
|
|
|
2019-11-16 00:51:42 +01:00
|
|
|
@bp.route("/threads/")
|
|
|
|
def list_all():
|
2018-07-13 22:28:08 +02:00
|
|
|
query = Thread.query
|
|
|
|
if not Permission.SEE_THREAD.check(current_user):
|
|
|
|
query = query.filter_by(private=False)
|
2020-01-21 23:40:51 +01:00
|
|
|
|
|
|
|
pid = request.args.get("pid")
|
|
|
|
if pid:
|
|
|
|
pid = get_int_or_abort(pid)
|
|
|
|
query = query.filter_by(package_id=pid)
|
|
|
|
|
2020-07-10 20:46:14 +02:00
|
|
|
query = query.order_by(db.desc(Thread.created_at))
|
|
|
|
|
2018-07-13 22:28:08 +02:00
|
|
|
return render_template("threads/list.html", threads=query.all())
|
2018-06-11 23:49:25 +02:00
|
|
|
|
2018-07-28 16:30:59 +02:00
|
|
|
|
2019-11-16 00:51:42 +01:00
|
|
|
@bp.route("/threads/<int:id>/subscribe/", methods=["POST"])
|
2018-07-28 16:30:59 +02:00
|
|
|
@login_required
|
2019-11-16 00:51:42 +01:00
|
|
|
def subscribe(id):
|
2018-07-28 16:30:59 +02:00
|
|
|
thread = Thread.query.get(id)
|
|
|
|
if thread is None or not thread.checkPerm(current_user, Permission.SEE_THREAD):
|
|
|
|
abort(404)
|
|
|
|
|
|
|
|
if current_user in thread.watchers:
|
|
|
|
flash("Already subscribed!", "success")
|
|
|
|
else:
|
|
|
|
flash("Subscribed to thread", "success")
|
|
|
|
thread.watchers.append(current_user)
|
|
|
|
db.session.commit()
|
|
|
|
|
2020-07-11 02:34:51 +02:00
|
|
|
return redirect(thread.getViewURL())
|
2018-07-28 16:30:59 +02:00
|
|
|
|
|
|
|
|
2019-11-16 00:51:42 +01:00
|
|
|
@bp.route("/threads/<int:id>/unsubscribe/", methods=["POST"])
|
2018-07-28 16:30:59 +02:00
|
|
|
@login_required
|
2019-11-16 00:51:42 +01:00
|
|
|
def unsubscribe(id):
|
2018-07-28 16:30:59 +02:00
|
|
|
thread = Thread.query.get(id)
|
|
|
|
if thread is None or not thread.checkPerm(current_user, Permission.SEE_THREAD):
|
|
|
|
abort(404)
|
|
|
|
|
|
|
|
if current_user in thread.watchers:
|
|
|
|
flash("Unsubscribed!", "success")
|
|
|
|
thread.watchers.remove(current_user)
|
|
|
|
db.session.commit()
|
|
|
|
else:
|
2020-07-11 02:34:51 +02:00
|
|
|
flash("Already not subscribed!", "success")
|
2018-07-28 16:30:59 +02:00
|
|
|
|
2020-07-11 02:34:51 +02:00
|
|
|
return redirect(thread.getViewURL())
|
|
|
|
|
|
|
|
|
|
|
|
@bp.route("/threads/<int:id>/set-lock/", methods=["POST"])
|
|
|
|
@login_required
|
|
|
|
def set_lock(id):
|
|
|
|
thread = Thread.query.get(id)
|
|
|
|
if thread is None or not thread.checkPerm(current_user, Permission.LOCK_THREAD):
|
|
|
|
abort(404)
|
|
|
|
|
|
|
|
thread.locked = isYes(request.args.get("lock"))
|
|
|
|
if thread.locked is None:
|
|
|
|
abort(400)
|
|
|
|
|
2020-07-11 03:32:17 +02:00
|
|
|
msg = None
|
2020-07-11 02:34:51 +02:00
|
|
|
if thread.locked:
|
2020-07-11 03:32:17 +02:00
|
|
|
msg = "Locked thread '{}'".format(thread.title)
|
2020-07-11 02:34:51 +02:00
|
|
|
flash("Locked thread", "success")
|
|
|
|
else:
|
2020-07-11 03:32:17 +02:00
|
|
|
msg = "Unlocked thread '{}'".format(thread.title)
|
2020-07-11 02:34:51 +02:00
|
|
|
flash("Unlocked thread", "success")
|
|
|
|
|
2020-07-11 03:32:17 +02:00
|
|
|
addNotification(thread.watchers, current_user, msg, thread.getViewURL(), thread.package)
|
|
|
|
addAuditLog(AuditSeverity.MODERATION, current_user, msg, thread.getViewURL(), thread.package)
|
|
|
|
|
|
|
|
db.session.commit()
|
|
|
|
|
2020-07-11 02:34:51 +02:00
|
|
|
return redirect(thread.getViewURL())
|
2018-07-28 16:30:59 +02:00
|
|
|
|
|
|
|
|
2020-07-11 04:29:33 +02:00
|
|
|
@bp.route("/threads/<int:id>/delete/", methods=["GET", "POST"])
|
|
|
|
@login_required
|
|
|
|
def delete_reply(id):
|
|
|
|
thread = Thread.query.get(id)
|
|
|
|
if thread is None:
|
|
|
|
abort(404)
|
|
|
|
|
|
|
|
reply_id = request.args.get("reply")
|
|
|
|
if reply_id is None:
|
|
|
|
abort(404)
|
|
|
|
|
|
|
|
reply = ThreadReply.query.get(reply_id)
|
|
|
|
if reply is None or reply.thread != thread:
|
|
|
|
abort(404)
|
|
|
|
|
|
|
|
if thread.replies[0] == reply:
|
|
|
|
flash("Cannot delete thread opening post!", "danger")
|
|
|
|
return redirect(thread.getViewURL())
|
|
|
|
|
2020-07-11 04:35:14 +02:00
|
|
|
if not reply.checkPerm(current_user, Permission.DELETE_REPLY):
|
2020-07-11 04:29:33 +02:00
|
|
|
abort(403)
|
|
|
|
|
|
|
|
if request.method == "GET":
|
|
|
|
return render_template("threads/delete_reply.html", thread=thread, reply=reply)
|
|
|
|
|
|
|
|
msg = "Deleted reply by {}".format(reply.author.display_name)
|
|
|
|
addAuditLog(AuditSeverity.MODERATION, current_user, msg, thread.getViewURL(), thread.package, reply.comment)
|
|
|
|
|
|
|
|
db.session.delete(reply)
|
|
|
|
db.session.commit()
|
|
|
|
|
|
|
|
return redirect(thread.getViewURL())
|
|
|
|
|
|
|
|
|
2020-07-11 04:52:56 +02:00
|
|
|
class CommentForm(FlaskForm):
|
2020-07-15 17:01:33 +02:00
|
|
|
comment = TextAreaField("Comment", [InputRequired(), Length(10, 2000)])
|
2020-07-11 04:52:56 +02:00
|
|
|
submit = SubmitField("Comment")
|
|
|
|
|
|
|
|
|
|
|
|
@bp.route("/threads/<int:id>/edit/", methods=["GET", "POST"])
|
|
|
|
@login_required
|
|
|
|
def edit_reply(id):
|
|
|
|
thread = Thread.query.get(id)
|
|
|
|
if thread is None:
|
|
|
|
abort(404)
|
|
|
|
|
|
|
|
reply_id = request.args.get("reply")
|
|
|
|
if reply_id is None:
|
|
|
|
abort(404)
|
|
|
|
|
|
|
|
reply = ThreadReply.query.get(reply_id)
|
|
|
|
if reply is None or reply.thread != thread:
|
|
|
|
abort(404)
|
|
|
|
|
|
|
|
if not reply.checkPerm(current_user, Permission.EDIT_REPLY):
|
|
|
|
abort(403)
|
|
|
|
|
|
|
|
form = CommentForm(formdata=request.form, obj=reply)
|
|
|
|
if request.method == "POST" and form.validate():
|
|
|
|
comment = form.comment.data
|
|
|
|
|
|
|
|
msg = "Edited reply by {}".format(reply.author.display_name)
|
|
|
|
severity = AuditSeverity.NORMAL if current_user == reply.author else AuditSeverity.MODERATION
|
|
|
|
addNotification(reply.author, current_user, msg, thread.getViewURL(), thread.package)
|
|
|
|
addAuditLog(severity, current_user, msg, thread.getViewURL(), thread.package, reply.comment)
|
|
|
|
|
|
|
|
reply.comment = comment
|
|
|
|
|
|
|
|
db.session.commit()
|
|
|
|
|
|
|
|
return redirect(thread.getViewURL())
|
|
|
|
|
|
|
|
return render_template("threads/edit_reply.html", thread=thread, reply=reply, form=form)
|
|
|
|
|
|
|
|
|
2019-11-16 00:51:42 +01:00
|
|
|
@bp.route("/threads/<int:id>/", methods=["GET", "POST"])
|
|
|
|
def view(id):
|
2018-06-11 23:49:25 +02:00
|
|
|
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"]
|
|
|
|
|
2020-07-11 02:34:51 +02:00
|
|
|
if not thread.checkPerm(current_user, Permission.COMMENT_THREAD):
|
|
|
|
flash("You cannot comment on this thread", "danger")
|
|
|
|
return redirect(thread.getViewURL())
|
|
|
|
|
2019-01-28 20:01:37 +01:00
|
|
|
if not current_user.canCommentRL():
|
|
|
|
flash("Please wait before commenting again", "danger")
|
2020-07-11 02:34:51 +02:00
|
|
|
return redirect(thread.getViewURL())
|
2019-01-28 20:01:37 +01:00
|
|
|
|
2018-06-11 23:49:25 +02:00
|
|
|
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)
|
2018-06-12 00:38:03 +02:00
|
|
|
if not current_user in thread.watchers:
|
|
|
|
thread.watchers.append(current_user)
|
|
|
|
|
2020-07-11 01:53:03 +02:00
|
|
|
msg = "New comment on '{}'".format(thread.title)
|
2020-07-11 03:32:17 +02:00
|
|
|
addNotification(thread.watchers, current_user, msg, thread.getViewURL(), thread.package)
|
2018-06-11 23:49:25 +02:00
|
|
|
db.session.commit()
|
|
|
|
|
2020-07-11 03:32:17 +02:00
|
|
|
return redirect(thread.getViewURL())
|
2018-06-11 23:49:25 +02:00
|
|
|
|
|
|
|
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)])
|
2020-07-15 17:01:33 +02:00
|
|
|
comment = TextAreaField("Comment", [InputRequired(), Length(10, 2000)])
|
2018-06-11 23:49:25 +02:00
|
|
|
private = BooleanField("Private")
|
|
|
|
submit = SubmitField("Open Thread")
|
|
|
|
|
2020-07-11 04:29:33 +02:00
|
|
|
|
2019-11-16 00:51:42 +01:00
|
|
|
@bp.route("/threads/new/", methods=["GET", "POST"])
|
2018-06-11 23:49:25 +02:00
|
|
|
@login_required
|
2019-11-16 00:51:42 +01:00
|
|
|
def new():
|
2018-06-11 23:49:25 +02:00
|
|
|
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:
|
2020-01-24 19:15:09 +01:00
|
|
|
flash("Unable to find that package!", "danger")
|
2018-06-11 23:49:25 +02:00
|
|
|
|
2019-01-28 20:01:37 +01:00
|
|
|
# Don't allow making orphan threads on approved packages for now
|
2018-07-28 16:19:30 +02:00
|
|
|
if package is None:
|
2018-06-11 23:49:25 +02:00
|
|
|
abort(403)
|
|
|
|
|
|
|
|
def_is_private = request.args.get("private") or False
|
2019-11-12 23:39:17 +01:00
|
|
|
if package is None:
|
2018-06-11 23:49:25 +02:00
|
|
|
def_is_private = True
|
2019-01-28 20:01:37 +01:00
|
|
|
allow_change = package and package.approved
|
|
|
|
is_review_thread = package and not package.approved
|
2018-06-11 23:49:25 +02:00
|
|
|
|
|
|
|
# Check that user can make the thread
|
2018-07-28 16:19:30 +02:00
|
|
|
if not package.checkPerm(current_user, Permission.CREATE_THREAD):
|
2020-01-24 19:15:09 +01:00
|
|
|
flash("Unable to create thread!", "danger")
|
2019-11-21 20:38:26 +01:00
|
|
|
return redirect(url_for("homepage.home"))
|
2018-06-11 23:49:25 +02:00
|
|
|
|
|
|
|
# Only allow creating one thread when not approved
|
|
|
|
elif is_review_thread and package.review_thread is not None:
|
2020-01-24 19:15:09 +01:00
|
|
|
flash("A review thread already exists!", "danger")
|
2020-07-11 03:32:17 +02:00
|
|
|
return redirect(package.review_thread.getViewURL())
|
2019-01-28 20:01:37 +01:00
|
|
|
|
|
|
|
elif not current_user.canOpenThreadRL():
|
|
|
|
flash("Please wait before opening another thread", "danger")
|
|
|
|
|
|
|
|
if package:
|
|
|
|
return redirect(package.getDetailsURL())
|
|
|
|
else:
|
2019-11-21 20:38:26 +01:00
|
|
|
return redirect(url_for("homepage.home"))
|
2018-06-11 23:49:25 +02:00
|
|
|
|
|
|
|
# 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)
|
|
|
|
|
2018-06-12 00:38:03 +02:00
|
|
|
thread.watchers.append(current_user)
|
|
|
|
if package is not None and package.author != current_user:
|
|
|
|
thread.watchers.append(package.author)
|
|
|
|
|
2018-06-11 23:49:25 +02:00
|
|
|
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
|
|
|
|
|
2020-07-11 01:53:03 +02:00
|
|
|
notif_msg = "New thread '{}'".format(thread.title)
|
2018-06-11 23:49:25 +02:00
|
|
|
if package is not None:
|
2020-07-11 03:32:17 +02:00
|
|
|
addNotification(package.maintainers, current_user, notif_msg, thread.getViewURL(), package)
|
2019-01-28 20:01:37 +01:00
|
|
|
|
2020-07-09 05:16:45 +02:00
|
|
|
editors = User.query.filter(User.rank >= UserRank.EDITOR).all()
|
2020-07-11 03:32:17 +02:00
|
|
|
addNotification(editors, current_user, notif_msg, thread.getViewURL(), package)
|
2018-06-11 23:49:25 +02:00
|
|
|
|
|
|
|
db.session.commit()
|
|
|
|
|
2020-07-11 03:32:17 +02:00
|
|
|
return redirect(thread.getViewURL())
|
2018-06-11 23:49:25 +02:00
|
|
|
|
|
|
|
|
2019-01-28 20:41:24 +01:00
|
|
|
return render_template("threads/new.html", form=form, allow_private_change=allow_change, package=package)
|