list.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. from django.core.urlresolvers import reverse
  2. from django.db.models import Q
  3. from django import forms
  4. from django.forms import ValidationError
  5. from django.shortcuts import redirect
  6. from django.template import RequestContext
  7. from django.utils import timezone
  8. from django.utils.translation import ugettext as _
  9. from misago.acl.utils import ACLError403, ACLError404
  10. from misago.forms import Form, FormLayout, FormFields
  11. from misago.forums.models import Forum
  12. from misago.messages import Message
  13. from misago.readstracker.trackers import ForumsTracker, ThreadsTracker
  14. from misago.threads.forms import MoveThreadsForm, MergeThreadsForm
  15. from misago.threads.models import Thread, Post
  16. from misago.threads.views.base import BaseView
  17. from misago.views import error403, error404
  18. from misago.utils import make_pagination, slugify
  19. class ThreadsView(BaseView):
  20. def fetch_forum(self, forum):
  21. self.forum = Forum.objects.get(pk=forum, type='forum')
  22. self.proxy = Forum.objects.parents_aware_forum(self.forum)
  23. self.request.acl.forums.allow_forum_view(self.forum)
  24. self.parents = Forum.objects.forum_parents(self.forum.pk)
  25. if self.forum.lft + 1 != self.forum.rght:
  26. self.forum.subforums = Forum.objects.treelist(self.request.acl.forums, self.forum, tracker=ForumsTracker(self.request.user))
  27. self.tracker = ThreadsTracker(self.request, self.forum)
  28. def fetch_threads(self, page):
  29. self.count = self.request.acl.threads.filter_threads(self.request, self.forum, Thread.objects.filter(forum=self.forum).filter(weight__lt=2)).count()
  30. self.pagination = make_pagination(page, self.count, self.request.settings.threads_per_page)
  31. self.threads = []
  32. queryset_anno = Thread.objects.filter(Q(forum=Forum.objects.token_to_pk('annoucements')) | (Q(forum=self.forum) & Q(weight=2)))
  33. queryset_threads = self.request.acl.threads.filter_threads(self.request, self.forum, Thread.objects.filter(forum=self.forum).filter(weight__lt=2)).order_by('-weight', '-last')
  34. if self.request.user.is_authenticated():
  35. ignored_users = self.request.user.ignored_users()
  36. if ignored_users:
  37. queryset_threads = queryset_threads.extra(where=["`threads_thread`.`start_poster_id` IS NULL OR `threads_thread`.`start_poster_id` NOT IN (%s)" % ','.join([str(i) for i in ignored_users])])
  38. if self.request.settings.avatars_on_threads_list:
  39. queryset_anno = queryset_anno.prefetch_related('start_poster', 'last_post')
  40. queryset_threads = queryset_threads.prefetch_related('start_poster', 'last_poster')
  41. for thread in queryset_anno:
  42. self.threads.append(thread)
  43. for thread in queryset_threads:
  44. self.threads.append(thread)
  45. if self.request.settings.threads_per_page < self.count:
  46. self.threads = self.threads[self.pagination['start']:self.pagination['stop']]
  47. for thread in self.threads:
  48. thread.is_read = self.tracker.is_read(thread)
  49. thread.last_poster_ignored = thread.last_poster_id in ignored_users
  50. def get_thread_actions(self):
  51. acl = self.request.acl.threads.get_role(self.forum)
  52. actions = []
  53. try:
  54. if acl['can_approve']:
  55. actions.append(('accept', _('Accept threads')))
  56. if acl['can_pin_threads'] == 2:
  57. actions.append(('annouce', _('Change to annoucements')))
  58. if acl['can_pin_threads'] > 0:
  59. actions.append(('sticky', _('Change to sticky threads')))
  60. if acl['can_pin_threads'] > 0:
  61. actions.append(('normal', _('Change to standard thread')))
  62. if acl['can_move_threads_posts']:
  63. actions.append(('move', _('Move threads')))
  64. actions.append(('merge', _('Merge threads')))
  65. if acl['can_close_threads']:
  66. actions.append(('open', _('Open threads')))
  67. actions.append(('close', _('Close threads')))
  68. if acl['can_delete_threads']:
  69. actions.append(('undelete', _('Undelete threads')))
  70. actions.append(('soft', _('Soft delete threads')))
  71. if acl['can_delete_threads'] == 2:
  72. actions.append(('hard', _('Hard delete threads')))
  73. except KeyError:
  74. pass
  75. return actions
  76. def make_form(self):
  77. self.form = None
  78. list_choices = self.get_thread_actions();
  79. if (not self.request.user.is_authenticated()
  80. or not list_choices):
  81. return
  82. form_fields = {}
  83. form_fields['list_action'] = forms.ChoiceField(choices=list_choices)
  84. list_choices = []
  85. for item in self.threads:
  86. if item.forum_id == self.forum.pk:
  87. list_choices.append((item.pk, None))
  88. if not list_choices:
  89. return
  90. form_fields['list_items'] = forms.MultipleChoiceField(choices=list_choices, widget=forms.CheckboxSelectMultiple)
  91. self.form = type('ThreadsViewForm', (Form,), form_fields)
  92. def handle_form(self):
  93. if self.request.method == 'POST':
  94. self.form = self.form(self.request.POST, request=self.request)
  95. if self.form.is_valid():
  96. checked_items = []
  97. posts = []
  98. for thread in self.threads:
  99. if str(thread.pk) in self.form.cleaned_data['list_items'] and thread.forum_id == self.forum.pk:
  100. posts.append(thread.start_post_id)
  101. if thread.start_post_id != thread.last_post_id:
  102. posts.append(thread.last_post_id)
  103. checked_items.append(thread.pk)
  104. if checked_items:
  105. if posts:
  106. for post in Post.objects.filter(id__in=posts).prefetch_related('user'):
  107. for thread in self.threads:
  108. if thread.start_post_id == post.pk:
  109. thread.start_post = post
  110. if thread.last_post_id == post.pk:
  111. thread.last_post = post
  112. if thread.start_post_id == post.pk or thread.last_post_id == post.pk:
  113. break
  114. form_action = getattr(self, 'action_' + self.form.cleaned_data['list_action'])
  115. try:
  116. response = form_action(checked_items)
  117. if response:
  118. return response
  119. return redirect(self.request.path)
  120. except forms.ValidationError as e:
  121. self.message = Message(e.messages[0], 'error')
  122. else:
  123. self.message = Message(_("You have to select at least one thread."), 'error')
  124. else:
  125. if 'list_action' in self.form.errors:
  126. self.message = Message(_("Action requested is incorrect."), 'error')
  127. else:
  128. self.message = Message(form.non_field_errors()[0], 'error')
  129. else:
  130. self.form = self.form(request=self.request)
  131. def action_accept(self, ids):
  132. accepted = 0
  133. last_posts = []
  134. users = []
  135. for thread in self.threads:
  136. if thread.pk in ids and thread.moderated:
  137. accepted += 1
  138. # Sync thread and post
  139. thread.moderated = False
  140. thread.replies_moderated -= 1
  141. thread.save(force_update=True)
  142. thread.start_post.moderated = False
  143. thread.start_post.save(force_update=True)
  144. thread.last_post.set_checkpoint(self.request, 'accepted')
  145. last_posts.append(thread.last_post.pk)
  146. # Sync user
  147. if thread.last_post.user:
  148. thread.start_post.user.threads += 1
  149. thread.start_post.user.posts += 1
  150. users.append(thread.start_post.user)
  151. if accepted:
  152. Post.objects.filter(id__in=last_posts).update(checkpoints=True)
  153. self.request.monitor['threads'] = int(self.request.monitor['threads']) + accepted
  154. self.request.monitor['posts'] = int(self.request.monitor['posts']) + accepted
  155. self.forum.threads_delta += 1
  156. self.forum.posts_delta += self.thread.replies + 1
  157. self.forum.sync()
  158. self.forum.save(force_update=True)
  159. for user in users:
  160. user.save(force_update=True)
  161. self.request.messages.set_flash(Message(_('Selected threads have been marked as reviewed and made visible to other members.')), 'success', 'threads')
  162. def action_annouce(self, ids):
  163. acl = self.request.acl.threads.get_role(self.forum)
  164. annouced = []
  165. for thread in self.threads:
  166. if thread.pk in ids and thread.weight < 2:
  167. annouced.append(thread.pk)
  168. if annouced:
  169. Thread.objects.filter(id__in=annouced).update(weight=2)
  170. self.request.messages.set_flash(Message(_('Selected threads have been turned into annoucements.')), 'success', 'threads')
  171. def action_sticky(self, ids):
  172. sticky = []
  173. for thread in self.threads:
  174. if thread.pk in ids and thread.weight != 1 and (acl['can_pin_threads'] == 2 or thread.weight < 2):
  175. sticky.append(thread.pk)
  176. if sticky:
  177. Thread.objects.filter(id__in=sticky).update(weight=1)
  178. self.request.messages.set_flash(Message(_('Selected threads have been sticked to the top of list.')), 'success', 'threads')
  179. def action_normal(self, ids):
  180. normalised = []
  181. for thread in self.threads:
  182. if thread.pk in ids and thread.weight > 0:
  183. normalised.append(thread.pk)
  184. if normalised:
  185. Thread.objects.filter(id__in=normalised).update(weight=0)
  186. self.request.messages.set_flash(Message(_('Selected threads weight has been removed.')), 'success', 'threads')
  187. def action_move(self, ids):
  188. threads = []
  189. for thread in self.threads:
  190. if thread.pk in ids:
  191. threads.append(thread)
  192. if self.request.POST.get('origin') == 'move_form':
  193. form = MoveThreadsForm(self.request.POST, request=self.request, forum=self.forum)
  194. if form.is_valid():
  195. new_forum = form.cleaned_data['new_forum']
  196. for thread in threads:
  197. thread.move_to(new_forum)
  198. thread.save(force_update=True)
  199. new_forum.sync()
  200. new_forum.save(force_update=True)
  201. self.forum.sync()
  202. self.forum.save(force_update=True)
  203. self.request.messages.set_flash(Message(_('Selected threads have been moved to "%(forum)s".') % {'forum': new_forum.name}), 'success', 'threads')
  204. return None
  205. self.message = Message(form.non_field_errors()[0], 'error')
  206. else:
  207. form = MoveThreadsForm(request=self.request, forum=self.forum)
  208. return self.request.theme.render_to_response('threads/move.html',
  209. {
  210. 'message': self.message,
  211. 'forum': self.forum,
  212. 'parents': self.parents,
  213. 'threads': threads,
  214. 'form': FormLayout(form),
  215. },
  216. context_instance=RequestContext(self.request));
  217. def action_merge(self, ids):
  218. if len(ids) < 2:
  219. raise ValidationError(_("You have to pick two or more threads to merge."))
  220. threads = []
  221. for thread in self.threads:
  222. if thread.pk in ids:
  223. threads.append(thread)
  224. if self.request.POST.get('origin') == 'merge_form':
  225. form = MergeThreadsForm(self.request.POST, request=self.request, threads=threads)
  226. if form.is_valid():
  227. new_thread = Thread.objects.create(
  228. forum=self.forum,
  229. name=form.cleaned_data['thread_name'],
  230. slug=slugify(form.cleaned_data['thread_name']),
  231. start=timezone.now(),
  232. last=timezone.now()
  233. )
  234. last_merge = 0
  235. last_thread = None
  236. merged = []
  237. for i in range(0, len(threads)):
  238. thread = form.merge_order[i]
  239. merged.append(thread.pk)
  240. if last_thread and last_thread.last > thread.start:
  241. last_merge += thread.merges + 1
  242. thread.merge_with(new_thread, last_merge=last_merge)
  243. last_thread = thread
  244. Thread.objects.filter(id__in=merged).delete()
  245. new_thread.sync()
  246. new_thread.save(force_update=True)
  247. self.forum.sync()
  248. self.forum.save(force_update=True)
  249. self.request.messages.set_flash(Message(_('Selected threads have been merged into new one.')), 'success', 'threads')
  250. return None
  251. self.message = Message(form.non_field_errors()[0], 'error')
  252. else:
  253. form = MergeThreadsForm(request=self.request, threads=threads)
  254. return self.request.theme.render_to_response('threads/merge.html',
  255. {
  256. 'message': self.message,
  257. 'forum': self.forum,
  258. 'parents': self.parents,
  259. 'threads': threads,
  260. 'form': FormLayout(form),
  261. },
  262. context_instance=RequestContext(self.request));
  263. def action_open(self, ids):
  264. opened = []
  265. last_posts = []
  266. for thread in self.threads:
  267. if thread.pk in ids and thread.closed:
  268. opened.append(thread.pk)
  269. thread.last_post.set_checkpoint(self.request, 'opened')
  270. last_posts.append(thread.last_post.pk)
  271. if opened:
  272. Post.objects.filter(id__in=last_posts).update(checkpoints=True)
  273. Thread.objects.filter(id__in=opened).update(closed=False)
  274. self.request.messages.set_flash(Message(_('Selected threads have been opened.')), 'success', 'threads')
  275. def action_close(self, ids):
  276. closed = []
  277. last_posts = []
  278. for thread in self.threads:
  279. if thread.pk in ids and not thread.closed:
  280. closed.append(thread.pk)
  281. thread.last_post.set_checkpoint(self.request, 'closed')
  282. last_posts.append(thread.last_post.pk)
  283. if closed:
  284. Post.objects.filter(id__in=last_posts).update(checkpoints=True)
  285. Thread.objects.filter(id__in=closed).update(closed=True)
  286. self.request.messages.set_flash(Message(_('Selected threads have been closed.')), 'success', 'threads')
  287. def action_undelete(self, ids):
  288. undeleted = []
  289. last_posts = []
  290. posts = 0
  291. for thread in self.threads:
  292. if thread.pk in ids and thread.deleted:
  293. undeleted.append(thread.pk)
  294. posts += thread.replies + 1
  295. thread.start_post.deleted = False
  296. thread.start_post.save(force_update=True)
  297. thread.last_post.set_checkpoint(self.request, 'undeleted')
  298. if undeleted:
  299. self.request.monitor['threads'] = int(self.request.monitor['threads']) + len(undeleted)
  300. self.request.monitor['posts'] = int(self.request.monitor['posts']) + posts
  301. self.forum.sync()
  302. self.forum.save(force_update=True)
  303. Post.objects.filter(id__in=last_posts).update(checkpoints=True)
  304. Thread.objects.filter(id__in=undeleted).update(deleted=False)
  305. self.request.messages.set_flash(Message(_('Selected threads have been undeleted.')), 'success', 'threads')
  306. def action_soft(self, ids):
  307. deleted = []
  308. last_posts = []
  309. posts = 0
  310. for thread in self.threads:
  311. if thread.pk in ids and not thread.deleted:
  312. deleted.append(thread.pk)
  313. posts += thread.replies + 1
  314. thread.start_post.deleted = True
  315. thread.start_post.save(force_update=True)
  316. thread.last_post.set_checkpoint(self.request, 'deleted')
  317. last_posts.append(thread.last_post.pk)
  318. if deleted:
  319. self.request.monitor['threads'] = int(self.request.monitor['threads']) - len(deleted)
  320. self.request.monitor['posts'] = int(self.request.monitor['posts']) - posts
  321. self.forum.sync()
  322. self.forum.save(force_update=True)
  323. Post.objects.filter(id__in=last_posts).update(checkpoints=True)
  324. Thread.objects.filter(id__in=deleted).update(deleted=True)
  325. self.request.messages.set_flash(Message(_('Selected threads have been softly deleted.')), 'success', 'threads')
  326. def action_hard(self, ids):
  327. deleted = []
  328. posts = 0
  329. for thread in self.threads:
  330. if thread.pk in ids:
  331. deleted.append(thread.pk)
  332. posts += thread.replies + 1
  333. thread.delete()
  334. if deleted:
  335. self.request.monitor['threads'] = int(self.request.monitor['threads']) - len(deleted)
  336. self.request.monitor['posts'] = int(self.request.monitor['posts']) - posts
  337. self.forum.sync()
  338. self.forum.save(force_update=True)
  339. self.request.messages.set_flash(Message(_('Selected threads have been deleted.')), 'success', 'threads')
  340. def __call__(self, request, slug=None, forum=None, page=0):
  341. self.request = request
  342. self.pagination = None
  343. self.parents = None
  344. self.message = request.messages.get_message('threads')
  345. try:
  346. self.fetch_forum(forum)
  347. self.fetch_threads(page)
  348. self.make_form()
  349. if self.form:
  350. response = self.handle_form()
  351. if response:
  352. return response
  353. except Forum.DoesNotExist:
  354. return error404(request)
  355. except ACLError403 as e:
  356. return error403(request, e.message)
  357. except ACLError404 as e:
  358. return error404(request, e.message)
  359. # Merge proxy into forum
  360. self.forum.closed = self.proxy.closed
  361. return request.theme.render_to_response('threads/list.html',
  362. {
  363. 'message': self.message,
  364. 'forum': self.forum,
  365. 'parents': self.parents,
  366. 'count': self.count,
  367. 'list_form': FormFields(self.form).fields if self.form else None,
  368. 'threads': self.threads,
  369. 'pagination': self.pagination,
  370. },
  371. context_instance=RequestContext(request));