2 # Copyright (C) 2018 rubenwardy
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation, either version 3 of the License, or
7 # (at your option) any later version.
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with this program. If not, see <https://www.gnu.org/licenses/>.
20 bp = Blueprint("threads", __name__)
22 from flask_user import *
23 from app.models import *
24 from app.utils import addNotification, clearNotifications, isYes, addAuditLog
28 from flask_wtf import FlaskForm
30 from wtforms.validators import *
31 from app.utils import get_int_or_abort
33 @bp.route("/threads/")
36 if not Permission.SEE_THREAD.check(current_user):
37 query = query.filter_by(private=False)
39 pid = request.args.get("pid")
41 pid = get_int_or_abort(pid)
42 query = query.filter_by(package_id=pid)
44 query = query.order_by(db.desc(Thread.created_at))
46 return render_template("threads/list.html", threads=query.all())
49 @bp.route("/threads/<int:id>/subscribe/", methods=["POST"])
52 thread = Thread.query.get(id)
53 if thread is None or not thread.checkPerm(current_user, Permission.SEE_THREAD):
56 if current_user in thread.watchers:
57 flash("Already subscribed!", "success")
59 flash("Subscribed to thread", "success")
60 thread.watchers.append(current_user)
63 return redirect(thread.getViewURL())
66 @bp.route("/threads/<int:id>/unsubscribe/", methods=["POST"])
69 thread = Thread.query.get(id)
70 if thread is None or not thread.checkPerm(current_user, Permission.SEE_THREAD):
73 if current_user in thread.watchers:
74 flash("Unsubscribed!", "success")
75 thread.watchers.remove(current_user)
78 flash("Already not subscribed!", "success")
80 return redirect(thread.getViewURL())
83 @bp.route("/threads/<int:id>/set-lock/", methods=["POST"])
86 thread = Thread.query.get(id)
87 if thread is None or not thread.checkPerm(current_user, Permission.LOCK_THREAD):
90 thread.locked = isYes(request.args.get("lock"))
91 if thread.locked is None:
96 msg = "Locked thread '{}'".format(thread.title)
97 flash("Locked thread", "success")
99 msg = "Unlocked thread '{}'".format(thread.title)
100 flash("Unlocked thread", "success")
102 addNotification(thread.watchers, current_user, msg, thread.getViewURL(), thread.package)
103 addAuditLog(AuditSeverity.MODERATION, current_user, msg, thread.getViewURL(), thread.package)
107 return redirect(thread.getViewURL())
110 @bp.route("/threads/<int:id>/delete/", methods=["GET", "POST"])
112 def delete_reply(id):
113 thread = Thread.query.get(id)
117 reply_id = request.args.get("reply")
121 reply = ThreadReply.query.get(reply_id)
122 if reply is None or reply.thread != thread:
125 if thread.replies[0] == reply:
126 flash("Cannot delete thread opening post!", "danger")
127 return redirect(thread.getViewURL())
129 if not reply.checkPerm(current_user, Permission.DELETE_REPLY):
132 if request.method == "GET":
133 return render_template("threads/delete_reply.html", thread=thread, reply=reply)
135 msg = "Deleted reply by {}".format(reply.author.display_name)
136 addAuditLog(AuditSeverity.MODERATION, current_user, msg, thread.getViewURL(), thread.package, reply.comment)
138 db.session.delete(reply)
141 return redirect(thread.getViewURL())
144 class CommentForm(FlaskForm):
145 comment = TextAreaField("Comment", [InputRequired(), Length(10, 2000)])
146 submit = SubmitField("Comment")
149 @bp.route("/threads/<int:id>/edit/", methods=["GET", "POST"])
152 thread = Thread.query.get(id)
156 reply_id = request.args.get("reply")
160 reply = ThreadReply.query.get(reply_id)
161 if reply is None or reply.thread != thread:
164 if not reply.checkPerm(current_user, Permission.EDIT_REPLY):
167 form = CommentForm(formdata=request.form, obj=reply)
168 if request.method == "POST" and form.validate():
169 comment = form.comment.data
171 msg = "Edited reply by {}".format(reply.author.display_name)
172 severity = AuditSeverity.NORMAL if current_user == reply.author else AuditSeverity.MODERATION
173 addNotification(reply.author, current_user, msg, thread.getViewURL(), thread.package)
174 addAuditLog(severity, current_user, msg, thread.getViewURL(), thread.package, reply.comment)
176 reply.comment = comment
180 return redirect(thread.getViewURL())
182 return render_template("threads/edit_reply.html", thread=thread, reply=reply, form=form)
185 @bp.route("/threads/<int:id>/", methods=["GET", "POST"])
187 thread = Thread.query.get(id)
188 if thread is None or not thread.checkPerm(current_user, Permission.SEE_THREAD):
191 if current_user.is_authenticated and request.method == "POST":
192 comment = request.form["comment"]
194 if not thread.checkPerm(current_user, Permission.COMMENT_THREAD):
195 flash("You cannot comment on this thread", "danger")
196 return redirect(thread.getViewURL())
198 if not current_user.canCommentRL():
199 flash("Please wait before commenting again", "danger")
200 return redirect(thread.getViewURL())
202 if len(comment) <= 2000 and len(comment) > 3:
203 reply = ThreadReply()
204 reply.author = current_user
205 reply.comment = comment
206 db.session.add(reply)
208 thread.replies.append(reply)
209 if not current_user in thread.watchers:
210 thread.watchers.append(current_user)
212 msg = "New comment on '{}'".format(thread.title)
213 addNotification(thread.watchers, current_user, msg, thread.getViewURL(), thread.package)
216 return redirect(thread.getViewURL())
219 flash("Comment needs to be between 3 and 2000 characters.")
221 return render_template("threads/view.html", thread=thread)
224 class ThreadForm(FlaskForm):
225 title = StringField("Title", [InputRequired(), Length(3,100)])
226 comment = TextAreaField("Comment", [InputRequired(), Length(10, 2000)])
227 private = BooleanField("Private")
228 submit = SubmitField("Open Thread")
231 @bp.route("/threads/new/", methods=["GET", "POST"])
234 form = ThreadForm(formdata=request.form)
237 if "pid" in request.args:
238 package = Package.query.get(int(request.args.get("pid")))
240 flash("Unable to find that package!", "danger")
242 # Don't allow making orphan threads on approved packages for now
246 def_is_private = request.args.get("private") or False
248 def_is_private = True
249 allow_change = package and package.approved
250 is_review_thread = package and not package.approved
252 # Check that user can make the thread
253 if not package.checkPerm(current_user, Permission.CREATE_THREAD):
254 flash("Unable to create thread!", "danger")
255 return redirect(url_for("homepage.home"))
257 # Only allow creating one thread when not approved
258 elif is_review_thread and package.review_thread is not None:
259 flash("A review thread already exists!", "danger")
260 return redirect(package.review_thread.getViewURL())
262 elif not current_user.canOpenThreadRL():
263 flash("Please wait before opening another thread", "danger")
266 return redirect(package.getDetailsURL())
268 return redirect(url_for("homepage.home"))
271 elif request.method == "GET":
272 form.private.data = def_is_private
273 form.title.data = request.args.get("title") or ""
275 # Validate and submit
276 elif request.method == "POST" and form.validate():
278 thread.author = current_user
279 thread.title = form.title.data
280 thread.private = form.private.data if allow_change else def_is_private
281 thread.package = package
282 db.session.add(thread)
284 thread.watchers.append(current_user)
285 if package is not None and package.author != current_user:
286 thread.watchers.append(package.author)
288 reply = ThreadReply()
289 reply.thread = thread
290 reply.author = current_user
291 reply.comment = form.comment.data
292 db.session.add(reply)
294 thread.replies.append(reply)
299 package.review_thread = thread
301 notif_msg = "New thread '{}'".format(thread.title)
302 if package is not None:
303 addNotification(package.maintainers, current_user, notif_msg, thread.getViewURL(), package)
305 editors = User.query.filter(User.rank >= UserRank.EDITOR).all()
306 addNotification(editors, current_user, notif_msg, thread.getViewURL(), package)
310 return redirect(thread.getViewURL())
313 return render_template("threads/new.html", form=form, allow_private_change=allow_change, package=package)