thread.py 21 KB

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