forum.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560
  1. from django.contrib import messages
  2. from django.core.urlresolvers import reverse
  3. from django.db.transaction import atomic
  4. from django.shortcuts import redirect
  5. from django.utils.translation import ugettext_lazy, ugettext as _, ungettext
  6. from misago.core.shortcuts import paginate
  7. from misago.forums.lists import get_forums_list, get_forum_path
  8. from misago.threads import moderation
  9. from misago.threads.models import ANNOUNCEMENT, Thread, Label
  10. from misago.threads.permissions import exclude_invisible_threads
  11. from misago.threads.views.generic.threads import (Actions, Sorting, Threads,
  12. ThreadsView)
  13. __all__ = ['ForumFiltering', 'ForumThreads', 'ForumView']
  14. class ForumActions(Actions):
  15. def get_available_actions(self, kwargs):
  16. self.forum = kwargs['forum']
  17. actions = []
  18. if self.forum.acl['can_change_threads_labels'] == 2:
  19. for label in self.forum.labels:
  20. actions.append({
  21. 'action': 'label:%s' % label.slug,
  22. 'icon': 'tag',
  23. 'name': _('Label as "%(label)s"') % {'label': label.name}
  24. })
  25. if self.forum.labels:
  26. actions.append({
  27. 'action': 'unlabel',
  28. 'icon': 'times-circle',
  29. 'name': _("Remove labels")
  30. })
  31. if self.forum.acl['can_change_threads_weight'] == 2:
  32. actions.append({
  33. 'action': 'announce',
  34. 'icon': 'star',
  35. 'name': _("Change to announcements")
  36. })
  37. if self.forum.acl['can_change_threads_weight']:
  38. actions.append({
  39. 'action': 'pin',
  40. 'icon': 'bookmark',
  41. 'name': _("Change to pinned")
  42. })
  43. actions.append({
  44. 'action': 'reset',
  45. 'icon': 'circle',
  46. 'name': _("Reset weight")
  47. })
  48. if self.forum.acl['can_review_moderated_content']:
  49. actions.append({
  50. 'action': 'approve',
  51. 'icon': 'check',
  52. 'name': _("Approve threads")
  53. })
  54. if self.forum.acl['can_move_threads']:
  55. actions.append({
  56. 'action': 'move',
  57. 'icon': 'arrow-right',
  58. 'name': _("Move threads")
  59. })
  60. if self.forum.acl['can_merge_threads']:
  61. actions.append({
  62. 'action': 'merge',
  63. 'icon': 'reply-all',
  64. 'name': _("Merge threads")
  65. })
  66. if self.forum.acl['can_close_threads']:
  67. actions.append({
  68. 'action': 'open',
  69. 'icon': 'unlock-alt',
  70. 'name': _("Open threads")
  71. })
  72. actions.append({
  73. 'action': 'close',
  74. 'icon': 'lock',
  75. 'name': _("Close threads")
  76. })
  77. if self.forum.acl['can_hide_threads']:
  78. actions.append({
  79. 'action': 'unhide',
  80. 'icon': 'eye',
  81. 'name': _("Unhide threads")
  82. })
  83. actions.append({
  84. 'action': 'hide',
  85. 'icon': 'eye-slash',
  86. 'name': _("Hide threads")
  87. })
  88. if self.forum.acl['can_hide_threads'] == 2:
  89. actions.append({
  90. 'action': 'delete',
  91. 'icon': 'times',
  92. 'name': _("Delete threads"),
  93. 'confirmation': _("Are you sure you want to delete selected "
  94. "threads? This action can't be undone.")
  95. })
  96. return actions
  97. def action_label(self, request, threads, label_slug):
  98. for label in self.forum.labels:
  99. if label.slug == label_slug:
  100. break
  101. else:
  102. raise ModerationError(_("Requested action is invalid."))
  103. changed_threads = 0
  104. for thread in threads:
  105. if moderation.label_thread(request.user, thread, label):
  106. changed_threads += 1
  107. if changed_threads:
  108. message = ungettext(
  109. '%(changed)d thread was labeled "%(label)s".',
  110. '%(changed)d threads were labeled "%(label)s".',
  111. changed_threads)
  112. messages.success(request, message % {
  113. 'changed': changed_threads,
  114. 'label': label.name
  115. })
  116. else:
  117. message = ("No threads were labeled.")
  118. messages.info(request, message)
  119. def action_unlabel(self, request, threads):
  120. changed_threads = 0
  121. for thread in threads:
  122. if moderation.unlabel_thread(request.user, thread):
  123. changed_threads += 1
  124. if changed_threads:
  125. message = ungettext(
  126. '%(changed)d thread label was remoded.',
  127. '%(changed)d threads labels were removed.',
  128. changed_threads)
  129. messages.success(request, message % {'changed': changed_threads})
  130. else:
  131. message = ("No threads were unlabeled.")
  132. messages.info(request, message)
  133. def action_announce(self, request, threads):
  134. changed_threads = 0
  135. for thread in threads:
  136. if moderation.announce_thread(request.user, thread):
  137. changed_threads += 1
  138. if changed_threads:
  139. message = ungettext(
  140. '%(changed)d thread was changed to announcement.',
  141. '%(changed)d threads were changed to announcements.',
  142. changed_threads)
  143. messages.success(request, message % {'changed': changed_threads})
  144. else:
  145. message = ("No threads were changed to announcements.")
  146. messages.info(request, message)
  147. def action_pin(self, request, threads):
  148. changed_threads = 0
  149. for thread in threads:
  150. if moderation.pin_thread(request.user, thread):
  151. changed_threads += 1
  152. if changed_threads:
  153. message = ungettext(
  154. '%(changed)d thread was pinned.',
  155. '%(changed)d threads were pinned.',
  156. changed_threads)
  157. messages.success(request, message % {'changed': changed_threads})
  158. else:
  159. message = ("No threads were pinned.")
  160. messages.info(request, message)
  161. def action_reset(self, request, threads):
  162. changed_threads = 0
  163. for thread in threads:
  164. if moderation.reset_thread(request.user, thread):
  165. changed_threads += 1
  166. if changed_threads:
  167. message = ungettext(
  168. '%(changed)d thread weight was reset.',
  169. '%(changed)d threads weight was reset.',
  170. changed_threads)
  171. messages.success(request, message % {'changed': changed_threads})
  172. else:
  173. message = ("No threads weight was reset.")
  174. messages.info(request, message)
  175. def action_close(self, request, threads):
  176. changed_threads = 0
  177. for thread in threads:
  178. if moderation.close_thread(request.user, thread):
  179. changed_threads += 1
  180. if changed_threads:
  181. message = ungettext(
  182. '%(changed)d thread was closed.',
  183. '%(changed)d threads were closed.',
  184. changed_threads)
  185. messages.success(request, message % {'changed': changed_threads})
  186. else:
  187. message = ("No threads were closed.")
  188. messages.info(request, message)
  189. def action_open(self, request, threads):
  190. changed_threads = 0
  191. for thread in threads:
  192. if moderation.open_thread(request.user, thread):
  193. changed_threads += 1
  194. if changed_threads:
  195. message = ungettext(
  196. '%(changed)d thread was opened.',
  197. '%(changed)d threads were opened.',
  198. changed_threads)
  199. messages.success(request, message % {'changed': changed_threads})
  200. else:
  201. message = ("No threads were opened.")
  202. messages.info(request, message)
  203. def action_unhide(self, request, threads):
  204. changed_threads = 0
  205. for thread in threads:
  206. if moderation.unhide_thread(request.user, thread):
  207. changed_threads += 1
  208. if changed_threads:
  209. with atomic():
  210. self.forum.synchronize()
  211. self.forum.save()
  212. message = ungettext(
  213. '%(changed)d thread was made visible.',
  214. '%(changed)d threads were made visible.',
  215. changed_threads)
  216. messages.success(request, message % {'changed': changed_threads})
  217. else:
  218. message = ("No threads were made visible.")
  219. messages.info(request, message)
  220. def action_hide(self, request, threads):
  221. changed_threads = 0
  222. for thread in threads:
  223. if moderation.hide_thread(request.user, thread):
  224. changed_threads += 1
  225. if changed_threads:
  226. with atomic():
  227. self.forum.synchronize()
  228. self.forum.save()
  229. message = ungettext(
  230. '%(changed)d thread was hidden.',
  231. '%(changed)d threads were hidden.',
  232. changed_threads)
  233. messages.success(request, message % {'changed': changed_threads})
  234. else:
  235. message = ("No threads were hidden.")
  236. messages.info(request, message)
  237. def action_delete(self, request, threads):
  238. changed_threads = 0
  239. for thread in threads:
  240. if moderation.delete_thread(request.user, thread):
  241. changed_threads += 1
  242. if changed_threads:
  243. with atomic():
  244. self.forum.synchronize()
  245. self.forum.save()
  246. message = ungettext(
  247. '%(changed)d thread was deleted.',
  248. '%(changed)d threads were deleted.',
  249. changed_threads)
  250. messages.success(request, message % {'changed': changed_threads})
  251. else:
  252. message = ("No threads were deleted.")
  253. messages.info(request, message)
  254. class ForumFiltering(object):
  255. def __init__(self, forum, link_name, link_params):
  256. self.forum = forum
  257. self.link_name = link_name
  258. self.link_params = link_params.copy()
  259. self.filters = self.get_available_filters()
  260. def get_available_filters(self):
  261. filters = []
  262. if self.forum.acl['can_see_all_threads']:
  263. filters.append({
  264. 'type': 'my-threads',
  265. 'name': _("My threads"),
  266. 'is_label': False,
  267. })
  268. if self.forum.acl['can_see_reports']:
  269. filters.append({
  270. 'type': 'reported',
  271. 'name': _("With reported posts"),
  272. 'is_label': False,
  273. })
  274. if self.forum.acl['can_review_moderated_content']:
  275. filters.extend(({
  276. 'type': 'moderated-threads',
  277. 'name': _("Moderated threads"),
  278. 'is_label': False,
  279. },
  280. {
  281. 'type': 'moderated-posts',
  282. 'name': _("With moderated posts"),
  283. 'is_label': False,
  284. }))
  285. for label in self.forum.labels:
  286. filters.append({
  287. 'type': label.slug,
  288. 'name': label.name,
  289. 'is_label': True,
  290. 'css_class': label.css_class,
  291. })
  292. return filters
  293. def clean_kwargs(self, kwargs):
  294. show = kwargs.get('show')
  295. if show:
  296. available_filters = [method['type'] for method in self.filters]
  297. if show in available_filters:
  298. self.show = show
  299. else:
  300. kwargs.pop('show')
  301. else:
  302. self.show = None
  303. return kwargs
  304. def filter(self, threads):
  305. threads.filter(self.show)
  306. def get_filtering_dics(self):
  307. try:
  308. return self._dicts
  309. except AttributeError:
  310. self._dicts = self.create_dicts()
  311. return self._dicts
  312. def create_dicts(self):
  313. dicts = []
  314. if self.forum.acl['can_see_all_threads']:
  315. default_name = _("All threads")
  316. else:
  317. default_name = _("Your threads")
  318. self.link_params.pop('show', None)
  319. dicts.append({
  320. 'type': None,
  321. 'url': reverse(self.link_name, kwargs=self.link_params),
  322. 'name': default_name,
  323. 'is_label': False,
  324. })
  325. for filtering in self.filters:
  326. self.link_params['show'] = filtering['type']
  327. filtering['url'] = reverse(self.link_name, kwargs=self.link_params)
  328. dicts.append(filtering)
  329. return dicts
  330. @property
  331. def is_active(self):
  332. return bool(self.show)
  333. @property
  334. def current(self):
  335. try:
  336. return self._current
  337. except AttributeError:
  338. for filtering in self.get_filtering_dics():
  339. if filtering['type'] == self.show:
  340. self._current = filtering
  341. return filtering
  342. def choices(self):
  343. if self.show:
  344. choices = []
  345. for filtering in self.get_filtering_dics():
  346. if filtering['type'] != self.show:
  347. choices.append(filtering)
  348. return choices
  349. else:
  350. return self.get_filtering_dics()[1:]
  351. class ForumThreads(Threads):
  352. def __init__(self, user, forum):
  353. self.user = user
  354. self.forum = forum
  355. self.filter_by = None
  356. self.sort_by = ('-weight', '-last_post_on')
  357. def filter(self, filter_by):
  358. self.filter_by = filter_by
  359. def sort(self, sort_by):
  360. if sort_by[0] == '-':
  361. weight = '-weight'
  362. else:
  363. weight = 'weight'
  364. self.sort_by = (weight, sort_by)
  365. def list(self, page=0):
  366. queryset = self.get_queryset()
  367. queryset = queryset.order_by(*self.sort_by)
  368. announcements_qs = queryset.filter(weight=ANNOUNCEMENT)
  369. threads_qs = queryset.filter(weight__lt=ANNOUNCEMENT)
  370. self._page = paginate(threads_qs, page, 20, 10)
  371. self._paginator = self._page.paginator
  372. threads = []
  373. for announcement in announcements_qs:
  374. threads.append(announcement)
  375. for thread in self._page.object_list:
  376. threads.append(thread)
  377. for thread in threads:
  378. thread.forum = self.forum
  379. self.label_threads(threads, self.forum.labels)
  380. self.make_threads_read_aware(threads)
  381. return threads
  382. def filter_threads(self, queryset):
  383. if self.filter_by == 'my-threads':
  384. return queryset.filter(starter_id=self.user.id)
  385. else:
  386. if self.forum.acl['can_see_own_threads']:
  387. if self.user.is_authenticated():
  388. queryset = queryset.filter(starter_id=self.user.id)
  389. else:
  390. queryset = queryset.filter(starter_id=0)
  391. if self.filter_by == 'reported':
  392. return queryset.filter(has_reported_posts=True)
  393. elif self.filter_by == 'moderated-threads':
  394. return queryset.filter(is_moderated=True)
  395. elif self.filter_by == 'moderated-posts':
  396. return queryset.filter(has_moderated_posts=True)
  397. else:
  398. for label in self.forum.labels:
  399. if label.slug == self.filter_by:
  400. return queryset.filter(label_id=label.pk)
  401. else:
  402. return queryset
  403. def get_queryset(self):
  404. queryset = exclude_invisible_threads(
  405. self.user, self.forum, self.forum.thread_set)
  406. return self.filter_threads(queryset)
  407. error_message = ("threads list has to be loaded via call to list() before "
  408. "pagination data will be available")
  409. @property
  410. def paginator(self):
  411. try:
  412. return self._paginator
  413. except AttributeError:
  414. raise AttributeError(self.error_message)
  415. @property
  416. def page(self):
  417. try:
  418. return self._page
  419. except AttributeError:
  420. raise AttributeError(self.error_message)
  421. class ForumView(ThreadsView):
  422. """
  423. Basic view for forum threads lists
  424. """
  425. template = 'misago/threads/forum.html'
  426. Threads = ForumThreads
  427. Sorting = Sorting
  428. Filtering = ForumFiltering
  429. Actions = ForumActions
  430. def dispatch(self, request, *args, **kwargs):
  431. forum = self.get_forum(request, **kwargs)
  432. forum.labels = Label.objects.get_forum_labels(forum)
  433. if forum.lft + 1 < forum.rght:
  434. forum.subforums = get_forums_list(request.user, forum)
  435. else:
  436. forum.subforums = []
  437. page_number = kwargs.pop('page', None)
  438. cleaned_kwargs = self.clean_kwargs(request, kwargs)
  439. sorting = self.Sorting(self.link_name, cleaned_kwargs)
  440. cleaned_kwargs = sorting.clean_kwargs(cleaned_kwargs)
  441. filtering = self.Filtering(forum, self.link_name, cleaned_kwargs)
  442. cleaned_kwargs = filtering.clean_kwargs(cleaned_kwargs)
  443. if cleaned_kwargs != kwargs:
  444. return redirect('misago:forum', **cleaned_kwargs)
  445. threads = self.Threads(request.user, forum)
  446. sorting.sort(threads)
  447. filtering.filter(threads)
  448. actions = self.Actions(user=request.user, forum=forum)
  449. if request.method == 'POST':
  450. # see if we can delegate anything to actions manager
  451. response = actions.handle_post(request, threads.get_queryset())
  452. if response:
  453. return response
  454. return self.render(request, {
  455. 'link_name': self.link_name,
  456. 'links_params': cleaned_kwargs,
  457. 'forum': forum,
  458. 'path': get_forum_path(forum),
  459. 'threads': threads.list(page_number),
  460. 'page': threads.page,
  461. 'paginator': threads.paginator,
  462. 'list_actions': actions.get_list(),
  463. 'selected_threads': actions.get_selected_ids(),
  464. 'sorting': sorting,
  465. 'filtering': filtering,
  466. })