thread.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570
  1. from django.core.urlresolvers import reverse
  2. from django import forms
  3. from django.db.models import F
  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.markdown import post_markdown
  13. from misago.messages import Message
  14. from misago.readstracker.trackers import ThreadsTracker
  15. from misago.threads.forms import MoveThreadsForm, SplitThreadForm, MovePostsForm, QuickReplyForm
  16. from misago.threads.models import Thread, Post, Karma, Change, Checkpoint
  17. from misago.threads.views.base import BaseView
  18. from misago.views import error403, error404
  19. from misago.utils import make_pagination, slugify
  20. from misago.watcher.models import ThreadWatch
  21. class ThreadView(BaseView):
  22. def fetch_thread(self, thread):
  23. self.thread = Thread.objects.get(pk=thread)
  24. self.forum = self.thread.forum
  25. self.proxy = Forum.objects.parents_aware_forum(self.forum)
  26. self.request.acl.forums.allow_forum_view(self.forum)
  27. self.request.acl.threads.allow_thread_view(self.request.user, self.thread)
  28. self.parents = Forum.objects.forum_parents(self.forum.pk, True)
  29. self.tracker = ThreadsTracker(self.request, self.forum)
  30. if self.request.user.is_authenticated():
  31. try:
  32. self.watcher = ThreadWatch.objects.get(user=self.request.user, thread=self.thread)
  33. except ThreadWatch.DoesNotExist:
  34. pass
  35. def fetch_posts(self, page):
  36. self.count = self.request.acl.threads.filter_posts(self.request, self.thread, Post.objects.filter(thread=self.thread)).count()
  37. 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')
  38. if self.thread.merges > 0:
  39. self.posts = self.posts.order_by('merge', 'pk')
  40. else:
  41. self.posts = self.posts.order_by('pk')
  42. self.pagination = make_pagination(page, self.count, self.request.settings.posts_per_page)
  43. if self.request.settings.posts_per_page < self.count:
  44. self.posts = self.posts[self.pagination['start']:self.pagination['stop']]
  45. self.read_date = self.tracker.get_read_date(self.thread)
  46. ignored_users = []
  47. if self.request.user.is_authenticated():
  48. ignored_users = self.request.user.ignored_users()
  49. posts_dict = {}
  50. for post in self.posts:
  51. posts_dict[post.pk] = post
  52. post.message = self.request.messages.get_message('threads_%s' % post.pk)
  53. post.is_read = post.date <= self.read_date or (post.pk != self.thread.start_post_id and (post.deleted or post.moderated))
  54. post.karma_vote = None
  55. post.ignored = self.thread.start_post_id != post.pk and not self.thread.pk in self.request.session.get('unignore_threads', []) and post.user_id in ignored_users
  56. if post.ignored:
  57. self.ignored = True
  58. last_post = self.posts[len(self.posts) - 1]
  59. if not self.tracker.is_read(self.thread):
  60. self.tracker.set_read(self.thread, last_post)
  61. self.tracker.sync()
  62. if self.watcher and last_post.date > self.watcher.last_read:
  63. self.watcher.last_read = timezone.now()
  64. self.watcher.save(force_update=True)
  65. if self.request.user.is_authenticated():
  66. for karma in Karma.objects.filter(post_id__in=posts_dict.keys()).filter(user=self.request.user):
  67. posts_dict[karma.post_id].karma_vote = karma
  68. def get_post_actions(self):
  69. acl = self.request.acl.threads.get_role(self.thread.forum_id)
  70. actions = []
  71. try:
  72. if acl['can_approve'] and self.thread.replies_moderated > 0:
  73. actions.append(('accept', _('Accept posts')))
  74. if acl['can_move_threads_posts']:
  75. actions.append(('merge', _('Merge posts into one')))
  76. actions.append(('split', _('Split posts to new thread')))
  77. actions.append(('move', _('Move posts to other thread')))
  78. if acl['can_protect_posts']:
  79. actions.append(('protect', _('Protect posts')))
  80. actions.append(('unprotect', _('Remove posts protection')))
  81. if acl['can_delete_posts']:
  82. if self.thread.replies_deleted > 0:
  83. actions.append(('undelete', _('Undelete posts')))
  84. actions.append(('soft', _('Soft delete posts')))
  85. if acl['can_delete_posts'] == 2:
  86. actions.append(('hard', _('Hard delete posts')))
  87. except KeyError:
  88. pass
  89. return actions
  90. def make_posts_form(self):
  91. self.posts_form = None
  92. list_choices = self.get_post_actions();
  93. if (not self.request.user.is_authenticated()
  94. or not list_choices):
  95. return
  96. form_fields = {}
  97. form_fields['list_action'] = forms.ChoiceField(choices=list_choices)
  98. list_choices = []
  99. for item in self.posts:
  100. list_choices.append((item.pk, None))
  101. if not list_choices:
  102. return
  103. form_fields['list_items'] = forms.MultipleChoiceField(choices=list_choices, widget=forms.CheckboxSelectMultiple)
  104. self.posts_form = type('PostsViewForm', (Form,), form_fields)
  105. def handle_posts_form(self):
  106. if self.request.method == 'POST' and self.request.POST.get('origin') == 'posts_form':
  107. self.posts_form = self.posts_form(self.request.POST, request=self.request)
  108. if self.posts_form.is_valid():
  109. checked_items = []
  110. for post in self.posts:
  111. if str(post.pk) in self.posts_form.cleaned_data['list_items']:
  112. checked_items.append(post.pk)
  113. if checked_items:
  114. form_action = getattr(self, 'post_action_' + self.posts_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 post."), 'error')
  124. else:
  125. if 'list_action' in self.posts_form.errors:
  126. self.message = Message(_("Action requested is incorrect."), 'error')
  127. else:
  128. self.message = Message(posts_form.non_field_errors()[0], 'error')
  129. else:
  130. self.posts_form = self.posts_form(request=self.request)
  131. def post_action_accept(self, ids):
  132. accepted = 0
  133. for post in self.posts:
  134. if post.pk in ids and post.moderated:
  135. accepted += 1
  136. if accepted:
  137. self.thread.post_set.filter(id__in=ids).update(moderated=False)
  138. self.thread.sync()
  139. self.thread.save(force_update=True)
  140. self.request.messages.set_flash(Message(_('Selected posts have been accepted and made visible to other members.')), 'success', 'threads')
  141. def post_action_merge(self, ids):
  142. users = []
  143. posts = []
  144. for post in self.posts:
  145. if post.pk in ids:
  146. posts.append(post)
  147. if not post.user_id in users:
  148. users.append(post.user_id)
  149. if len(users) > 1:
  150. raise forms.ValidationError(_("You cannot merge replies made by different members!"))
  151. if len(posts) < 2:
  152. raise forms.ValidationError(_("You have to select two or more posts you want to merge."))
  153. new_post = posts[0]
  154. for post in posts[1:]:
  155. post.merge_with(new_post)
  156. post.delete()
  157. md, new_post.post_preparsed = post_markdown(self.request, new_post.post)
  158. new_post.save(force_update=True)
  159. self.thread.sync()
  160. self.thread.save(force_update=True)
  161. self.forum.sync()
  162. self.forum.save(force_update=True)
  163. self.request.messages.set_flash(Message(_('Selected posts have been merged into one message.')), 'success', 'threads')
  164. def post_action_split(self, ids):
  165. for id in ids:
  166. if id == self.thread.start_post_id:
  167. raise forms.ValidationError(_("You cannot split first post from thread."))
  168. message = None
  169. if self.request.POST.get('do') == 'split':
  170. form = SplitThreadForm(self.request.POST, request=self.request)
  171. if form.is_valid():
  172. new_thread = Thread()
  173. new_thread.forum = form.cleaned_data['thread_forum']
  174. new_thread.name = form.cleaned_data['thread_name']
  175. new_thread.slug = slugify(form.cleaned_data['thread_name'])
  176. new_thread.start = timezone.now()
  177. new_thread.last = timezone.now()
  178. new_thread.start_poster_name = 'n'
  179. new_thread.start_poster_slug = 'n'
  180. new_thread.last_poster_name = 'n'
  181. new_thread.last_poster_slug = 'n'
  182. new_thread.save(force_insert=True)
  183. prev_merge = -1
  184. merge = -1
  185. for post in self.posts:
  186. if post.pk in ids:
  187. if prev_merge != post.merge:
  188. prev_merge = post.merge
  189. merge += 1
  190. post.merge = merge
  191. post.move_to(new_thread)
  192. post.save(force_update=True)
  193. new_thread.sync()
  194. new_thread.save(force_update=True)
  195. self.thread.sync()
  196. self.thread.save(force_update=True)
  197. self.forum.sync()
  198. self.forum.save(force_update=True)
  199. if new_thread.forum != self.forum:
  200. new_thread.forum.sync()
  201. new_thread.forum.save(force_update=True)
  202. self.request.messages.set_flash(Message(_("Selected posts have been split to new thread.")), 'success', 'threads')
  203. return redirect(reverse('thread', kwargs={'thread': new_thread.pk, 'slug': new_thread.slug}))
  204. message = Message(form.non_field_errors()[0], 'error')
  205. else:
  206. form = SplitThreadForm(request=self.request, initial={
  207. 'thread_name': _('[Split] %s') % self.thread.name,
  208. 'thread_forum': self.forum,
  209. })
  210. return self.request.theme.render_to_response('threads/split.html',
  211. {
  212. 'message': message,
  213. 'forum': self.forum,
  214. 'parents': self.parents,
  215. 'thread': self.thread,
  216. 'posts': ids,
  217. 'form': FormLayout(form),
  218. },
  219. context_instance=RequestContext(self.request));
  220. def post_action_move(self, ids):
  221. message = None
  222. if self.request.POST.get('do') == 'move':
  223. form = MovePostsForm(self.request.POST, request=self.request, thread=self.thread)
  224. if form.is_valid():
  225. thread = form.cleaned_data['thread_url']
  226. prev_merge = -1
  227. merge = -1
  228. for post in self.posts:
  229. if post.pk in ids:
  230. if prev_merge != post.merge:
  231. prev_merge = post.merge
  232. merge += 1
  233. post.merge = merge + thread.merges
  234. post.move_to(thread)
  235. post.save(force_update=True)
  236. if self.thread.post_set.count() == 0:
  237. self.thread.delete()
  238. else:
  239. self.thread.sync()
  240. self.thread.save(force_update=True)
  241. thread.sync()
  242. thread.save(force_update=True)
  243. thread.forum.sync()
  244. thread.forum.save(force_update=True)
  245. if self.forum.pk != thread.forum.pk:
  246. self.forum.sync()
  247. self.forum.save(force_update=True)
  248. self.request.messages.set_flash(Message(_("Selected posts have been moved to new thread.")), 'success', 'threads')
  249. return redirect(reverse('thread', kwargs={'thread': thread.pk, 'slug': thread.slug}))
  250. message = Message(form.non_field_errors()[0], 'error')
  251. else:
  252. form = MovePostsForm(request=self.request)
  253. return self.request.theme.render_to_response('threads/move.html',
  254. {
  255. 'message': message,
  256. 'forum': self.forum,
  257. 'parents': self.parents,
  258. 'thread': self.thread,
  259. 'posts': ids,
  260. 'form': FormLayout(form),
  261. },
  262. context_instance=RequestContext(self.request));
  263. def post_action_undelete(self, ids):
  264. undeleted = []
  265. for post in self.posts:
  266. if post.pk in ids and post.deleted:
  267. undeleted.append(post.pk)
  268. if undeleted:
  269. self.thread.post_set.filter(id__in=undeleted).update(deleted=False)
  270. self.thread.sync()
  271. self.thread.save(force_update=True)
  272. self.forum.sync()
  273. self.forum.save(force_update=True)
  274. self.request.messages.set_flash(Message(_('Selected posts have been restored.')), 'success', 'threads')
  275. def post_action_protect(self, ids):
  276. protected = 0
  277. for post in self.posts:
  278. if post.pk in ids and not post.protected:
  279. protected += 1
  280. if protected:
  281. self.thread.post_set.filter(id__in=ids).update(protected=True)
  282. self.request.messages.set_flash(Message(_('Selected posts have been protected from edition.')), 'success', 'threads')
  283. def post_action_unprotect(self, ids):
  284. unprotected = 0
  285. for post in self.posts:
  286. if post.pk in ids and post.protected:
  287. unprotected += 1
  288. if unprotected:
  289. self.thread.post_set.filter(id__in=ids).update(protected=False)
  290. self.request.messages.set_flash(Message(_('Protection from editions has been removed from selected posts.')), 'success', 'threads')
  291. def post_action_soft(self, ids):
  292. deleted = []
  293. for post in self.posts:
  294. if post.pk in ids and not post.deleted:
  295. if post.pk == self.thread.start_post_id:
  296. raise forms.ValidationError(_("You cannot delete first post of thread using this action. If you want to delete thread, use thread moderation instead."))
  297. deleted.append(post.pk)
  298. if deleted:
  299. self.thread.post_set.filter(id__in=deleted).update(deleted=True)
  300. self.thread.sync()
  301. self.thread.save(force_update=True)
  302. self.forum.sync()
  303. self.forum.save(force_update=True)
  304. self.request.messages.set_flash(Message(_('Selected posts have been deleted.')), 'success', 'threads')
  305. def post_action_hard(self, ids):
  306. deleted = []
  307. for post in self.posts:
  308. if post.pk in ids and not post.deleted:
  309. if post.pk == self.thread.start_post_id:
  310. raise forms.ValidationError(_("You cannot delete first post of thread using this action. If you want to delete thread, use thread moderation instead."))
  311. deleted.append(post.pk)
  312. if deleted:
  313. for post in deleted:
  314. post.delete()
  315. self.thread.post_set.filter(id__in=deleted).delete()
  316. Change.objects.d(post__in=ids).delete()
  317. Checkpoint.objects.filter(post__in=ids).delete()
  318. self.thread.sync()
  319. self.thread.save(force_update=True)
  320. self.forum.sync()
  321. self.forum.save(force_update=True)
  322. self.request.messages.set_flash(Message(_('Selected posts have been deleted.')), 'success', 'threads')
  323. def get_thread_actions(self):
  324. acl = self.request.acl.threads.get_role(self.thread.forum_id)
  325. actions = []
  326. try:
  327. if acl['can_approve'] and self.thread.moderated:
  328. actions.append(('accept', _('Accept this thread')))
  329. if acl['can_pin_threads'] == 2 and self.thread.weight < 2:
  330. actions.append(('annouce', _('Change this thread to annoucement')))
  331. if acl['can_pin_threads'] > 0 and self.thread.weight != 1:
  332. actions.append(('sticky', _('Change this thread to sticky')))
  333. if acl['can_pin_threads'] > 0:
  334. if self.thread.weight == 2:
  335. actions.append(('normal', _('Change this thread to normal')))
  336. if self.thread.weight == 1:
  337. actions.append(('normal', _('Unpin this thread')))
  338. if acl['can_move_threads_posts']:
  339. actions.append(('move', _('Move this thread')))
  340. if acl['can_close_threads']:
  341. if self.thread.closed:
  342. actions.append(('open', _('Open this thread')))
  343. else:
  344. actions.append(('close', _('Close this thread')))
  345. if acl['can_delete_threads']:
  346. if self.thread.deleted:
  347. actions.append(('undelete', _('Undelete this thread')))
  348. else:
  349. actions.append(('soft', _('Soft delete this thread')))
  350. if acl['can_delete_threads'] == 2:
  351. actions.append(('hard', _('Hard delete this thread')))
  352. except KeyError:
  353. pass
  354. return actions
  355. def make_thread_form(self):
  356. self.thread_form = None
  357. list_choices = self.get_thread_actions();
  358. if (not self.request.user.is_authenticated()
  359. or not list_choices):
  360. return
  361. form_fields = {'thread_action': forms.ChoiceField(choices=list_choices)}
  362. self.thread_form = type('ThreadViewForm', (Form,), form_fields)
  363. def handle_thread_form(self):
  364. if self.request.method == 'POST' and self.request.POST.get('origin') == 'thread_form':
  365. self.thread_form = self.thread_form(self.request.POST, request=self.request)
  366. if self.thread_form.is_valid():
  367. form_action = getattr(self, 'thread_action_' + self.thread_form.cleaned_data['thread_action'])
  368. try:
  369. response = form_action()
  370. if response:
  371. return response
  372. return redirect(self.request.path)
  373. except forms.ValidationError as e:
  374. self.message = Message(e.messages[0], 'error')
  375. else:
  376. if 'thread_action' in self.thread_form.errors:
  377. self.message = Message(_("Action requested is incorrect."), 'error')
  378. else:
  379. self.message = Message(form.non_field_errors()[0], 'error')
  380. else:
  381. self.thread_form = self.thread_form(request=self.request)
  382. def thread_action_accept(self):
  383. # Sync thread and post
  384. self.thread.moderated = False
  385. self.thread.replies_moderated -= 1
  386. self.thread.save(force_update=True)
  387. self.thread.start_post.moderated = False
  388. self.thread.start_post.save(force_update=True)
  389. self.thread.last_post.set_checkpoint(self.request, 'accepted')
  390. # Sync user
  391. if self.thread.last_post.user:
  392. self.thread.start_post.user.threads += 1
  393. self.thread.start_post.user.posts += 1
  394. self.thread.start_post.user.save(force_update=True)
  395. # Sync forum
  396. self.forum.threads_delta += 1
  397. self.forum.posts_delta += self.thread.replies + 1
  398. self.forum.sync()
  399. self.forum.save(force_update=True)
  400. # Update monitor
  401. self.request.monitor['threads'] = int(self.request.monitor['threads']) + 1
  402. self.request.monitor['posts'] = int(self.request.monitor['posts']) + self.thread.replies + 1
  403. self.request.messages.set_flash(Message(_('Thread has been marked as reviewed and made visible to other members.')), 'success', 'threads')
  404. def thread_action_annouce(self):
  405. self.thread.weight = 2
  406. self.thread.save(force_update=True)
  407. self.request.messages.set_flash(Message(_('Thread has been turned into annoucement.')), 'success', 'threads')
  408. def thread_action_sticky(self):
  409. self.thread.weight = 1
  410. self.thread.save(force_update=True)
  411. self.request.messages.set_flash(Message(_('Thread has been turned into sticky.')), 'success', 'threads')
  412. def thread_action_normal(self):
  413. self.thread.weight = 0
  414. self.thread.save(force_update=True)
  415. self.request.messages.set_flash(Message(_('Thread weight has been changed to normal.')), 'success', 'threads')
  416. def thread_action_move(self):
  417. message = None
  418. if self.request.POST.get('do') == 'move':
  419. form = MoveThreadsForm(self.request.POST, request=self.request, forum=self.forum)
  420. if form.is_valid():
  421. new_forum = form.cleaned_data['new_forum']
  422. self.thread.move_to(new_forum)
  423. self.thread.save(force_update=True)
  424. self.forum.sync()
  425. self.forum.save(force_update=True)
  426. new_forum.sync()
  427. new_forum.save(force_update=True)
  428. self.request.messages.set_flash(Message(_('Thread has been moved to "%(forum)s".') % {'forum': new_forum.name}), 'success', 'threads')
  429. return None
  430. message = Message(form.non_field_errors()[0], 'error')
  431. else:
  432. form = MoveThreadsForm(request=self.request, forum=self.forum)
  433. return self.request.theme.render_to_response('threads/move.html',
  434. {
  435. 'message': message,
  436. 'forum': self.forum,
  437. 'parents': self.parents,
  438. 'thread': self.thread,
  439. 'form': FormLayout(form),
  440. },
  441. context_instance=RequestContext(self.request));
  442. def thread_action_open(self):
  443. self.thread.closed = False
  444. self.thread.save(force_update=True)
  445. self.thread.last_post.set_checkpoint(self.request, 'opened')
  446. self.request.messages.set_flash(Message(_('Thread has been opened.')), 'success', 'threads')
  447. def thread_action_close(self):
  448. self.thread.closed = True
  449. self.thread.save(force_update=True)
  450. self.thread.last_post.set_checkpoint(self.request, 'closed')
  451. self.request.messages.set_flash(Message(_('Thread has been closed.')), 'success', 'threads')
  452. def thread_action_undelete(self):
  453. # Update thread
  454. self.thread.deleted = False
  455. self.thread.replies_deleted -= 1
  456. self.thread.save(force_update=True)
  457. # Update first post in thread
  458. self.thread.start_post.deleted = False
  459. self.thread.start_post.save(force_update=True)
  460. # Set checkpoint
  461. self.thread.last_post.set_checkpoint(self.request, 'undeleted')
  462. # Update forum
  463. self.forum.sync()
  464. self.forum.save(force_update=True)
  465. # Update monitor
  466. self.request.monitor['threads'] = int(self.request.monitor['threads']) + 1
  467. self.request.monitor['posts'] = int(self.request.monitor['posts']) + self.thread.replies + 1
  468. self.request.messages.set_flash(Message(_('Thread has been undeleted.')), 'success', 'threads')
  469. def thread_action_soft(self):
  470. # Update thread
  471. self.thread.deleted = True
  472. self.thread.replies_deleted += 1
  473. self.thread.save(force_update=True)
  474. # Update first post in thread
  475. self.thread.start_post.deleted = True
  476. self.thread.start_post.save(force_update=True)
  477. # Set checkpoint
  478. self.thread.last_post.set_checkpoint(self.request, 'deleted')
  479. # Update forum
  480. self.forum.sync()
  481. self.forum.save(force_update=True)
  482. # Update monitor
  483. self.request.monitor['threads'] = int(self.request.monitor['threads']) - 1
  484. self.request.monitor['posts'] = int(self.request.monitor['posts']) - self.thread.replies - 1
  485. self.request.messages.set_flash(Message(_('Thread has been deleted.')), 'success', 'threads')
  486. def thread_action_hard(self):
  487. # Delete thread
  488. self.thread.delete()
  489. # Update forum
  490. self.forum.sync()
  491. self.forum.save(force_update=True)
  492. # Update monitor
  493. self.request.monitor['threads'] = int(self.request.monitor['threads']) - 1
  494. self.request.monitor['posts'] = int(self.request.monitor['posts']) - self.thread.replies - 1
  495. self.request.messages.set_flash(Message(_('Thread "%(thread)s" has been deleted.') % {'thread': self.thread.name}), 'success', 'threads')
  496. return redirect(reverse('forum', kwargs={'forum': self.forum.pk, 'slug': self.forum.slug}))
  497. def __call__(self, request, slug=None, thread=None, page=0):
  498. self.request = request
  499. self.pagination = None
  500. self.parents = None
  501. self.ignored = False
  502. self.watcher = None
  503. try:
  504. self.fetch_thread(thread)
  505. self.fetch_posts(page)
  506. self.message = request.messages.get_message('threads')
  507. self.make_thread_form()
  508. if self.thread_form:
  509. response = self.handle_thread_form()
  510. if response:
  511. return response
  512. self.make_posts_form()
  513. if self.posts_form:
  514. response = self.handle_posts_form()
  515. if response:
  516. return response
  517. except Thread.DoesNotExist:
  518. return error404(self.request)
  519. except ACLError403 as e:
  520. return error403(request, e.message)
  521. except ACLError404 as e:
  522. return error404(request, e.message)
  523. # Merge proxy into forum
  524. self.forum.closed = self.proxy.closed
  525. return request.theme.render_to_response('threads/thread.html',
  526. {
  527. 'message': self.message,
  528. 'forum': self.forum,
  529. 'parents': self.parents,
  530. 'thread': self.thread,
  531. 'is_read': self.tracker.is_read(self.thread),
  532. 'count': self.count,
  533. 'posts': self.posts,
  534. 'ignored_posts': self.ignored,
  535. 'watcher': self.watcher,
  536. 'pagination': self.pagination,
  537. 'quick_reply': FormFields(QuickReplyForm(request=request)).fields,
  538. 'thread_form': FormFields(self.thread_form).fields if self.thread_form else None,
  539. 'posts_form': FormFields(self.posts_form).fields if self.posts_form else None,
  540. },
  541. context_instance=RequestContext(request));