views.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606
  1. # -*- coding: utf-8 -*-
  2. """
  3. flaskbb.forum.views
  4. ~~~~~~~~~~~~~~~~~~~~
  5. This module handles the forum logic like creating and viewing
  6. topics and posts.
  7. :copyright: (c) 2014 by the FlaskBB Team.
  8. :license: BSD, see LICENSE for more details.
  9. """
  10. import datetime
  11. from flask import (Blueprint, redirect, url_for, current_app,
  12. request, flash, jsonify)
  13. from flask_login import login_required, current_user
  14. from flask_babelex import gettext as _
  15. from flaskbb.extensions import db
  16. from flaskbb.utils.settings import flaskbb_config
  17. from flaskbb.utils.helpers import (get_online_users, time_diff, format_quote,
  18. render_template, do_topic_action)
  19. from flaskbb.utils.permissions import (can_post_reply, can_post_topic,
  20. can_delete_topic, can_delete_post,
  21. can_edit_post, can_moderate)
  22. from flaskbb.forum.models import (Category, Forum, Topic, Post, ForumsRead,
  23. TopicsRead)
  24. from flaskbb.forum.forms import (QuickreplyForm, ReplyForm, NewTopicForm,
  25. ReportForm, UserSearchForm, SearchPageForm)
  26. from flaskbb.user.models import User
  27. forum = Blueprint("forum", __name__)
  28. @forum.route("/")
  29. def index():
  30. categories = Category.get_all(user=current_user)
  31. # Fetch a few stats about the forum
  32. user_count = User.query.count()
  33. topic_count = Topic.query.count()
  34. post_count = Post.query.count()
  35. newest_user = User.query.order_by(User.id.desc()).first()
  36. # Check if we use redis or not
  37. if not current_app.config["REDIS_ENABLED"]:
  38. online_users = User.query.filter(User.lastseen >= time_diff()).count()
  39. # Because we do not have server side sessions, we cannot check if there
  40. # are online guests
  41. online_guests = None
  42. else:
  43. online_users = len(get_online_users())
  44. online_guests = len(get_online_users(guest=True))
  45. return render_template("forum/index.html",
  46. categories=categories,
  47. user_count=user_count,
  48. topic_count=topic_count,
  49. post_count=post_count,
  50. newest_user=newest_user,
  51. online_users=online_users,
  52. online_guests=online_guests)
  53. @forum.route("/category/<int:category_id>")
  54. @forum.route("/category/<int:category_id>-<slug>")
  55. def view_category(category_id, slug=None):
  56. category, forums = Category.\
  57. get_forums(category_id=category_id, user=current_user)
  58. return render_template("forum/category.html", forums=forums,
  59. category=category)
  60. @forum.route("/forum/<int:forum_id>")
  61. @forum.route("/forum/<int:forum_id>-<slug>")
  62. def view_forum(forum_id, slug=None):
  63. page = request.args.get('page', 1, type=int)
  64. forum_instance, forumsread = Forum.get_forum(forum_id=forum_id,
  65. user=current_user)
  66. if forum_instance.external:
  67. return redirect(forum_instance.external)
  68. topics = Forum.get_topics(
  69. forum_id=forum_instance.id, user=current_user, page=page,
  70. per_page=flaskbb_config["TOPICS_PER_PAGE"]
  71. )
  72. return render_template(
  73. "forum/forum.html", forum=forum_instance,
  74. topics=topics, forumsread=forumsread,
  75. )
  76. @forum.route("/topic/<int:topic_id>", methods=["POST", "GET"])
  77. @forum.route("/topic/<int:topic_id>-<slug>", methods=["POST", "GET"])
  78. def view_topic(topic_id, slug=None):
  79. page = request.args.get('page', 1, type=int)
  80. # Fetch some information about the topic
  81. topic = Topic.query.filter_by(id=topic_id).first()
  82. # Count the topic views
  83. topic.views += 1
  84. topic.save()
  85. # fetch the posts in the topic
  86. posts = Post.query.\
  87. join(User, Post.user_id == User.id).\
  88. filter(Post.topic_id == topic.id).\
  89. add_entity(User).\
  90. order_by(Post.id.asc()).\
  91. paginate(page, flaskbb_config['POSTS_PER_PAGE'], False)
  92. # Update the topicsread status if the user hasn't read it
  93. forumsread = None
  94. if current_user.is_authenticated():
  95. forumsread = ForumsRead.query.\
  96. filter_by(user_id=current_user.id,
  97. forum_id=topic.forum.id).first()
  98. topic.update_read(current_user, topic.forum, forumsread)
  99. form = None
  100. if can_post_reply(user=current_user, topic=topic):
  101. form = QuickreplyForm()
  102. if form.validate_on_submit():
  103. post = form.save(current_user, topic)
  104. return view_post(post.id)
  105. return render_template("forum/topic.html", topic=topic, posts=posts,
  106. last_seen=time_diff(), form=form)
  107. @forum.route("/post/<int:post_id>")
  108. def view_post(post_id):
  109. post = Post.query.filter_by(id=post_id).first_or_404()
  110. count = post.topic.post_count
  111. page = count / flaskbb_config["POSTS_PER_PAGE"]
  112. if count > flaskbb_config["POSTS_PER_PAGE"]:
  113. page += 1
  114. else:
  115. page = 1
  116. return redirect(post.topic.url + "?page=%d#pid%s" % (page, post.id))
  117. @forum.route("/<int:forum_id>/topic/new", methods=["POST", "GET"])
  118. @forum.route("/<int:forum_id>-<slug>/topic/new", methods=["POST", "GET"])
  119. @login_required
  120. def new_topic(forum_id, slug=None):
  121. forum_instance = Forum.query.filter_by(id=forum_id).first_or_404()
  122. if not can_post_topic(user=current_user, forum=forum):
  123. flash(_("You do not have the permissions to create a new topic."),
  124. "danger")
  125. return redirect(forum.url)
  126. form = NewTopicForm()
  127. if request.method == "POST":
  128. if "preview" in request.form and form.validate():
  129. return render_template(
  130. "forum/new_topic.html", forum=forum_instance,
  131. form=form, preview=form.content.data
  132. )
  133. if "submit" in request.form and form.validate():
  134. topic = form.save(current_user, forum_instance)
  135. # redirect to the new topic
  136. return redirect(url_for('forum.view_topic', topic_id=topic.id))
  137. return render_template(
  138. "forum/new_topic.html", forum=forum_instance, form=form
  139. )
  140. @forum.route("/topic/<int:topic_id>/delete", methods=["POST"])
  141. @forum.route("/topic/<int:topic_id>-<slug>/delete", methods=["POST"])
  142. @login_required
  143. def delete_topic(topic_id=None, slug=None):
  144. topic = Topic.query.filter_by(id=topic_id).first_or_404()
  145. if not can_delete_topic(user=current_user, topic=topic):
  146. flash(_("You do not have the permissions to delete this topic."),
  147. "danger")
  148. return redirect(topic.forum.url)
  149. involved_users = User.query.filter(Post.topic_id == topic.id,
  150. User.id == Post.user_id).all()
  151. topic.delete(users=involved_users)
  152. return redirect(url_for("forum.view_forum", forum_id=topic.forum_id))
  153. @forum.route("/topic/<int:topic_id>/lock", methods=["POST"])
  154. @forum.route("/topic/<int:topic_id>-<slug>/lock", methods=["POST"])
  155. @login_required
  156. def lock_topic(topic_id=None, slug=None):
  157. topic = Topic.query.filter_by(id=topic_id).first_or_404()
  158. if not can_moderate(user=current_user, forum=topic.forum):
  159. flash(_("You do not have the permissions to lock this topic."),
  160. "danger")
  161. return redirect(topic.url)
  162. topic.locked = True
  163. topic.save()
  164. return redirect(topic.url)
  165. @forum.route("/topic/<int:topic_id>/unlock", methods=["POST"])
  166. @forum.route("/topic/<int:topic_id>-<slug>/unlock", methods=["POST"])
  167. @login_required
  168. def unlock_topic(topic_id=None, slug=None):
  169. topic = Topic.query.filter_by(id=topic_id).first_or_404()
  170. if not can_moderate(user=current_user, forum=topic.forum):
  171. flash(_("You do not have the permissions to unlock this topic."),
  172. "danger")
  173. return redirect(topic.url)
  174. topic.locked = False
  175. topic.save()
  176. return redirect(topic.url)
  177. @forum.route("/topic/<int:topic_id>/highlight", methods=["POST"])
  178. @forum.route("/topic/<int:topic_id>-<slug>/highlight", methods=["POST"])
  179. @login_required
  180. def highlight_topic(topic_id=None, slug=None):
  181. topic = Topic.query.filter_by(id=topic_id).first_or_404()
  182. if not can_moderate(user=current_user, forum=topic.forum):
  183. flash(_("You do not have the permissions to highlight this topic."),
  184. "danger")
  185. return redirect(topic.url)
  186. topic.important = True
  187. topic.save()
  188. return redirect(topic.url)
  189. @forum.route("/topic/<int:topic_id>/trivialize", methods=["POST"])
  190. @forum.route("/topic/<int:topic_id>-<slug>/trivialize", methods=["POST"])
  191. @login_required
  192. def trivialize_topic(topic_id=None, slug=None):
  193. topic = Topic.query.filter_by(id=topic_id).first_or_404()
  194. # Unlock is basically the same as lock
  195. if not can_moderate(user=current_user, forum=topic.forum):
  196. flash(_("You do not have the permissions to trivialize this topic."),
  197. "danger")
  198. return redirect(topic.url)
  199. topic.important = False
  200. topic.save()
  201. return redirect(topic.url)
  202. @forum.route("/forum/<int:forum_id>/edit", methods=["POST", "GET"])
  203. @forum.route("/forum/<int:forum_id>-<slug>/edit", methods=["POST", "GET"])
  204. @login_required
  205. def manage_forum(forum_id, slug=None):
  206. page = request.args.get('page', 1, type=int)
  207. forum_instance, forumsread = Forum.get_forum(forum_id=forum_id,
  208. user=current_user)
  209. # remove the current forum from the select field (move).
  210. available_forums = Forum.query.order_by(Forum.position).all()
  211. available_forums.remove(forum_instance)
  212. if not can_moderate(current_user, forum=forum_instance):
  213. flash(_("You do not have the permissions to moderate this forum."),
  214. "danger")
  215. return redirect(forum_instance.url)
  216. if forum_instance.external:
  217. return redirect(forum_instance.external)
  218. topics = Forum.get_topics(
  219. forum_id=forum_instance.id, user=current_user, page=page,
  220. per_page=flaskbb_config["TOPICS_PER_PAGE"]
  221. )
  222. mod_forum_url = url_for("forum.manage_forum", forum_id=forum_instance.id,
  223. slug=forum_instance.slug)
  224. # the code is kind of the same here but it somehow still looks cleaner than
  225. # doin some magic
  226. if request.method == "POST":
  227. ids = request.form.getlist("rowid")
  228. tmp_topics = Topic.query.filter(Topic.id.in_(ids)).all()
  229. # locking/unlocking
  230. if "lock" in request.form:
  231. changed = do_topic_action(topics=tmp_topics, user=current_user,
  232. action="locked", reverse=False)
  233. flash(_("%(count)s Topics locked.", count=changed), "success")
  234. return redirect(mod_forum_url)
  235. elif "unlock" in request.form:
  236. changed = do_topic_action(topics=tmp_topics, user=current_user,
  237. action="locked", reverse=True)
  238. flash(_("%(count)s Topics unlocked.", count=changed), "success")
  239. return redirect(mod_forum_url)
  240. # highlighting/trivializing
  241. elif "highlight" in request.form:
  242. changed = do_topic_action(topics=tmp_topics, user=current_user,
  243. action="important", reverse=False)
  244. flash(_("%(count)s Topics highlighted.", count=changed), "success")
  245. return redirect(mod_forum_url)
  246. elif "trivialize" in request.form:
  247. changed = do_topic_action(topics=tmp_topics, user=current_user,
  248. action="important", reverse=True)
  249. flash(_("%(count)s Topics trivialized.", count=changed), "success")
  250. return redirect(mod_forum_url)
  251. # deleting
  252. elif "delete" in request.form:
  253. changed = do_topic_action(topics=tmp_topics, user=current_user,
  254. action="delete", reverse=False)
  255. flash(_("%(count)s Topics deleted.", count=changed), "success")
  256. return redirect(mod_forum_url)
  257. # moving
  258. elif "move" in request.form:
  259. new_forum_id = request.form.get("forum")
  260. if not new_forum_id:
  261. flash(_("Please choose a new forum for the topics."), "info")
  262. return redirect(mod_forum_url)
  263. new_forum = Forum.query.filter_by(id=new_forum_id).first_or_404()
  264. # check the permission in the current forum and in the new forum
  265. if not can_moderate(current_user, forum_instance) or \
  266. not can_moderate(current_user, new_forum):
  267. flash(_("You do not have the permissions to move this topic."),
  268. "danger")
  269. return redirect(mod_forum_url)
  270. new_forum.move_topics_to(tmp_topics)
  271. return redirect(mod_forum_url)
  272. return render_template(
  273. "forum/edit_forum.html", forum=forum_instance, topics=topics,
  274. available_forums=available_forums, forumsread=forumsread,
  275. )
  276. @forum.route("/topic/<int:topic_id>/post/new", methods=["POST", "GET"])
  277. @forum.route("/topic/<int:topic_id>-<slug>/post/new", methods=["POST", "GET"])
  278. @login_required
  279. def new_post(topic_id, slug=None):
  280. topic = Topic.query.filter_by(id=topic_id).first_or_404()
  281. if not can_post_reply(user=current_user, topic=topic):
  282. flash(_("You do not have the permissions to post in this topic."),
  283. "danger")
  284. return redirect(topic.forum.url)
  285. form = ReplyForm()
  286. if form.validate_on_submit():
  287. if "preview" in request.form:
  288. return render_template(
  289. "forum/new_post.html", topic=topic,
  290. form=form, preview=form.content.data
  291. )
  292. else:
  293. post = form.save(current_user, topic)
  294. return view_post(post.id)
  295. return render_template("forum/new_post.html", topic=topic, form=form)
  296. @forum.route(
  297. "/topic/<int:topic_id>/post/<int:post_id>/reply", methods=["POST", "GET"]
  298. )
  299. @login_required
  300. def reply_post(topic_id, post_id):
  301. topic = Topic.query.filter_by(id=topic_id).first_or_404()
  302. post = Post.query.filter_by(id=post_id).first_or_404()
  303. if not can_post_reply(user=current_user, topic=topic):
  304. flash(_("You do not have the permissions to post in this topic."),
  305. "danger")
  306. return redirect(topic.forum.url)
  307. form = ReplyForm()
  308. if form.validate_on_submit():
  309. if "preview" in request.form:
  310. return render_template(
  311. "forum/new_post.html", topic=topic,
  312. form=form, preview=form.content.data
  313. )
  314. else:
  315. form.save(current_user, topic)
  316. return redirect(post.topic.url)
  317. else:
  318. form.content.data = format_quote(post)
  319. return render_template("forum/new_post.html", topic=post.topic, form=form)
  320. @forum.route("/post/<int:post_id>/edit", methods=["POST", "GET"])
  321. @login_required
  322. def edit_post(post_id):
  323. post = Post.query.filter_by(id=post_id).first_or_404()
  324. if not can_edit_post(user=current_user, post=post):
  325. flash(_("You do not have the permissions to edit this post."),
  326. "danger")
  327. return redirect(post.topic.url)
  328. form = ReplyForm()
  329. if form.validate_on_submit():
  330. if "preview" in request.form:
  331. return render_template(
  332. "forum/new_post.html", topic=post.topic,
  333. form=form, preview=form.content.data
  334. )
  335. else:
  336. form.populate_obj(post)
  337. post.date_modified = datetime.datetime.utcnow()
  338. post.modified_by = current_user.username
  339. post.save()
  340. return redirect(post.topic.url)
  341. else:
  342. form.content.data = post.content
  343. return render_template("forum/new_post.html", topic=post.topic, form=form)
  344. @forum.route("/post/<int:post_id>/delete", methods=["POST"])
  345. @login_required
  346. def delete_post(post_id):
  347. post = Post.query.filter_by(id=post_id).first_or_404()
  348. # TODO: Bulk delete
  349. if not can_delete_post(user=current_user, post=post):
  350. flash(_("You do not have the permissions to delete this post."),
  351. "danger")
  352. return redirect(post.topic.url)
  353. first_post = post.first_post
  354. topic_url = post.topic.url
  355. forum_url = post.topic.forum.url
  356. post.delete()
  357. # If the post was the first post in the topic, redirect to the forums
  358. if first_post:
  359. return redirect(forum_url)
  360. return redirect(topic_url)
  361. @forum.route("/post/<int:post_id>/report", methods=["GET", "POST"])
  362. @login_required
  363. def report_post(post_id):
  364. post = Post.query.filter_by(id=post_id).first_or_404()
  365. form = ReportForm()
  366. if form.validate_on_submit():
  367. form.save(current_user, post)
  368. flash(_("Thanks for reporting."), "success")
  369. return render_template("forum/report_post.html", form=form)
  370. @forum.route("/post/<int:post_id>/raw", methods=["POST", "GET"])
  371. @login_required
  372. def raw_post(post_id):
  373. post = Post.query.filter_by(id=post_id).first_or_404()
  374. return format_quote(username=post.username, content=post.content)
  375. @forum.route("/<int:forum_id>/markread", methods=["POST"])
  376. @forum.route("/<int:forum_id>-<slug>/markread", methods=["POST"])
  377. @login_required
  378. def markread(forum_id=None, slug=None):
  379. # Mark a single forum as read
  380. if forum_id:
  381. forum_instance = Forum.query.filter_by(id=forum_id).first_or_404()
  382. forumsread = ForumsRead.query.filter_by(
  383. user_id=current_user.id, forum_id=forum_instance.id
  384. ).first()
  385. TopicsRead.query.filter_by(user_id=current_user.id,
  386. forum_id=forum_instance.id).delete()
  387. if not forumsread:
  388. forumsread = ForumsRead()
  389. forumsread.user_id = current_user.id
  390. forumsread.forum_id = forum_instance.id
  391. forumsread.last_read = datetime.datetime.utcnow()
  392. forumsread.cleared = datetime.datetime.utcnow()
  393. db.session.add(forumsread)
  394. db.session.commit()
  395. flash(_("Forum %(forum)s marked as read.", forum=forum_instance.title),
  396. "success")
  397. return redirect(forum_instance.url)
  398. # Mark all forums as read
  399. ForumsRead.query.filter_by(user_id=current_user.id).delete()
  400. TopicsRead.query.filter_by(user_id=current_user.id).delete()
  401. forums = Forum.query.all()
  402. forumsread_list = []
  403. for forum_instance in forums:
  404. forumsread = ForumsRead()
  405. forumsread.user_id = current_user.id
  406. forumsread.forum_id = forum_instance.id
  407. forumsread.last_read = datetime.datetime.utcnow()
  408. forumsread.cleared = datetime.datetime.utcnow()
  409. forumsread_list.append(forumsread)
  410. db.session.add_all(forumsread_list)
  411. db.session.commit()
  412. flash(_("All forums marked as read."), "success")
  413. return redirect(url_for("forum.index"))
  414. @forum.route("/who-is-online")
  415. def who_is_online():
  416. if current_app.config['REDIS_ENABLED']:
  417. online_users = get_online_users()
  418. else:
  419. online_users = User.query.filter(User.lastseen >= time_diff()).all()
  420. return render_template("forum/online_users.html",
  421. online_users=online_users)
  422. @forum.route("/memberlist", methods=['GET', 'POST'])
  423. def memberlist():
  424. page = request.args.get('page', 1, type=int)
  425. search_form = UserSearchForm()
  426. if search_form.validate():
  427. users = search_form.get_results().\
  428. paginate(page, flaskbb_config['USERS_PER_PAGE'], False)
  429. return render_template("forum/memberlist.html", users=users,
  430. search_form=search_form)
  431. else:
  432. users = User.query.\
  433. paginate(page, flaskbb_config['USERS_PER_PAGE'], False)
  434. return render_template("forum/memberlist.html", users=users,
  435. search_form=search_form)
  436. @forum.route("/topictracker")
  437. @login_required
  438. def topictracker():
  439. page = request.args.get("page", 1, type=int)
  440. topics = current_user.tracked_topics.\
  441. outerjoin(TopicsRead,
  442. db.and_(TopicsRead.topic_id == Topic.id,
  443. TopicsRead.user_id == current_user.id)).\
  444. add_entity(TopicsRead).\
  445. order_by(Topic.last_updated.desc()).\
  446. paginate(page, flaskbb_config['TOPICS_PER_PAGE'], True)
  447. return render_template("forum/topictracker.html", topics=topics)
  448. @forum.route("/topictracker/<int:topic_id>/add", methods=["POST"])
  449. @forum.route("/topictracker/<int:topic_id>-<slug>/add", methods=["POST"])
  450. @login_required
  451. def track_topic(topic_id, slug=None):
  452. topic = Topic.query.filter_by(id=topic_id).first_or_404()
  453. current_user.track_topic(topic)
  454. current_user.save()
  455. return redirect(topic.url)
  456. @forum.route("/topictracker/<int:topic_id>/delete", methods=["POST"])
  457. @forum.route("/topictracker/<int:topic_id>-<slug>/delete", methods=["POST"])
  458. @login_required
  459. def untrack_topic(topic_id, slug=None):
  460. topic = Topic.query.filter_by(id=topic_id).first_or_404()
  461. current_user.untrack_topic(topic)
  462. current_user.save()
  463. return redirect(topic.url)
  464. @forum.route("/search", methods=['GET', 'POST'])
  465. def search():
  466. form = SearchPageForm()
  467. if form.validate_on_submit():
  468. result = form.get_results()
  469. return render_template('forum/search_result.html', form=form,
  470. result=result)
  471. return render_template('forum/search_form.html', form=form)