thread.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. from django.core.urlresolvers import reverse
  2. from django import forms
  3. from django.forms import ValidationError
  4. from django.shortcuts import redirect
  5. from django.template import RequestContext
  6. from django.utils.translation import ugettext as _
  7. from misago.acl.utils import ACLError403, ACLError404
  8. from misago.forms import Form, FormLayout, FormFields
  9. from misago.forums.models import Forum
  10. from misago.messages import Message
  11. from misago.readstracker.trackers import ThreadsTracker
  12. from misago.threads.forms import MoveThreadsForm, QuickReplyForm
  13. from misago.threads.models import Thread, Post
  14. from misago.threads.views.base import BaseView
  15. from misago.views import error403, error404
  16. from misago.utils import make_pagination
  17. class ThreadView(BaseView):
  18. def fetch_thread(self, thread):
  19. self.thread = Thread.objects.get(pk=thread)
  20. self.forum = self.thread.forum
  21. self.proxy = Forum.objects.parents_aware_forum(self.forum)
  22. self.request.acl.forums.allow_forum_view(self.forum)
  23. self.request.acl.threads.allow_thread_view(self.request.user, self.thread)
  24. self.parents = Forum.objects.forum_parents(self.forum.pk, True)
  25. self.tracker = ThreadsTracker(self.request.user, self.forum)
  26. def fetch_posts(self, page):
  27. self.count = self.request.acl.threads.filter_posts(self.request, self.thread, Post.objects.filter(thread=self.thread)).count()
  28. self.posts = self.request.acl.threads.filter_posts(self.request, self.thread, Post.objects.filter(thread=self.thread)).prefetch_related('checkpoint_set', 'user', 'user__rank')
  29. if self.thread.merges > 0:
  30. self.posts = self.posts.order_by('merge', 'pk')
  31. else:
  32. self.posts = self.posts.order_by('pk')
  33. self.pagination = make_pagination(page, self.count, self.request.settings.posts_per_page)
  34. if self.request.settings.posts_per_page < self.count:
  35. self.posts = self.posts[self.pagination['start']:self.pagination['stop']]
  36. self.read_date = self.tracker.get_read_date(self.thread)
  37. for post in self.posts:
  38. post.message = self.request.messages.get_message('threads_%s' % post.pk)
  39. post.is_read = post.date <= self.read_date
  40. last_post = self.posts[len(self.posts) - 1]
  41. if not self.tracker.is_read(self.thread):
  42. self.tracker.set_read(self.thread, last_post)
  43. self.tracker.sync()
  44. def get_post_actions(self):
  45. acl = self.request.acl.threads.get_role(self.thread.forum_id)
  46. actions = []
  47. try:
  48. if acl['can_approve'] and self.thread.replies_moderated > 0:
  49. actions.append(('accept', _('Accept posts')))
  50. if acl['can_move_threads_posts']:
  51. actions.append(('merge', _('Merge posts into one')))
  52. actions.append(('split', _('Split posts to new thread')))
  53. actions.append(('move', _('Move posts to other thread')))
  54. if acl['can_protect_posts']:
  55. actions.append(('protect', _('Protect posts')))
  56. actions.append(('unprotect', _('Remove posts protection')))
  57. if acl['can_delete_posts']:
  58. if self.thread.replies_deleted > 0:
  59. actions.append(('undelete', _('Undelete posts')))
  60. actions.append(('soft', _('Soft delete posts')))
  61. if acl['can_delete_posts'] == 2:
  62. actions.append(('hard', _('Hard delete posts')))
  63. except KeyError:
  64. pass
  65. return actions
  66. def make_posts_form(self):
  67. self.posts_form = None
  68. list_choices = self.get_post_actions();
  69. if (not self.request.user.is_authenticated()
  70. or not list_choices):
  71. return
  72. form_fields = {}
  73. form_fields['list_action'] = forms.ChoiceField(choices=list_choices)
  74. list_choices = []
  75. for item in self.posts:
  76. list_choices.append((item.pk, None))
  77. if not list_choices:
  78. return
  79. form_fields['list_items'] = forms.MultipleChoiceField(choices=list_choices,widget=forms.CheckboxSelectMultiple)
  80. self.posts_form = type('PostsViewForm', (Form,), form_fields)
  81. def handle_posts_form(self):
  82. if self.request.method == 'POST' and self.request.POST.get('origin') == 'posts_form':
  83. self.posts_form = self.posts_form(self.request.POST, request=self.request)
  84. if self.posts_form.is_valid():
  85. checked_items = []
  86. for post in self.posts:
  87. if str(post.pk) in self.posts_form.cleaned_data['list_items']:
  88. checked_items.append(post.pk)
  89. if checked_items:
  90. form_action = getattr(self, 'post_action_' + self.posts_form.cleaned_data['list_action'])
  91. try:
  92. response = form_action(checked_items)
  93. if response:
  94. return response
  95. return redirect(self.request.path)
  96. except forms.ValidationError as e:
  97. self.message = Message(e.messages[0], 'error')
  98. else:
  99. self.message = Message(_("You have to select at least one post."), 'error')
  100. else:
  101. if 'list_action' in self.posts_form.errors:
  102. self.message = Message(_("Action requested is incorrect."), 'error')
  103. else:
  104. self.message = Message(posts_form.non_field_errors()[0], 'error')
  105. else:
  106. self.posts_form = self.posts_form(request=self.request)
  107. def get_thread_actions(self):
  108. acl = self.request.acl.threads.get_role(self.thread.forum_id)
  109. actions = []
  110. try:
  111. if acl['can_approve'] and self.thread.moderated:
  112. actions.append(('accept', _('Accept this thread')))
  113. if acl['can_pin_threads'] == 2 and self.thread.weight < 2:
  114. actions.append(('annouce', _('Change this thread to annoucement')))
  115. if acl['can_pin_threads'] > 0 and self.thread.weight != 1:
  116. actions.append(('sticky', _('Change this thread to sticky')))
  117. if acl['can_pin_threads'] > 0:
  118. if self.thread.weight == 2:
  119. actions.append(('normal', _('Change this thread to normal')))
  120. if self.thread.weight == 1:
  121. actions.append(('normal', _('Unpin this thread')))
  122. if acl['can_move_threads_posts']:
  123. actions.append(('move', _('Move this thread')))
  124. if acl['can_close_threads']:
  125. if self.thread.closed:
  126. actions.append(('open', _('Open this thread')))
  127. else:
  128. actions.append(('close', _('Close this thread')))
  129. if acl['can_delete_threads']:
  130. if self.thread.deleted:
  131. actions.append(('undelete', _('Undelete this thread')))
  132. else:
  133. actions.append(('soft', _('Soft delete this thread')))
  134. if acl['can_delete_threads'] == 2:
  135. actions.append(('hard', _('Hard delete this thread')))
  136. except KeyError:
  137. pass
  138. return actions
  139. def make_thread_form(self):
  140. self.thread_form = None
  141. list_choices = self.get_thread_actions();
  142. if (not self.request.user.is_authenticated()
  143. or not list_choices):
  144. return
  145. form_fields = {'thread_action': forms.ChoiceField(choices=list_choices)}
  146. self.thread_form = type('ThreadViewForm', (Form,), form_fields)
  147. def handle_thread_form(self):
  148. if self.request.method == 'POST' and self.request.POST.get('origin') == 'thread_form':
  149. self.thread_form = self.thread_form(self.request.POST, request=self.request)
  150. if self.thread_form.is_valid():
  151. form_action = getattr(self, 'thread_action_' + self.thread_form.cleaned_data['thread_action'])
  152. try:
  153. response = form_action()
  154. if response:
  155. return response
  156. return redirect(self.request.path)
  157. except forms.ValidationError as e:
  158. self.message = Message(e.messages[0], 'error')
  159. else:
  160. if 'thread_action' in self.thread_form.errors:
  161. self.message = Message(_("Action requested is incorrect."), 'error')
  162. else:
  163. self.message = Message(form.non_field_errors()[0], 'error')
  164. else:
  165. self.thread_form = self.thread_form(request=self.request)
  166. def thread_action_accept(self):
  167. # Sync thread and post
  168. self.thread.moderated = False
  169. self.thread.replies_moderated -= 1
  170. self.thread.save(force_update=True)
  171. self.thread.start_post.moderated = False
  172. self.thread.start_post.save(force_update=True)
  173. self.thread.last_post.set_checkpoint(self.request, 'accepted')
  174. # Sync user
  175. if self.thread.last_post.user:
  176. self.thread.start_post.user.threads += 1
  177. self.thread.start_post.user.posts += 1
  178. self.thread.start_post.user.save(force_update=True)
  179. # Sync forum
  180. self.forum.threads_delta += 1
  181. self.forum.posts_delta += self.thread.replies + 1
  182. self.forum.sync()
  183. self.forum.save(force_update=True)
  184. # Update monitor
  185. self.request.monitor['threads'] = int(self.request.monitor['threads']) + 1
  186. self.request.monitor['posts'] = int(self.request.monitor['posts']) + self.thread.replies + 1
  187. self.request.messages.set_flash(Message(_('Thread has been marked as reviewed and made visible to other members.')), 'success', 'threads')
  188. def thread_action_annouce(self):
  189. self.thread.weight = 2
  190. self.thread.save(force_update=True)
  191. self.request.messages.set_flash(Message(_('Thread has been turned into annoucement.')), 'success', 'threads')
  192. def thread_action_sticky(self):
  193. self.thread.weight = 1
  194. self.thread.save(force_update=True)
  195. self.request.messages.set_flash(Message(_('Thread has been turned into sticky.')), 'success', 'threads')
  196. def thread_action_normal(self):
  197. self.thread.weight = 0
  198. self.thread.save(force_update=True)
  199. self.request.messages.set_flash(Message(_('Thread weight has been changed to normal.')), 'success', 'threads')
  200. def thread_action_move(self):
  201. message = None
  202. if self.request.POST.get('do') == 'move':
  203. form = MoveThreadsForm(self.request.POST,request=self.request,forum=self.forum)
  204. if form.is_valid():
  205. new_forum = form.cleaned_data['new_forum']
  206. self.thread.forum = new_forum
  207. self.thread.post_set.update(forum=new_forum)
  208. self.thread.change_set.update(forum=new_forum)
  209. self.thread.checkpoint_set.update(forum=new_forum)
  210. self.thread.save(force_update=True)
  211. self.forum.sync()
  212. self.forum.save(force_update=True)
  213. self.request.messages.set_flash(Message(_('Thread has been moved to "%(forum)s".') % {'forum': new_forum.name}), 'success', 'threads')
  214. return None
  215. message = Message(form.non_field_errors()[0], 'error')
  216. else:
  217. form = MoveThreadsForm(request=self.request,forum=self.forum)
  218. return self.request.theme.render_to_response('threads/move.html',
  219. {
  220. 'message': message,
  221. 'forum': self.forum,
  222. 'parents': self.parents,
  223. 'thread': self.thread,
  224. 'form': FormLayout(form),
  225. },
  226. context_instance=RequestContext(self.request));
  227. def thread_action_open(self):
  228. self.thread.closed = False
  229. self.thread.save(force_update=True)
  230. self.thread.last_post.set_checkpoint(self.request, 'opened')
  231. self.request.messages.set_flash(Message(_('Thread has been opened.')), 'success', 'threads')
  232. def thread_action_close(self):
  233. self.thread.closed = True
  234. self.thread.save(force_update=True)
  235. self.thread.last_post.set_checkpoint(self.request, 'closed')
  236. self.request.messages.set_flash(Message(_('Thread has been closed.')), 'success', 'threads')
  237. def thread_action_undelete(self):
  238. # Update thread
  239. self.thread.deleted = False
  240. self.thread.replies_deleted -= 1
  241. self.thread.save(force_update=True)
  242. # Update first post in thread
  243. self.thread.start_post.deleted = False
  244. self.thread.start_post.save(force_update=True)
  245. # Set checkpoint
  246. self.thread.last_post.set_checkpoint(self.request, 'undeleted')
  247. # Update forum
  248. self.forum.sync()
  249. self.forum.save(force_update=True)
  250. # Update monitor
  251. self.request.monitor['threads'] = int(self.request.monitor['threads']) + 1
  252. self.request.monitor['posts'] = int(self.request.monitor['posts']) + self.thread.replies + 1
  253. self.request.messages.set_flash(Message(_('Thread has been undeleted.')), 'success', 'threads')
  254. def thread_action_soft(self):
  255. # Update thread
  256. self.thread.deleted = True
  257. self.thread.replies_deleted += 1
  258. self.thread.save(force_update=True)
  259. # Update first post in thread
  260. self.thread.start_post.deleted = True
  261. self.thread.start_post.save(force_update=True)
  262. # Set checkpoint
  263. self.thread.last_post.set_checkpoint(self.request, 'deleted')
  264. # Update forum
  265. self.forum.sync()
  266. self.forum.save(force_update=True)
  267. # Update monitor
  268. self.request.monitor['threads'] = int(self.request.monitor['threads']) - 1
  269. self.request.monitor['posts'] = int(self.request.monitor['posts']) - self.thread.replies - 1
  270. self.request.messages.set_flash(Message(_('Thread has been deleted.')), 'success', 'threads')
  271. def thread_action_hard(self):
  272. # Delete thread
  273. self.thread.delete()
  274. # Update forum
  275. self.forum.sync()
  276. self.forum.save(force_update=True)
  277. # Update monitor
  278. self.request.monitor['threads'] = int(self.request.monitor['threads']) - 1
  279. self.request.monitor['posts'] = int(self.request.monitor['posts']) - self.thread.replies - 1
  280. self.request.messages.set_flash(Message(_('Thread "%(thread)s" has been deleted.') % {'thread': self.thread.name}), 'success', 'threads')
  281. return redirect(reverse('forum', kwargs={'forum': self.forum.pk, 'slug': self.forum.slug}))
  282. def __call__(self, request, slug=None, thread=None, page=0):
  283. self.request = request
  284. self.pagination = None
  285. self.parents = None
  286. try:
  287. self.fetch_thread(thread)
  288. self.fetch_posts(page)
  289. self.make_thread_form()
  290. if self.thread_form:
  291. response = self.handle_thread_form()
  292. if response:
  293. return response
  294. self.make_posts_form()
  295. if self.posts_form:
  296. response = self.handle_posts_form()
  297. if response:
  298. return response
  299. except Thread.DoesNotExist:
  300. return error404(self.request)
  301. except ACLError403 as e:
  302. return error403(request, e.message)
  303. except ACLError404 as e:
  304. return error404(request, e.message)
  305. # Merge proxy into forum
  306. self.forum.closed = self.proxy.closed
  307. return request.theme.render_to_response('threads/thread.html',
  308. {
  309. 'message': request.messages.get_message('threads'),
  310. 'forum': self.forum,
  311. 'parents': self.parents,
  312. 'thread': self.thread,
  313. 'is_read': self.tracker.is_read(self.thread),
  314. 'count': self.count,
  315. 'posts': self.posts,
  316. 'pagination': self.pagination,
  317. 'quick_reply': FormFields(QuickReplyForm(request=request)).fields,
  318. 'thread_form': FormFields(self.thread_form).fields if self.thread_form else None,
  319. 'posts_form': FormFields(self.posts_form).fields if self.posts_form else None,
  320. },
  321. context_instance=RequestContext(request));