views.py 38 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241
  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 logging
  11. import math
  12. from flask import (Blueprint, abort, current_app, flash, redirect, request,
  13. url_for)
  14. from flask.views import MethodView
  15. from flask_allows import And, Permission
  16. from flask_babelplus import gettext as _
  17. from flask_login import current_user, login_required
  18. from pluggy import HookimplMarker
  19. from sqlalchemy import asc, desc
  20. from flaskbb.extensions import allows, db
  21. from flaskbb.forum.forms import (NewTopicForm, QuickreplyForm, ReplyForm,
  22. ReportForm, SearchPageForm, UserSearchForm)
  23. from flaskbb.forum.models import (Category, Forum, ForumsRead, Post, Topic,
  24. TopicsRead)
  25. from flaskbb.user.models import User
  26. from flaskbb.utils.helpers import (do_topic_action, format_quote,
  27. get_online_users, real, register_view,
  28. render_template, time_diff, time_utcnow,
  29. FlashAndRedirect)
  30. from flaskbb.utils.requirements import (CanAccessForum,
  31. CanDeletePost, CanDeleteTopic,
  32. CanEditPost, CanPostReply,
  33. CanPostTopic, Has,
  34. IsAtleastModeratorInForum)
  35. from flaskbb.utils.settings import flaskbb_config
  36. from .locals import current_topic, current_forum, current_category
  37. impl = HookimplMarker('flaskbb')
  38. logger = logging.getLogger(__name__)
  39. class ForumIndex(MethodView):
  40. def get(self):
  41. categories = Category.get_all(user=real(current_user))
  42. # Fetch a few stats about the forum
  43. user_count = User.query.count()
  44. topic_count = Topic.query.count()
  45. post_count = Post.query.count()
  46. newest_user = User.query.order_by(User.id.desc()).first()
  47. # Check if we use redis or not
  48. if not current_app.config['REDIS_ENABLED']:
  49. online_users = User.query.filter(User.lastseen >= time_diff()
  50. ).count()
  51. # Because we do not have server side sessions,
  52. # we cannot check if there are online guests
  53. online_guests = None
  54. else:
  55. online_users = len(get_online_users())
  56. online_guests = len(get_online_users(guest=True))
  57. return render_template(
  58. 'forum/index.html',
  59. categories=categories,
  60. user_count=user_count,
  61. topic_count=topic_count,
  62. post_count=post_count,
  63. newest_user=newest_user,
  64. online_users=online_users,
  65. online_guests=online_guests
  66. )
  67. class ViewCategory(MethodView):
  68. def get(self, category_id, slug=None):
  69. category, forums = Category.get_forums(
  70. category_id=category_id, user=real(current_user)
  71. )
  72. return render_template(
  73. 'forum/category.html', forums=forums, category=category
  74. )
  75. class ViewForum(MethodView):
  76. decorators = [allows.requires(
  77. CanAccessForum(),
  78. on_fail=FlashAndRedirect(
  79. message=_("You are not allowed to access that forum"),
  80. level="warning",
  81. endpoint=lambda *a, **k: current_category.url
  82. )
  83. )]
  84. def get(self, forum_id, slug=None):
  85. page = request.args.get('page', 1, type=int)
  86. forum_instance, forumsread = Forum.get_forum(
  87. forum_id=forum_id, user=real(current_user)
  88. )
  89. if forum_instance.external:
  90. return redirect(forum_instance.external)
  91. topics = Forum.get_topics(
  92. forum_id=forum_instance.id,
  93. user=real(current_user),
  94. page=page,
  95. per_page=flaskbb_config['TOPICS_PER_PAGE']
  96. )
  97. return render_template(
  98. 'forum/forum.html',
  99. forum=forum_instance,
  100. topics=topics,
  101. forumsread=forumsread,
  102. )
  103. class ViewPost(MethodView):
  104. decorators = [allows.requires(
  105. CanAccessForum(),
  106. on_fail=FlashAndRedirect(
  107. message=_("You are not allowed to access that topic"),
  108. level="warning",
  109. endpoint=lambda *a, **k: current_category.url
  110. )
  111. )]
  112. def get(self, post_id):
  113. '''Redirects to a post in a topic.'''
  114. post = Post.query.filter_by(id=post_id).first_or_404()
  115. post_in_topic = Post.query.filter(
  116. Post.topic_id == post.topic_id, Post.id <= post_id
  117. ).order_by(Post.id.asc()).count()
  118. page = int(
  119. math.ceil(post_in_topic / float(flaskbb_config['POSTS_PER_PAGE']))
  120. )
  121. return redirect(
  122. url_for(
  123. 'forum.view_topic',
  124. topic_id=post.topic.id,
  125. slug=post.topic.slug,
  126. page=page,
  127. _anchor='pid{}'.format(post.id)
  128. )
  129. )
  130. class ViewTopic(MethodView):
  131. decorators = [allows.requires(
  132. CanAccessForum(),
  133. on_fail=FlashAndRedirect(
  134. message=_("You are not allowed to access that topic"),
  135. level="warning",
  136. endpoint=lambda *a, **k: current_category.url
  137. )
  138. )]
  139. def get(self, topic_id, slug=None):
  140. page = request.args.get('page', 1, type=int)
  141. # Fetch some information about the topic
  142. topic = Topic.get_topic(topic_id=topic_id, user=real(current_user))
  143. # Count the topic views
  144. topic.views += 1
  145. topic.save()
  146. # Update the topicsread status if the user hasn't read it
  147. forumsread = None
  148. if current_user.is_authenticated:
  149. forumsread = ForumsRead.query.filter_by(
  150. user_id=current_user.id,
  151. forum_id=topic.forum_id).first()
  152. topic.update_read(real(current_user), topic.forum, forumsread)
  153. # fetch the posts in the topic
  154. posts = Post.query.outerjoin(
  155. User, Post.user_id == User.id
  156. ).filter(
  157. Post.topic_id == topic.id
  158. ).add_entity(
  159. User
  160. ).order_by(
  161. Post.id.asc()
  162. ).paginate(page, flaskbb_config['POSTS_PER_PAGE'], False)
  163. # Abort if there are no posts on this page
  164. if len(posts.items) == 0:
  165. abort(404)
  166. return render_template(
  167. 'forum/topic.html',
  168. topic=topic,
  169. posts=posts,
  170. last_seen=time_diff(),
  171. form=self.form()
  172. )
  173. @allows.requires(
  174. CanPostReply,
  175. on_fail=FlashAndRedirect(
  176. message=_("You are not allowed to post a reply to this topic."),
  177. level="warning",
  178. endpoint=lambda *a, **k: url_for(
  179. "forum.view_topic",
  180. topic_id=k['topic_id'],
  181. )
  182. )
  183. )
  184. def post(self, topic_id, slug=None):
  185. topic = Topic.get_topic(topic_id=topic_id, user=real(current_user))
  186. form = self.form()
  187. if not form:
  188. flash(_('Cannot post reply'), 'warning')
  189. return redirect('forum.view_topic', topic_id=topic_id, slug=slug)
  190. elif form.validate_on_submit():
  191. post = form.save(real(current_user), topic)
  192. return redirect(url_for('forum.view_post', post_id=post.id))
  193. else:
  194. for e in form.errors.get('content', []):
  195. flash(e, 'danger')
  196. return redirect(
  197. url_for('forum.view_topic', topic_id=topic_id, slug=slug)
  198. )
  199. def form(self):
  200. if Permission(CanPostReply):
  201. return QuickreplyForm()
  202. return None
  203. class NewTopic(MethodView):
  204. decorators = [
  205. login_required,
  206. allows.requires(
  207. CanAccessForum(), CanPostTopic,
  208. on_fail=FlashAndRedirect(
  209. message=_("You are not allowed to post a topic here"),
  210. level="warning",
  211. endpoint=lambda *a, **k: current_forum.url
  212. )
  213. ),
  214. ]
  215. def get(self, forum_id, slug=None):
  216. forum_instance = Forum.query.filter_by(id=forum_id).first_or_404()
  217. return render_template(
  218. 'forum/new_topic.html', forum=forum_instance, form=self.form()
  219. )
  220. def post(self, forum_id, slug=None):
  221. forum_instance = Forum.query.filter_by(id=forum_id).first_or_404()
  222. form = self.form()
  223. if 'preview' in request.form and form.validate():
  224. return render_template(
  225. 'forum/new_topic.html',
  226. forum=forum_instance,
  227. form=form,
  228. preview=form.content.data
  229. )
  230. elif 'submit' in request.form and form.validate():
  231. topic = form.save(real(current_user), forum_instance)
  232. # redirect to the new topic
  233. return redirect(url_for('forum.view_topic', topic_id=topic.id))
  234. else:
  235. return render_template(
  236. 'forum/new_topic.html', forum=forum_instance, form=form
  237. )
  238. def form(self):
  239. current_app.pluggy.hook.flaskbb_form_new_topic(form=NewTopicForm)
  240. return NewTopicForm()
  241. class ManageForum(MethodView):
  242. decorators = [
  243. login_required,
  244. allows.requires(
  245. IsAtleastModeratorInForum(),
  246. on_fail=FlashAndRedirect(
  247. message=_("You are not allowed to manage this forum"),
  248. level="danger",
  249. endpoint=lambda *a, **k: url_for(
  250. "forum.view_forum",
  251. forum_id=k["forum_id"],
  252. )
  253. )
  254. ),
  255. ]
  256. def get(self, forum_id, slug=None):
  257. forum_instance, forumsread = Forum.get_forum(
  258. forum_id=forum_id, user=real(current_user)
  259. )
  260. if forum_instance.external:
  261. return redirect(forum_instance.external)
  262. # remove the current forum from the select field (move).
  263. available_forums = Forum.query.order_by(Forum.position).all()
  264. available_forums.remove(forum_instance)
  265. page = request.args.get('page', 1, type=int)
  266. topics = Forum.get_topics(
  267. forum_id=forum_instance.id,
  268. user=real(current_user),
  269. page=page,
  270. per_page=flaskbb_config['TOPICS_PER_PAGE']
  271. )
  272. return render_template(
  273. 'forum/edit_forum.html',
  274. forum=forum_instance,
  275. topics=topics,
  276. available_forums=available_forums,
  277. forumsread=forumsread,
  278. )
  279. # TODO(anr): Clean this up. @_@
  280. def post(self, forum_id, slug=None):
  281. forum_instance, __ = Forum.get_forum(
  282. forum_id=forum_id, user=real(current_user)
  283. )
  284. mod_forum_url = url_for(
  285. 'forum.manage_forum',
  286. forum_id=forum_instance.id,
  287. slug=forum_instance.slug
  288. )
  289. ids = request.form.getlist('rowid')
  290. tmp_topics = Topic.query.filter(Topic.id.in_(ids)).all()
  291. if not len(tmp_topics) > 0:
  292. flash(
  293. _(
  294. 'In order to perform this action you have to select at '
  295. 'least one topic.'
  296. ), 'danger'
  297. )
  298. return redirect(mod_forum_url)
  299. # locking/unlocking
  300. if 'lock' in request.form:
  301. changed = do_topic_action(
  302. topics=tmp_topics,
  303. user=real(current_user),
  304. action='locked',
  305. reverse=False
  306. )
  307. flash(_('%(count)s topics locked.', count=changed), 'success')
  308. return redirect(mod_forum_url)
  309. elif 'unlock' in request.form:
  310. changed = do_topic_action(
  311. topics=tmp_topics,
  312. user=real(current_user),
  313. action='locked',
  314. reverse=True
  315. )
  316. flash(_('%(count)s topics unlocked.', count=changed), 'success')
  317. return redirect(mod_forum_url)
  318. # highlighting/trivializing
  319. elif 'highlight' in request.form:
  320. changed = do_topic_action(
  321. topics=tmp_topics,
  322. user=real(current_user),
  323. action='important',
  324. reverse=False
  325. )
  326. flash(_('%(count)s topics highlighted.', count=changed), 'success')
  327. return redirect(mod_forum_url)
  328. elif 'trivialize' in request.form:
  329. changed = do_topic_action(
  330. topics=tmp_topics,
  331. user=real(current_user),
  332. action='important',
  333. reverse=True
  334. )
  335. flash(_('%(count)s topics trivialized.', count=changed), 'success')
  336. return redirect(mod_forum_url)
  337. # deleting
  338. elif 'delete' in request.form:
  339. changed = do_topic_action(
  340. topics=tmp_topics,
  341. user=real(current_user),
  342. action='delete',
  343. reverse=False
  344. )
  345. flash(_('%(count)s topics deleted.', count=changed), 'success')
  346. return redirect(mod_forum_url)
  347. # moving
  348. elif 'move' in request.form:
  349. new_forum_id = request.form.get('forum')
  350. if not new_forum_id:
  351. flash(_('Please choose a new forum for the topics.'), 'info')
  352. return redirect(mod_forum_url)
  353. new_forum = Forum.query.filter_by(id=new_forum_id).first_or_404()
  354. # check the permission in the current forum and in the new forum
  355. if not Permission(
  356. And(IsAtleastModeratorInForum(forum_id=new_forum_id),
  357. IsAtleastModeratorInForum(forum=forum_instance))):
  358. flash(
  359. _('You do not have the permissions to move this topic.'),
  360. 'danger'
  361. )
  362. return redirect(mod_forum_url)
  363. if new_forum.move_topics_to(tmp_topics):
  364. flash(_('Topics moved.'), 'success')
  365. else:
  366. flash(_('Failed to move topics.'), 'danger')
  367. return redirect(mod_forum_url)
  368. # hiding/unhiding
  369. elif "hide" in request.form:
  370. changed = do_topic_action(
  371. topics=tmp_topics,
  372. user=real(current_user),
  373. action="hide",
  374. reverse=False
  375. )
  376. flash(_("%(count)s topics hidden.", count=changed), "success")
  377. return redirect(mod_forum_url)
  378. elif "unhide" in request.form:
  379. changed = do_topic_action(
  380. topics=tmp_topics,
  381. user=real(current_user),
  382. action="unhide",
  383. reverse=False
  384. )
  385. flash(_("%(count)s topics unhidden.", count=changed), "success")
  386. return redirect(mod_forum_url)
  387. else:
  388. flash(_('Unknown action requested'), 'danger')
  389. return redirect(mod_forum_url)
  390. class NewPost(MethodView):
  391. decorators = [
  392. login_required,
  393. allows.requires(
  394. CanAccessForum(), CanPostReply,
  395. on_fail=FlashAndRedirect(
  396. message=_("You are not allowed to post a reply"),
  397. level="warning",
  398. endpoint=lambda *a, **k: url_for(
  399. "forum.view_topic",
  400. topic_id=k["topic_id"],
  401. )
  402. )
  403. ),
  404. ]
  405. def get(self, topic_id, slug=None, post_id=None):
  406. topic = Topic.query.filter_by(id=topic_id).first_or_404()
  407. form = self.form()
  408. if post_id is not None:
  409. post = Post.query.filter_by(id=post_id).first_or_404()
  410. form.content.data = format_quote(post.username, post.content)
  411. return render_template(
  412. 'forum/new_post.html', topic=topic, form=form
  413. )
  414. def post(self, topic_id, slug=None, post_id=None):
  415. topic = Topic.query.filter_by(id=topic_id).first_or_404()
  416. form = self.form()
  417. # check if topic exists
  418. if post_id is not None:
  419. post = Post.query.filter_by(id=post_id).first_or_404()
  420. if form.validate_on_submit():
  421. if 'preview' in request.form:
  422. return render_template(
  423. 'forum/new_post.html',
  424. topic=topic,
  425. form=form,
  426. preview=form.content.data
  427. )
  428. else:
  429. post = form.save(real(current_user), topic)
  430. return redirect(url_for('forum.view_post', post_id=post.id))
  431. return render_template('forum/new_post.html', topic=topic, form=form)
  432. def form(self):
  433. current_app.pluggy.hook.flaskbb_form_new_post(form=ReplyForm)
  434. return ReplyForm()
  435. class EditPost(MethodView):
  436. decorators = [
  437. allows.requires(
  438. CanEditPost,
  439. on_fail=FlashAndRedirect(
  440. message=_("You are not allowed to edit that post"),
  441. level="danger",
  442. endpoint=lambda *a, **k: current_topic.url
  443. )
  444. ),
  445. login_required
  446. ]
  447. def get(self, post_id):
  448. post = Post.query.filter_by(id=post_id).first_or_404()
  449. form = self.form(obj=post)
  450. return render_template(
  451. 'forum/new_post.html', topic=post.topic, form=form, edit_mode=True
  452. )
  453. def post(self, post_id):
  454. post = Post.query.filter_by(id=post_id).first_or_404()
  455. form = self.form(obj=post)
  456. if form.validate_on_submit():
  457. if 'preview' in request.form:
  458. return render_template(
  459. 'forum/new_post.html',
  460. topic=post.topic,
  461. form=form,
  462. preview=form.content.data,
  463. edit_mode=True
  464. )
  465. else:
  466. form.populate_obj(post)
  467. post.date_modified = time_utcnow()
  468. post.modified_by = real(current_user).username
  469. post.save()
  470. return redirect(url_for('forum.view_post', post_id=post.id))
  471. return render_template(
  472. 'forum/new_post.html', topic=post.topic, form=form, edit_mode=True
  473. )
  474. def form(self, **kwargs):
  475. current_app.pluggy.hook.flaskbb_form_new_post(form=ReplyForm)
  476. return ReplyForm(**kwargs)
  477. class ReportView(MethodView):
  478. decorators = [login_required]
  479. form = ReportForm
  480. def get(self, post_id):
  481. return render_template('forum/report_post.html', form=self.form())
  482. def post(self, post_id):
  483. form = self.form()
  484. if form.validate_on_submit():
  485. post = Post.query.filter_by(id=post_id).first_or_404()
  486. form.save(real(current_user), post)
  487. flash(_('Thanks for reporting.'), 'success')
  488. return render_template('forum/report_post.html', form=form)
  489. class MemberList(MethodView):
  490. form = UserSearchForm
  491. def get(self):
  492. page = request.args.get('page', 1, type=int)
  493. sort_by = request.args.get('sort_by', 'reg_date')
  494. order_by = request.args.get('order_by', 'asc')
  495. if order_by == 'asc':
  496. order_func = asc
  497. else:
  498. order_func = desc
  499. if sort_by == 'reg_date':
  500. sort_obj = User.id
  501. elif sort_by == 'post_count':
  502. sort_obj = User.post_count
  503. else:
  504. sort_obj = User.username
  505. users = User.query.order_by(order_func(sort_obj)).paginate(
  506. page, flaskbb_config['USERS_PER_PAGE'], False
  507. )
  508. return render_template(
  509. 'forum/memberlist.html', users=users, search_form=self.form()
  510. )
  511. def post(self):
  512. page = request.args.get('page', 1, type=int)
  513. sort_by = request.args.get('sort_by', 'reg_date')
  514. order_by = request.args.get('order_by', 'asc')
  515. if order_by == 'asc':
  516. order_func = asc
  517. else:
  518. order_func = desc
  519. if sort_by == 'reg_date':
  520. sort_obj = User.id
  521. elif sort_by == 'post_count':
  522. sort_obj = User.post_count
  523. else:
  524. sort_obj = User.username
  525. form = self.form()
  526. if form.validate():
  527. users = form.get_results().paginate(
  528. page, flaskbb_config['USERS_PER_PAGE'], False
  529. )
  530. return render_template(
  531. 'forum/memberlist.html', users=users, search_form=form
  532. )
  533. users = User.query.order_by(order_func(sort_obj)).paginate(
  534. page, flaskbb_config['USERS_PER_PAGE'], False
  535. )
  536. return render_template(
  537. 'forum/memberlist.html', users=users, search_form=form
  538. )
  539. class TopicTracker(MethodView):
  540. decorators = [login_required]
  541. def get(self):
  542. page = request.args.get('page', 1, type=int)
  543. topics = real(current_user).tracked_topics.\
  544. outerjoin(
  545. TopicsRead,
  546. db.and_(
  547. TopicsRead.topic_id == Topic.id,
  548. TopicsRead.user_id == real(current_user).id
  549. )).\
  550. outerjoin(Post, Topic.last_post_id == Post.id).\
  551. outerjoin(Forum, Topic.forum_id == Forum.id).\
  552. outerjoin(
  553. ForumsRead,
  554. db.and_(
  555. ForumsRead.forum_id == Forum.id,
  556. ForumsRead.user_id == real(current_user).id
  557. )).\
  558. add_entity(Post).\
  559. add_entity(TopicsRead).\
  560. add_entity(ForumsRead).\
  561. order_by(Topic.last_updated.desc()).\
  562. paginate(page, flaskbb_config['TOPICS_PER_PAGE'], True)
  563. return render_template('forum/topictracker.html', topics=topics)
  564. def post(self):
  565. topic_ids = request.form.getlist('rowid')
  566. tmp_topics = Topic.query.filter(Topic.id.in_(topic_ids)).all()
  567. for topic in tmp_topics:
  568. real(current_user).untrack_topic(topic)
  569. real(current_user).save()
  570. flash(
  571. _('%(topic_count)s topics untracked.', topic_count=len(tmp_topics)),
  572. 'success'
  573. )
  574. return redirect(url_for('forum.topictracker'))
  575. class Search(MethodView):
  576. form = SearchPageForm
  577. def get(self):
  578. return render_template('forum/search_form.html', form=self.form())
  579. def post(self):
  580. form = self.form()
  581. if form.validate_on_submit():
  582. result = form.get_results()
  583. return render_template(
  584. 'forum/search_result.html', form=form, result=result
  585. )
  586. return render_template('forum/search_form.html', form=form)
  587. class DeleteTopic(MethodView):
  588. decorators = [
  589. login_required,
  590. allows.requires(
  591. CanDeleteTopic,
  592. on_fail=FlashAndRedirect(
  593. message=_("You are not allowed to delete this topic"),
  594. level="danger",
  595. # TODO(anr): consider the referrer -- for now, back to topic
  596. endpoint=lambda *a, **k: current_topic.url
  597. )
  598. ),
  599. ]
  600. def post(self, topic_id, slug=None):
  601. topic = Topic.query.filter_by(id=topic_id).first_or_404()
  602. involved_users = User.query.filter(
  603. Post.topic_id == topic.id, User.id == Post.user_id
  604. ).all()
  605. topic.delete(users=involved_users)
  606. return redirect(url_for('forum.view_forum', forum_id=topic.forum_id))
  607. class LockTopic(MethodView):
  608. decorators = [
  609. login_required,
  610. allows.requires(
  611. IsAtleastModeratorInForum(),
  612. on_fail=FlashAndRedirect(
  613. message=_("You are not allowed to lock this topic"),
  614. level="danger",
  615. # TODO(anr): consider the referrer -- for now, back to topic
  616. endpoint=lambda *a, **k: current_topic.url
  617. )
  618. ),
  619. ]
  620. def post(self, topic_id, slug=None):
  621. topic = Topic.query.filter_by(id=topic_id).first_or_404()
  622. topic.locked = True
  623. topic.save()
  624. return redirect(topic.url)
  625. class UnlockTopic(MethodView):
  626. decorators = [
  627. login_required,
  628. allows.requires(
  629. IsAtleastModeratorInForum(),
  630. on_fail=FlashAndRedirect(
  631. message=_("You are not allowed to unlock this topic"),
  632. level="danger",
  633. # TODO(anr): consider the referrer -- for now, back to topic
  634. endpoint=lambda *a, **k: current_topic.url
  635. )
  636. ),
  637. ]
  638. def post(self, topic_id, slug=None):
  639. topic = Topic.query.filter_by(id=topic_id).first_or_404()
  640. topic.locked = False
  641. topic.save()
  642. return redirect(topic.url)
  643. class HighlightTopic(MethodView):
  644. decorators = [
  645. login_required,
  646. allows.requires(
  647. IsAtleastModeratorInForum(),
  648. on_fail=FlashAndRedirect(
  649. message=_("You are not allowed to highlight this topic"),
  650. level="danger",
  651. # TODO(anr): consider the referrer -- for now, back to topic
  652. endpoint=lambda *a, **k: current_topic.url
  653. )
  654. ),
  655. ]
  656. def post(self, topic_id, slug=None):
  657. topic = Topic.query.filter_by(id=topic_id).first_or_404()
  658. topic.important = True
  659. topic.save()
  660. return redirect(topic.url)
  661. class TrivializeTopic(MethodView):
  662. decorators = [
  663. login_required,
  664. allows.requires(
  665. IsAtleastModeratorInForum(),
  666. on_fail=FlashAndRedirect(
  667. message=_("You are not allowed to trivialize this topic"),
  668. level="danger",
  669. # TODO(anr): consider the referrer -- for now, back to topic
  670. endpoint=lambda *a, **k: current_topic.url
  671. )
  672. ),
  673. ]
  674. def post(self, topic_id=None, slug=None):
  675. topic = Topic.query.filter_by(id=topic_id).first_or_404()
  676. topic.important = False
  677. topic.save()
  678. return redirect(topic.url)
  679. class DeletePost(MethodView):
  680. decorators = [
  681. login_required,
  682. allows.requires(
  683. CanDeletePost,
  684. on_fail=FlashAndRedirect(
  685. message=_("You are not allowed to delete this post"),
  686. level="danger",
  687. endpoint=lambda *a, **k: current_topic.url
  688. )
  689. ),
  690. ]
  691. def post(self, post_id):
  692. post = Post.query.filter_by(id=post_id).first_or_404()
  693. first_post = post.first_post
  694. topic_url = post.topic.url
  695. forum_url = post.topic.forum.url
  696. post.delete()
  697. # If the post was the first post in the topic, redirect to the forums
  698. if first_post:
  699. return redirect(forum_url)
  700. return redirect(topic_url)
  701. class RawPost(MethodView):
  702. decorators = [
  703. login_required,
  704. allows.requires(
  705. CanAccessForum(),
  706. on_fail=FlashAndRedirect(
  707. message=_("You are not allowed to access that forum"),
  708. level="warning",
  709. endpoint=lambda *a, **k: current_category.url
  710. )
  711. ),
  712. ]
  713. def get(self, post_id):
  714. post = Post.query.filter_by(id=post_id).first_or_404()
  715. return format_quote(username=post.username, content=post.content)
  716. class MarkRead(MethodView):
  717. decorators = [
  718. login_required,
  719. allows.requires(
  720. CanAccessForum(),
  721. on_fail=FlashAndRedirect(
  722. message=_("You are not allowed to access that forum"),
  723. level="warning",
  724. endpoint=lambda *a, **k: current_category.url
  725. )
  726. ),
  727. ]
  728. def post(self, forum_id=None, slug=None):
  729. # Mark a single forum as read
  730. if forum_id is not None:
  731. forum_instance = Forum.query.filter_by(id=forum_id).first_or_404()
  732. forumsread = ForumsRead.query.filter_by(
  733. user_id=real(current_user).id, forum_id=forum_instance.id
  734. ).first()
  735. TopicsRead.query.filter_by(
  736. user_id=real(current_user).id, forum_id=forum_instance.id
  737. ).delete()
  738. if not forumsread:
  739. forumsread = ForumsRead()
  740. forumsread.user = real(current_user)
  741. forumsread.forum = forum_instance
  742. forumsread.last_read = time_utcnow()
  743. forumsread.cleared = time_utcnow()
  744. db.session.add(forumsread)
  745. db.session.commit()
  746. flash(
  747. _(
  748. 'Forum %(forum)s marked as read.',
  749. forum=forum_instance.title
  750. ), 'success'
  751. )
  752. return redirect(forum_instance.url)
  753. # Mark all forums as read
  754. ForumsRead.query.filter_by(user_id=real(current_user).id).delete()
  755. TopicsRead.query.filter_by(user_id=real(current_user).id).delete()
  756. forums = Forum.query.all()
  757. forumsread_list = []
  758. for forum_instance in forums:
  759. forumsread = ForumsRead()
  760. forumsread.user = real(current_user)
  761. forumsread.forum = forum_instance
  762. forumsread.last_read = time_utcnow()
  763. forumsread.cleared = time_utcnow()
  764. forumsread_list.append(forumsread)
  765. db.session.add_all(forumsread_list)
  766. db.session.commit()
  767. flash(_('All forums marked as read.'), 'success')
  768. return redirect(url_for('forum.index'))
  769. class WhoIsOnline(MethodView):
  770. def get(self):
  771. if current_app.config['REDIS_ENABLED']:
  772. online_users = get_online_users()
  773. else:
  774. online_users = User.query.filter(User.lastseen >= time_diff()).all()
  775. return render_template(
  776. 'forum/online_users.html', online_users=online_users
  777. )
  778. class TrackTopic(MethodView):
  779. decorators = [
  780. login_required,
  781. allows.requires(
  782. CanAccessForum(),
  783. on_fail=FlashAndRedirect(
  784. message=_("You are not allowed to access that forum"),
  785. level="warning",
  786. endpoint=lambda *a, **k: current_category.url
  787. )
  788. ),
  789. ]
  790. def post(self, topic_id, slug=None):
  791. topic = Topic.query.filter_by(id=topic_id).first_or_404()
  792. real(current_user).track_topic(topic)
  793. real(current_user).save()
  794. return redirect(topic.url)
  795. class UntrackTopic(MethodView):
  796. decorators = [
  797. login_required,
  798. allows.requires(
  799. CanAccessForum(),
  800. on_fail=FlashAndRedirect(
  801. message=_("You are not allowed to access that forum"),
  802. level="warning",
  803. endpoint=lambda *a, **k: current_category.url
  804. )
  805. ),
  806. ]
  807. def post(self, topic_id, slug=None):
  808. topic = Topic.query.filter_by(id=topic_id).first_or_404()
  809. real(current_user).untrack_topic(topic)
  810. real(current_user).save()
  811. return redirect(topic.url)
  812. class HideTopic(MethodView):
  813. decorators = [login_required]
  814. def post(self, topic_id, slug=None):
  815. topic = Topic.query.with_hidden().filter_by(id=topic_id).first_or_404()
  816. if not Permission(Has('makehidden'), IsAtleastModeratorInForum(
  817. forum=topic.forum)):
  818. flash(_("You do not have permission to hide this topic"), "danger")
  819. return redirect(topic.url)
  820. topic.hide(user=current_user)
  821. topic.save()
  822. if Permission(Has('viewhidden')):
  823. return redirect(topic.url)
  824. return redirect(topic.forum.url)
  825. class UnhideTopic(MethodView):
  826. decorators = [login_required]
  827. def post(self, topic_id, slug=None):
  828. topic = Topic.query.filter_by(id=topic_id).first_or_404()
  829. if not Permission(Has('makehidden'), IsAtleastModeratorInForum(
  830. forum=topic.forum)):
  831. flash(
  832. _("You do not have permission to unhide this topic"), "danger"
  833. )
  834. return redirect(topic.url)
  835. topic.unhide()
  836. topic.save()
  837. return redirect(topic.url)
  838. class HidePost(MethodView):
  839. decorators = [login_required]
  840. def post(self, post_id):
  841. post = Post.query.filter(Post.id == post_id).first_or_404()
  842. if not Permission(Has('makehidden'), IsAtleastModeratorInForum(
  843. forum=post.topic.forum)):
  844. flash(_("You do not have permission to hide this post"), "danger")
  845. return redirect(post.topic.url)
  846. if post.hidden:
  847. flash(_("Post is already hidden"), "warning")
  848. return redirect(post.topic.url)
  849. first_post = post.first_post
  850. post.hide(current_user)
  851. post.save()
  852. if first_post:
  853. flash(_("Topic hidden"), "success")
  854. else:
  855. flash(_("Post hidden"), "success")
  856. if post.first_post and not Permission(Has("viewhidden")):
  857. return redirect(post.topic.forum.url)
  858. return redirect(post.topic.url)
  859. class UnhidePost(MethodView):
  860. decorators = [login_required]
  861. def post(self, post_id):
  862. post = Post.query.filter(Post.id == post_id).first_or_404()
  863. if not Permission(Has('makehidden'), IsAtleastModeratorInForum(
  864. forum=post.topic.forum)):
  865. flash(_("You do not have permission to unhide this post"), "danger")
  866. return redirect(post.topic.url)
  867. if not post.hidden:
  868. flash(_("Post is already unhidden"), "warning")
  869. redirect(post.topic.url)
  870. post.unhide()
  871. post.save()
  872. flash(_("Post unhidden"), "success")
  873. return redirect(post.topic.url)
  874. @impl(tryfirst=True)
  875. def flaskbb_load_blueprints(app):
  876. forum = Blueprint("forum", __name__)
  877. register_view(
  878. forum,
  879. routes=[
  880. '/category/<int:category_id>', '/category/<int:category_id>-<slug>'
  881. ],
  882. view_func=ViewCategory.as_view('view_category')
  883. )
  884. register_view(
  885. forum,
  886. routes=[
  887. '/forum/<int:forum_id>/edit', '/forum/<int:forum_id>-<slug>/edit'
  888. ],
  889. view_func=ManageForum.as_view('manage_forum')
  890. )
  891. register_view(
  892. forum,
  893. routes=['/forum/<int:forum_id>', '/forum/<int:forum_id>-<slug>'],
  894. view_func=ViewForum.as_view('view_forum')
  895. )
  896. register_view(
  897. forum,
  898. routes=['/<int:forum_id>/markread', '/<int:forum_id>-<slug>/markread'],
  899. view_func=MarkRead.as_view('markread')
  900. )
  901. register_view(
  902. forum,
  903. routes=[
  904. '/<int:forum_id>/topic/new', '/<int:forum_id>-<slug>/topic/new'
  905. ],
  906. view_func=NewTopic.as_view('new_topic')
  907. )
  908. register_view(
  909. forum,
  910. routes=['/memberlist'],
  911. view_func=MemberList.as_view('memberlist')
  912. )
  913. register_view(
  914. forum,
  915. routes=['/post/<int:post_id>/delete'],
  916. view_func=DeletePost.as_view('delete_post')
  917. )
  918. register_view(
  919. forum,
  920. routes=['/post/<int:post_id>/edit'],
  921. view_func=EditPost.as_view('edit_post')
  922. )
  923. register_view(
  924. forum,
  925. routes=['/post/<int:post_id>/raw'],
  926. view_func=RawPost.as_view('raw_post')
  927. )
  928. register_view(
  929. forum,
  930. routes=['/post/<int:post_id>/report'],
  931. view_func=ReportView.as_view('report_post')
  932. )
  933. register_view(
  934. forum,
  935. routes=['/post/<int:post_id>'],
  936. view_func=ViewPost.as_view('view_post')
  937. )
  938. register_view(forum, routes=['/search'], view_func=Search.as_view('search'))
  939. register_view(
  940. forum,
  941. routes=[
  942. '/topic/<int:topic_id>/delete',
  943. '/topic/<int:topic_id>-<slug>/delete'
  944. ],
  945. view_func=DeleteTopic.as_view('delete_topic')
  946. )
  947. register_view(
  948. forum,
  949. routes=[
  950. '/topic/<int:topic_id>/highlight',
  951. '/topic/<int:topic_id>-<slug>/highlight'
  952. ],
  953. view_func=HighlightTopic.as_view('highlight_topic')
  954. )
  955. register_view(
  956. forum,
  957. routes=[
  958. '/topic/<int:topic_id>/lock', '/topic/<int:topic_id>-<slug>/lock'
  959. ],
  960. view_func=LockTopic.as_view('lock_topic')
  961. )
  962. register_view(
  963. forum,
  964. routes=[
  965. '/topic/<int:topic_id>/post/<int:post_id>/reply',
  966. '/topic/<int:topic_id>-<slug>/post/<int:post_id>/reply'
  967. ],
  968. view_func=NewPost.as_view('reply_post')
  969. )
  970. register_view(
  971. forum,
  972. routes=[
  973. '/topic/<int:topic_id>/post/new',
  974. '/topic/<int:topic_id>-<slug>/post/new'
  975. ],
  976. view_func=NewPost.as_view('new_post')
  977. )
  978. register_view(
  979. forum,
  980. routes=['/topic/<int:topic_id>', '/topic/<int:topic_id>-<slug>'],
  981. view_func=ViewTopic.as_view('view_topic')
  982. )
  983. register_view(
  984. forum,
  985. routes=[
  986. '/topic/<int:topic_id>/trivialize',
  987. '/topic/<int:topic_id>-<slug>/trivialize'
  988. ],
  989. view_func=TrivializeTopic.as_view('trivialize_topic')
  990. )
  991. register_view(
  992. forum,
  993. routes=[
  994. '/topic/<int:topic_id>/unlock',
  995. '/topic/<int:topic_id>-<slug>/unlock'
  996. ],
  997. view_func=UnlockTopic.as_view('unlock_topic')
  998. )
  999. register_view(
  1000. forum,
  1001. routes=[
  1002. '/topictracker/<int:topic_id>/add',
  1003. '/topictracker/<int:topic_id>-<slug>/add'
  1004. ],
  1005. view_func=TrackTopic.as_view('track_topic')
  1006. )
  1007. register_view(
  1008. forum,
  1009. routes=[
  1010. '/topictracker/<int:topic_id>/delete',
  1011. '/topictracker/<int:topic_id>-<slug>/delete'
  1012. ],
  1013. view_func=UntrackTopic.as_view('untrack_topic')
  1014. )
  1015. register_view(
  1016. forum,
  1017. routes=['/topictracker'],
  1018. view_func=TopicTracker.as_view('topictracker')
  1019. )
  1020. register_view(forum, routes=['/'], view_func=ForumIndex.as_view('index'))
  1021. register_view(
  1022. forum,
  1023. routes=['/who-is-online'],
  1024. view_func=WhoIsOnline.as_view('who_is_online')
  1025. )
  1026. register_view(
  1027. forum,
  1028. routes=[
  1029. "/topic/<int:topic_id>/hide", "/topic/<int:topic_id>-<slug>/hide"
  1030. ],
  1031. view_func=HideTopic.as_view('hide_topic')
  1032. )
  1033. register_view(
  1034. forum,
  1035. routes=[
  1036. "/topic/<int:topic_id>/unhide",
  1037. "/topic/<int:topic_id>-<slug>/unhide"
  1038. ],
  1039. view_func=UnhideTopic.as_view('unhide_topic')
  1040. )
  1041. register_view(
  1042. forum,
  1043. routes=["/post/<int:post_id>/hide"],
  1044. view_func=HidePost.as_view('hide_post')
  1045. )
  1046. register_view(
  1047. forum,
  1048. routes=["/post/<int:post_id>/unhide"],
  1049. view_func=UnhidePost.as_view('unhide_post')
  1050. )
  1051. app.register_blueprint(forum, url_prefix=app.config["FORUM_URL_PREFIX"])