views.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568
  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)
  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)
  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")
  141. @forum.route("/topic/<int:topic_id>-<slug>/delete")
  142. @login_required
  143. def delete_topic(topic_id, 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")
  154. @forum.route("/topic/<int:topic_id>-<slug>/lock")
  155. @login_required
  156. def lock_topic(topic_id, slug=None):
  157. topic = Topic.query.filter_by(id=topic_id).first_or_404()
  158. # TODO: Bulk lock
  159. if not can_moderate(user=current_user, forum=topic.forum):
  160. flash(_("You do not have the permissions to lock this topic."),
  161. "danger")
  162. return redirect(topic.url)
  163. topic.locked = True
  164. topic.save()
  165. return redirect(topic.url)
  166. @forum.route("/topic/<int:topic_id>/unlock")
  167. @forum.route("/topic/<int:topic_id>-<slug>/unlock")
  168. @login_required
  169. def unlock_topic(topic_id, slug=None):
  170. topic = Topic.query.filter_by(id=topic_id).first_or_404()
  171. # TODO: Bulk unlock
  172. # Unlock is basically the same as lock
  173. if not can_moderate(user=current_user, forum=topic.forum):
  174. flash(_("You do not have the permissions to unlock this topic."),
  175. "danger")
  176. return redirect(topic.url)
  177. topic.locked = False
  178. topic.save()
  179. return redirect(topic.url)
  180. @forum.route("/topic/<int:topic_id>/highlight")
  181. @forum.route("/topic/<int:topic_id>-<slug>/highlight")
  182. @login_required
  183. def highlight_topic(topic_id, slug=None):
  184. topic = Topic.query.filter_by(id=topic_id).first_or_404()
  185. if not can_moderate(user=current_user, forum=topic.forum):
  186. flash(_("You do not have the permissions to highlight this topic."),
  187. "danger")
  188. return redirect(topic.url)
  189. topic.important = True
  190. topic.save()
  191. return redirect(topic.url)
  192. @forum.route("/topic/<int:topic_id>/trivialize")
  193. @forum.route("/topic/<int:topic_id>-<slug>/trivialize")
  194. @login_required
  195. def trivialize_topic(topic_id, slug=None):
  196. topic = Topic.query.filter_by(id=topic_id).first_or_404()
  197. # Unlock is basically the same as lock
  198. if not can_moderate(user=current_user, forum=topic.forum):
  199. flash(_("You do not have the permissions to trivialize this topic."),
  200. "danger")
  201. return redirect(topic.url)
  202. topic.important = False
  203. topic.save()
  204. return redirect(topic.url)
  205. @forum.route("/topic/<int:topic_id>/move/<int:forum_id>")
  206. @forum.route(
  207. "/topic/<int:topic_id>-<topic_slug>/move/<int:forum_id>-<forum_slug>"
  208. )
  209. @login_required
  210. def move_topic(topic_id, forum_id, topic_slug=None, forum_slug=None):
  211. forum_instance = Forum.query.filter_by(id=forum_id).first_or_404()
  212. topic = Topic.query.filter_by(id=topic_id).first_or_404()
  213. # TODO: Bulk move
  214. if not can_moderate(user=current_user, forum=topic.forum):
  215. flash(_("You do not have the permissions to move this topic."),
  216. "danger")
  217. return redirect(forum_instance.url)
  218. if not topic.move(forum_instance):
  219. flash(_("Could not move the topic to forum %(title)s.",
  220. title=forum_instance.title), "danger")
  221. return redirect(topic.url)
  222. flash(_("Topic was moved to forum %(title)s.",
  223. title=forum_instance.title), "success")
  224. return redirect(topic.url)
  225. @forum.route("/topic/<int:old_id>/merge/<int:new_id>")
  226. @forum.route("/topic/<int:old_id>-<old_slug>/merge/<int:new_id>-<new_slug>")
  227. @login_required
  228. def merge_topic(old_id, new_id, old_slug=None, new_slug=None):
  229. _old_topic = Topic.query.filter_by(id=old_id).first_or_404()
  230. _new_topic = Topic.query.filter_by(id=new_id).first_or_404()
  231. # TODO: Bulk merge
  232. # Looks to me that the user should have permissions on both forums, right?
  233. if not can_moderate(user=current_user, forum=_old_topic.forum):
  234. flash(_("You do not have the permissions to merge this topic."),
  235. "danger")
  236. return redirect(_old_topic.url)
  237. if not _old_topic.merge(_new_topic):
  238. flash(_("Could not merge the topics."), "danger")
  239. return redirect(_old_topic.url)
  240. flash(_("Topics succesfully merged."), "success")
  241. return redirect(_new_topic.url)
  242. @forum.route("/topic/<int:topic_id>/post/new", methods=["POST", "GET"])
  243. @forum.route("/topic/<int:topic_id>-<slug>/post/new", methods=["POST", "GET"])
  244. @login_required
  245. def new_post(topic_id, slug=None):
  246. topic = Topic.query.filter_by(id=topic_id).first_or_404()
  247. if not can_post_reply(user=current_user, topic=topic):
  248. flash(_("You do not have the permissions to post in this topic."),
  249. "danger")
  250. return redirect(topic.forum.url)
  251. form = ReplyForm()
  252. if form.validate_on_submit():
  253. if "preview" in request.form:
  254. return render_template(
  255. "forum/new_post.html", topic=topic,
  256. form=form, preview=form.content.data
  257. )
  258. else:
  259. post = form.save(current_user, topic)
  260. return view_post(post.id)
  261. return render_template("forum/new_post.html", topic=topic, form=form)
  262. @forum.route(
  263. "/topic/<int:topic_id>/post/<int:post_id>/reply", methods=["POST", "GET"]
  264. )
  265. @login_required
  266. def reply_post(topic_id, post_id):
  267. topic = Topic.query.filter_by(id=topic_id).first_or_404()
  268. post = Post.query.filter_by(id=post_id).first_or_404()
  269. if not can_post_reply(user=current_user, topic=topic):
  270. flash(_("You do not have the permissions to post in this topic."),
  271. "danger")
  272. return redirect(topic.forum.url)
  273. form = ReplyForm()
  274. if form.validate_on_submit():
  275. if "preview" in request.form:
  276. return render_template(
  277. "forum/new_post.html", topic=topic,
  278. form=form, preview=form.content.data
  279. )
  280. else:
  281. form.save(current_user, topic)
  282. return redirect(post.topic.url)
  283. else:
  284. form.content.data = format_quote(post)
  285. return render_template("forum/new_post.html", topic=post.topic, form=form)
  286. @forum.route("/post/<int:post_id>/edit", methods=["POST", "GET"])
  287. @login_required
  288. def edit_post(post_id):
  289. post = Post.query.filter_by(id=post_id).first_or_404()
  290. if not can_edit_post(user=current_user, post=post):
  291. flash(_("You do not have the permissions to edit this post."), "danger")
  292. return redirect(post.topic.url)
  293. form = ReplyForm()
  294. if form.validate_on_submit():
  295. if "preview" in request.form:
  296. return render_template(
  297. "forum/new_post.html", topic=post.topic,
  298. form=form, preview=form.content.data
  299. )
  300. else:
  301. form.populate_obj(post)
  302. post.date_modified = datetime.datetime.utcnow()
  303. post.modified_by = current_user.username
  304. post.save()
  305. return redirect(post.topic.url)
  306. else:
  307. form.content.data = post.content
  308. return render_template("forum/new_post.html", topic=post.topic, form=form)
  309. @forum.route("/post/<int:post_id>/delete")
  310. @login_required
  311. def delete_post(post_id, slug=None):
  312. post = Post.query.filter_by(id=post_id).first_or_404()
  313. # TODO: Bulk delete
  314. if not can_delete_post(user=current_user, post=post):
  315. flash(_("You do not have the permissions to delete this post."),
  316. "danger")
  317. return redirect(post.topic.url)
  318. first_post = post.first_post
  319. topic_url = post.topic.url
  320. forum_url = post.topic.forum.url
  321. post.delete()
  322. # If the post was the first post in the topic, redirect to the forums
  323. if first_post:
  324. return redirect(forum_url)
  325. return redirect(topic_url)
  326. @forum.route("/post/<int:post_id>/report", methods=["GET", "POST"])
  327. @login_required
  328. def report_post(post_id):
  329. post = Post.query.filter_by(id=post_id).first_or_404()
  330. form = ReportForm()
  331. if form.validate_on_submit():
  332. form.save(current_user, post)
  333. flash(_("Thanks for reporting."), "success")
  334. return render_template("forum/report_post.html", form=form)
  335. @forum.route("/post/<int:post_id>/raw", methods=["POST", "GET"])
  336. @login_required
  337. def raw_post(post_id):
  338. post = Post.query.filter_by(id=post_id).first_or_404()
  339. return format_quote(post)
  340. @forum.route("/markread")
  341. @forum.route("/<int:forum_id>/markread")
  342. @forum.route("/<int:forum_id>-<slug>/markread")
  343. @login_required
  344. def markread(forum_id=None, slug=None):
  345. # Mark a single forum as read
  346. if forum_id:
  347. forum_instance = Forum.query.filter_by(id=forum_id).first_or_404()
  348. forumsread = ForumsRead.query.filter_by(
  349. user_id=current_user.id, forum_id=forum_instance.id
  350. ).first()
  351. TopicsRead.query.filter_by(user_id=current_user.id,
  352. forum_id=forum_instance.id).delete()
  353. if not forumsread:
  354. forumsread = ForumsRead()
  355. forumsread.user_id = current_user.id
  356. forumsread.forum_id = forum_instance.id
  357. forumsread.last_read = datetime.datetime.utcnow()
  358. forumsread.cleared = datetime.datetime.utcnow()
  359. db.session.add(forumsread)
  360. db.session.commit()
  361. flash(_("Forum %(forum)s marked as read.", forum=forum_instance.title),
  362. "success")
  363. return redirect(forum_instance.url)
  364. # Mark all forums as read
  365. ForumsRead.query.filter_by(user_id=current_user.id).delete()
  366. TopicsRead.query.filter_by(user_id=current_user.id).delete()
  367. forums = Forum.query.all()
  368. forumsread_list = []
  369. for forum_instance in forums:
  370. forumsread = ForumsRead()
  371. forumsread.user_id = current_user.id
  372. forumsread.forum_id = forum_instance.id
  373. forumsread.last_read = datetime.datetime.utcnow()
  374. forumsread.cleared = datetime.datetime.utcnow()
  375. forumsread_list.append(forumsread)
  376. db.session.add_all(forumsread_list)
  377. db.session.commit()
  378. flash(_("All forums marked as read."), "success")
  379. return redirect(url_for("forum.index"))
  380. @forum.route("/who_is_online")
  381. def who_is_online():
  382. if current_app.config['REDIS_ENABLED']:
  383. online_users = get_online_users()
  384. else:
  385. online_users = User.query.filter(User.lastseen >= time_diff()).all()
  386. return render_template("forum/online_users.html",
  387. online_users=online_users)
  388. @forum.route("/memberlist", methods=['GET', 'POST'])
  389. def memberlist():
  390. page = request.args.get('page', 1, type=int)
  391. search_form = UserSearchForm()
  392. if search_form.validate():
  393. users = search_form.get_results().\
  394. paginate(page, flaskbb_config['USERS_PER_PAGE'], False)
  395. return render_template("forum/memberlist.html", users=users,
  396. search_form=search_form)
  397. else:
  398. users = User.query.\
  399. paginate(page, flaskbb_config['USERS_PER_PAGE'], False)
  400. return render_template("forum/memberlist.html", users=users,
  401. search_form=search_form)
  402. @forum.route("/topictracker")
  403. @login_required
  404. def topictracker():
  405. page = request.args.get("page", 1, type=int)
  406. topics = current_user.tracked_topics.\
  407. outerjoin(TopicsRead,
  408. db.and_(TopicsRead.topic_id == Topic.id,
  409. TopicsRead.user_id == current_user.id)).\
  410. add_entity(TopicsRead).\
  411. order_by(Topic.last_updated.desc()).\
  412. paginate(page, flaskbb_config['TOPICS_PER_PAGE'], True)
  413. return render_template("forum/topictracker.html", topics=topics)
  414. @forum.route("/topictracker/<int:topic_id>/add")
  415. @forum.route("/topictracker/<int:topic_id>-<slug>/add")
  416. @login_required
  417. def track_topic(topic_id, slug=None):
  418. topic = Topic.query.filter_by(id=topic_id).first_or_404()
  419. current_user.track_topic(topic)
  420. current_user.save()
  421. return redirect(topic.url)
  422. @forum.route("/topictracker/<int:topic_id>/delete")
  423. @forum.route("/topictracker/<int:topic_id>-<slug>/delete")
  424. @login_required
  425. def untrack_topic(topic_id, slug=None):
  426. topic = Topic.query.filter_by(id=topic_id).first_or_404()
  427. current_user.untrack_topic(topic)
  428. current_user.save()
  429. return redirect(topic.url)
  430. @forum.route("/search", methods=['GET', 'POST'])
  431. def search():
  432. form = SearchPageForm()
  433. if form.validate_on_submit():
  434. result = form.get_results()
  435. return render_template('forum/search_result.html', form=form,
  436. result=result)
  437. return render_template('forum/search_form.html', form=form)