]> git.lizzy.rs Git - cheatdb.git/blob - app/blueprints/threads/__init__.py
6a2fdb8516efd0fcef3fd18ff66946029d6b6774
[cheatdb.git] / app / blueprints / threads / __init__.py
1 # ContentDB
2 # Copyright (C) 2018  rubenwardy
3 #
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.
8 #
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.
13 #
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/>.
16
17
18 from flask import *
19
20 bp = Blueprint("threads", __name__)
21
22 from flask_user import *
23 from app.models import *
24 from app.utils import addNotification, clearNotifications, isYes, addAuditLog
25
26 import datetime
27
28 from flask_wtf import FlaskForm
29 from wtforms import *
30 from wtforms.validators import *
31 from app.utils import get_int_or_abort
32
33 @bp.route("/threads/")
34 def list_all():
35         query = Thread.query
36         if not Permission.SEE_THREAD.check(current_user):
37                 query = query.filter_by(private=False)
38
39         pid = request.args.get("pid")
40         if pid:
41                 pid = get_int_or_abort(pid)
42                 query = query.filter_by(package_id=pid)
43
44         query = query.order_by(db.desc(Thread.created_at))
45
46         return render_template("threads/list.html", threads=query.all())
47
48
49 @bp.route("/threads/<int:id>/subscribe/", methods=["POST"])
50 @login_required
51 def subscribe(id):
52         thread = Thread.query.get(id)
53         if thread is None or not thread.checkPerm(current_user, Permission.SEE_THREAD):
54                 abort(404)
55
56         if current_user in thread.watchers:
57                 flash("Already subscribed!", "success")
58         else:
59                 flash("Subscribed to thread", "success")
60                 thread.watchers.append(current_user)
61                 db.session.commit()
62
63         return redirect(thread.getViewURL())
64
65
66 @bp.route("/threads/<int:id>/unsubscribe/", methods=["POST"])
67 @login_required
68 def unsubscribe(id):
69         thread = Thread.query.get(id)
70         if thread is None or not thread.checkPerm(current_user, Permission.SEE_THREAD):
71                 abort(404)
72
73         if current_user in thread.watchers:
74                 flash("Unsubscribed!", "success")
75                 thread.watchers.remove(current_user)
76                 db.session.commit()
77         else:
78                 flash("Already not subscribed!", "success")
79
80         return redirect(thread.getViewURL())
81
82
83 @bp.route("/threads/<int:id>/set-lock/", methods=["POST"])
84 @login_required
85 def set_lock(id):
86         thread = Thread.query.get(id)
87         if thread is None or not thread.checkPerm(current_user, Permission.LOCK_THREAD):
88                 abort(404)
89
90         thread.locked = isYes(request.args.get("lock"))
91         if thread.locked is None:
92                 abort(400)
93
94         msg = None
95         if thread.locked:
96                 msg = "Locked thread '{}'".format(thread.title)
97                 flash("Locked thread", "success")
98         else:
99                 msg = "Unlocked thread '{}'".format(thread.title)
100                 flash("Unlocked thread", "success")
101
102         addNotification(thread.watchers, current_user, msg, thread.getViewURL(), thread.package)
103         addAuditLog(AuditSeverity.MODERATION, current_user, msg, thread.getViewURL(), thread.package)
104
105         db.session.commit()
106
107         return redirect(thread.getViewURL())
108
109
110 @bp.route("/threads/<int:id>/delete/", methods=["GET", "POST"])
111 @login_required
112 def delete_reply(id):
113         thread = Thread.query.get(id)
114         if thread is None:
115                 abort(404)
116
117         reply_id = request.args.get("reply")
118         if reply_id is None:
119                 abort(404)
120
121         reply = ThreadReply.query.get(reply_id)
122         if reply is None or reply.thread != thread:
123                 abort(404)
124
125         if thread.replies[0] == reply:
126                 flash("Cannot delete thread opening post!", "danger")
127                 return redirect(thread.getViewURL())
128
129         if not reply.checkPerm(current_user, Permission.DELETE_REPLY):
130                 abort(403)
131
132         if request.method == "GET":
133                 return render_template("threads/delete_reply.html", thread=thread, reply=reply)
134
135         msg = "Deleted reply by {}".format(reply.author.display_name)
136         addAuditLog(AuditSeverity.MODERATION, current_user, msg, thread.getViewURL(), thread.package, reply.comment)
137
138         db.session.delete(reply)
139         db.session.commit()
140
141         return redirect(thread.getViewURL())
142
143
144 class CommentForm(FlaskForm):
145         comment = TextAreaField("Comment", [InputRequired(), Length(10, 2000)])
146         submit  = SubmitField("Comment")
147
148
149 @bp.route("/threads/<int:id>/edit/", methods=["GET", "POST"])
150 @login_required
151 def edit_reply(id):
152         thread = Thread.query.get(id)
153         if thread is None:
154                 abort(404)
155
156         reply_id = request.args.get("reply")
157         if reply_id is None:
158                 abort(404)
159
160         reply = ThreadReply.query.get(reply_id)
161         if reply is None or reply.thread != thread:
162                 abort(404)
163
164         if not reply.checkPerm(current_user, Permission.EDIT_REPLY):
165                 abort(403)
166
167         form = CommentForm(formdata=request.form, obj=reply)
168         if request.method == "POST" and form.validate():
169                 comment = form.comment.data
170
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)
175
176                 reply.comment = comment
177
178                 db.session.commit()
179
180                 return redirect(thread.getViewURL())
181
182         return render_template("threads/edit_reply.html", thread=thread, reply=reply, form=form)
183
184
185 @bp.route("/threads/<int:id>/", methods=["GET", "POST"])
186 def view(id):
187         thread = Thread.query.get(id)
188         if thread is None or not thread.checkPerm(current_user, Permission.SEE_THREAD):
189                 abort(404)
190
191         if current_user.is_authenticated and request.method == "POST":
192                 comment = request.form["comment"]
193
194                 if not thread.checkPerm(current_user, Permission.COMMENT_THREAD):
195                         flash("You cannot comment on this thread", "danger")
196                         return redirect(thread.getViewURL())
197
198                 if not current_user.canCommentRL():
199                         flash("Please wait before commenting again", "danger")
200                         return redirect(thread.getViewURL())
201
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)
207
208                         thread.replies.append(reply)
209                         if not current_user in thread.watchers:
210                                 thread.watchers.append(current_user)
211
212                         msg = "New comment on '{}'".format(thread.title)
213                         addNotification(thread.watchers, current_user, msg, thread.getViewURL(), thread.package)
214                         db.session.commit()
215
216                         return redirect(thread.getViewURL())
217
218                 else:
219                         flash("Comment needs to be between 3 and 2000 characters.")
220
221         return render_template("threads/view.html", thread=thread)
222
223
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")
229
230
231 @bp.route("/threads/new/", methods=["GET", "POST"])
232 @login_required
233 def new():
234         form = ThreadForm(formdata=request.form)
235
236         package = None
237         if "pid" in request.args:
238                 package = Package.query.get(int(request.args.get("pid")))
239                 if package is None:
240                         flash("Unable to find that package!", "danger")
241
242         # Don't allow making orphan threads on approved packages for now
243         if package is None:
244                 abort(403)
245
246         def_is_private   = request.args.get("private") or False
247         if package is None:
248                 def_is_private = True
249         allow_change     = package and package.approved
250         is_review_thread = package and not package.approved
251
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"))
256
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())
261
262         elif not current_user.canOpenThreadRL():
263                 flash("Please wait before opening another thread", "danger")
264
265                 if package:
266                         return redirect(package.getDetailsURL())
267                 else:
268                         return redirect(url_for("homepage.home"))
269
270         # Set default values
271         elif request.method == "GET":
272                 form.private.data = def_is_private
273                 form.title.data   = request.args.get("title") or ""
274
275         # Validate and submit
276         elif request.method == "POST" and form.validate():
277                 thread = Thread()
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)
283
284                 thread.watchers.append(current_user)
285                 if package is not None and package.author != current_user:
286                         thread.watchers.append(package.author)
287
288                 reply = ThreadReply()
289                 reply.thread  = thread
290                 reply.author  = current_user
291                 reply.comment = form.comment.data
292                 db.session.add(reply)
293
294                 thread.replies.append(reply)
295
296                 db.session.commit()
297
298                 if is_review_thread:
299                         package.review_thread = thread
300
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)
304
305                 editors = User.query.filter(User.rank >= UserRank.EDITOR).all()
306                 addNotification(editors, current_user, notif_msg, thread.getViewURL(), package)
307
308                 db.session.commit()
309
310                 return redirect(thread.getViewURL())
311
312
313         return render_template("threads/new.html", form=form, allow_private_change=allow_change, package=package)