views.py 22 KB

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