posting.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434
  1. from datetime import timedelta
  2. from django.core.urlresolvers import reverse
  3. from django.shortcuts import redirect
  4. from django.template import RequestContext
  5. from django.utils import timezone
  6. from django.utils.translation import ugettext as _
  7. from misago.acl.exceptions import ACLError403, ACLError404
  8. from misago.apps.errors import error403, error404
  9. from misago.forms import FormLayout
  10. from misago.markdown import post_markdown
  11. from misago.messages import Message
  12. from misago.models import Forum, Thread, Post, WatchedThread
  13. from misago.utils.datesformats import date
  14. from misago.utils.strings import slugify
  15. from misago.utils.pagination import make_pagination
  16. from misago.utils.translation import ugettext_lazy
  17. from misago.apps.threads.forms import PostForm
  18. from misago.apps.threads.views.base import BaseView
  19. class PostingBaseView(BaseView):
  20. def __call__(self, request, **kwargs):
  21. self.request = request
  22. # Empty context attributes
  23. self.forum = None
  24. self.thread = None
  25. self.quote = None
  26. self.post = None
  27. # Let inheriting class set context
  28. self.set_context()
  29. # And set forum parents for render
  30. self.parents = Forum.objects.forum_parents(self.forum.pk, True)
  31. # Create form instance
  32. def set_context(self):
  33. raise NotImplementedError(u"\"set_context\" method should be implemented in inheriting objects.")
  34. class PostingNewThreadView(PostingBaseView):
  35. pass
  36. class PostingEditThreadView(PostingBaseView):
  37. pass
  38. class PostingNewReplyView(PostingBaseView):
  39. pass
  40. class PostingEditReplyView(PostingBaseView):
  41. pass
  42. class PostingView(BaseView):
  43. def fetch_target(self, kwargs):
  44. if self.mode == 'new_thread':
  45. self.fetch_forum(kwargs)
  46. else:
  47. self.fetch_thread(kwargs)
  48. if self.mode == 'edit_thread':
  49. self.fetch_post(self.thread.start_post_id)
  50. if self.mode == 'edit_post':
  51. self.fetch_post(kwargs['post'])
  52. def fetch_forum(self, kwargs):
  53. self.forum = Forum.objects.get(pk=kwargs['forum'], type='forum')
  54. self.proxy = Forum.objects.parents_aware_forum(self.forum)
  55. self.request.acl.forums.allow_forum_view(self.forum)
  56. self.request.acl.threads.allow_new_threads(self.proxy)
  57. self.parents = Forum.objects.forum_parents(self.forum.pk, True)
  58. def fetch_thread(self, kwargs):
  59. self.thread = Thread.objects.get(pk=kwargs['thread'])
  60. self.forum = self.thread.forum
  61. self.proxy = Forum.objects.parents_aware_forum(self.forum)
  62. self.request.acl.forums.allow_forum_view(self.forum)
  63. self.request.acl.threads.allow_thread_view(self.request.user, self.thread)
  64. self.request.acl.threads.allow_reply(self.proxy, self.thread)
  65. self.parents = Forum.objects.forum_parents(self.forum.pk, True)
  66. if kwargs.get('quote'):
  67. self.quote = Post.objects.select_related('user').get(pk=kwargs['quote'], thread=self.thread.pk)
  68. self.request.acl.threads.allow_post_view(self.request.user, self.thread, self.quote)
  69. def fetch_post(self, post):
  70. self.post = self.thread.post_set.get(pk=post)
  71. self.request.acl.threads.allow_post_view(self.request.user, self.thread, self.post)
  72. if self.mode == 'edit_thread':
  73. self.request.acl.threads.allow_thread_edit(self.request.user, self.proxy, self.thread, self.post)
  74. if self.mode == 'edit_post':
  75. self.request.acl.threads.allow_reply_edit(self.request.user, self.proxy, self.thread, self.post)
  76. def get_form(self, bound=False):
  77. initial = {}
  78. if self.mode == 'edit_thread':
  79. initial['thread_name'] = self.thread.name
  80. if self.mode in ['edit_thread', 'edit_post']:
  81. initial['post'] = self.post.post
  82. if self.quote:
  83. quote_post = []
  84. if self.quote.user:
  85. quote_post.append('@%s' % self.quote.user.username)
  86. else:
  87. quote_post.append('@%s' % self.quote.user_name)
  88. for line in self.quote.post.splitlines():
  89. quote_post.append('> %s' % line)
  90. quote_post.append('\n')
  91. initial['post'] = '\n'.join(quote_post)
  92. if bound:
  93. return PostForm(self.request.POST, request=self.request, mode=self.mode, initial=initial)
  94. return PostForm(request=self.request, mode=self.mode, initial=initial)
  95. def __call__(self, request, **kwargs):
  96. self.request = request
  97. self.forum = None
  98. self.thread = None
  99. self.quote = None
  100. self.post = None
  101. self.parents = None
  102. self.mode = kwargs.get('mode')
  103. if self.request.POST.get('quick_reply') and self.mode == 'new_post':
  104. self.mode = 'new_post_quick'
  105. try:
  106. self.fetch_target(kwargs)
  107. if not request.user.is_authenticated():
  108. raise ACLError403(_("Guest, you have to sign-in in order to post replies."))
  109. except (Forum.DoesNotExist, Thread.DoesNotExist, Post.DoesNotExist):
  110. return error404(self.request)
  111. except ACLError403 as e:
  112. return error403(request, e.message)
  113. except ACLError404 as e:
  114. return error404(request, e.message)
  115. message = request.messages.get_message('threads')
  116. if request.method == 'POST':
  117. form = self.get_form(True)
  118. # Show message preview
  119. if 'preview' in request.POST:
  120. if form['post'].value():
  121. md, preparsed = post_markdown(request, form['post'].value())
  122. else:
  123. md, preparsed = None, None
  124. form.empty_errors()
  125. return request.theme.render_to_response('threads/posting.html',
  126. {
  127. 'mode': self.mode,
  128. 'forum': self.forum,
  129. 'thread': self.thread,
  130. 'post': self.post,
  131. 'quote': self.quote,
  132. 'parents': self.parents,
  133. 'message': message,
  134. 'preview': preparsed,
  135. 'form': FormLayout(form),
  136. },
  137. context_instance=RequestContext(request));
  138. # Commit form to database
  139. if form.is_valid():
  140. # Record original vars if user is editing
  141. if self.mode in ['edit_thread', 'edit_post']:
  142. old_name = self.thread.name
  143. old_post = self.post.post
  144. # If there is no change, throw user back
  145. changed_name = (old_name != form.cleaned_data['thread_name']) if self.mode == 'edit_thread' else False
  146. changed_post = old_post != form.cleaned_data['post']
  147. changed_anything = changed_name or changed_post
  148. # Some extra initialisation
  149. now = timezone.now()
  150. md = None
  151. moderation = False
  152. if not request.acl.threads.acl[self.forum.pk]['can_approve']:
  153. if self.mode == 'new_thread' and request.acl.threads.acl[self.forum.pk]['can_start_threads'] == 1:
  154. moderation = True
  155. if self.mode in ['new_post', 'new_post_quick'] and request.acl.threads.acl[self.forum.pk]['can_write_posts'] == 1:
  156. moderation = True
  157. # Get or create new thread
  158. if self.mode == 'new_thread':
  159. thread = Thread.objects.create(
  160. forum=self.forum,
  161. name=form.cleaned_data['thread_name'],
  162. slug=slugify(form.cleaned_data['thread_name']),
  163. start=now,
  164. last=now,
  165. moderated=moderation,
  166. score=request.settings['thread_ranking_initial_score'],
  167. )
  168. if moderation:
  169. thread.replies_moderated += 1
  170. else:
  171. thread = self.thread
  172. if self.mode == 'edit_thread':
  173. thread.name = form.cleaned_data['thread_name']
  174. thread.slug = slugify(form.cleaned_data['thread_name'])
  175. thread.previous_last = thread.last
  176. # Create new message
  177. if self.mode in ['new_thread', 'new_post', 'new_post_quick']:
  178. # Use last post instead?
  179. if self.mode in ['new_post', 'new_post_quick']:
  180. merge_diff = (now - self.thread.last)
  181. merge_diff = (merge_diff.days * 86400) + merge_diff.seconds
  182. if (self.mode in ['new_post', 'new_post_quick']
  183. and request.settings.post_merge_time
  184. and merge_diff < (request.settings.post_merge_time * 60)
  185. and self.thread.last_poster_id == request.user.id):
  186. # Overtake posting
  187. post = self.thread.last_post
  188. post.appended = True
  189. post.moderated = moderation
  190. post.date = now
  191. post.post = '%s\n\n- - -\n**%s**\n%s' % (post.post, _("Added on %(date)s:") % {'date': date(now, 'SHORT_DATETIME_FORMAT')}, form.cleaned_data['post'])
  192. md, post.post_preparsed = post_markdown(request, post.post)
  193. post.save(force_update=True)
  194. thread.last = now
  195. thread.save(force_update=True)
  196. self.forum.last = now
  197. self.forum.save(force_update=True)
  198. # Ignore rest of posting action
  199. request.messages.set_flash(Message(_("Your reply has been added to previous one.")), 'success', 'threads_%s' % post.pk)
  200. return self.redirect_to_post(post)
  201. else:
  202. md, post_preparsed = post_markdown(request, form.cleaned_data['post'])
  203. post = Post.objects.create(
  204. forum=self.forum,
  205. thread=thread,
  206. merge=thread.merges,
  207. user=request.user,
  208. user_name=request.user.username,
  209. ip=request.session.get_ip(request),
  210. agent=request.META.get('HTTP_USER_AGENT'),
  211. post=form.cleaned_data['post'],
  212. post_preparsed=post_preparsed,
  213. date=now,
  214. moderated=moderation,
  215. )
  216. post.appended = False
  217. elif changed_post:
  218. # Change message
  219. post = self.post
  220. post.post = form.cleaned_data['post']
  221. md, post.post_preparsed = post_markdown(request, form.cleaned_data['post'])
  222. post.edits += 1
  223. post.edit_date = now
  224. post.edit_user = request.user
  225. post.edit_user_name = request.user.username
  226. post.edit_user_slug = request.user.username_slug
  227. post.save(force_update=True)
  228. # Record this edit in changelog?
  229. if self.mode in ['edit_thread', 'edit_post'] and changed_anything:
  230. self.post.change_set.create(
  231. forum=self.forum,
  232. thread=self.thread,
  233. post=self.post,
  234. user=request.user,
  235. user_name=request.user.username,
  236. user_slug=request.user.username_slug,
  237. date=now,
  238. ip=request.session.get_ip(request),
  239. agent=request.META.get('HTTP_USER_AGENT'),
  240. reason=form.cleaned_data['edit_reason'],
  241. size=len(self.post.post),
  242. change=len(self.post.post) - len(old_post),
  243. thread_name_old=old_name if self.mode == 'edit_thread' and form.cleaned_data['thread_name'] != old_name else None,
  244. thread_name_new=self.thread.name if self.mode == 'edit_thread' and form.cleaned_data['thread_name'] != old_name else None,
  245. post_content=old_post,
  246. )
  247. # Set thread start post and author data
  248. if self.mode == 'new_thread':
  249. thread.start_post = post
  250. thread.start_poster = request.user
  251. thread.start_poster_name = request.user.username
  252. thread.start_poster_slug = request.user.username_slug
  253. if request.user.rank and request.user.rank.style:
  254. thread.start_poster_style = request.user.rank.style
  255. # Reward user for posting new thread?
  256. if not request.user.last_post or request.user.last_post < timezone.now() - timedelta(seconds=request.settings['score_reward_new_post_cooldown']):
  257. request.user.score += request.settings['score_reward_new_thread']
  258. # New post - increase post counters, thread score
  259. # Notify quoted post author and close thread if it has hit limit
  260. if self.mode in ['new_post', 'new_post_quick']:
  261. if moderation:
  262. thread.replies_moderated += 1
  263. else:
  264. thread.replies += 1
  265. if thread.last_poster_id != request.user.pk:
  266. thread.score += request.settings['thread_ranking_reply_score']
  267. # Notify quoted poster of reply?
  268. if self.quote and self.quote.user_id and self.quote.user_id != request.user.pk and not self.quote.user.is_ignoring(request.user):
  269. alert = self.quote.user.alert(ugettext_lazy("%(username)s has replied to your post in thread %(thread)s").message)
  270. alert.profile('username', request.user)
  271. alert.post('thread', self.thread, post)
  272. alert.save_all()
  273. if (self.request.settings.thread_length > 0
  274. and not thread.closed
  275. and thread.replies >= self.request.settings.thread_length):
  276. thread.closed = True
  277. post.set_checkpoint(self.request, 'limit')
  278. # Reward user for posting new post?
  279. if not post.appended and (not request.user.last_post or request.user.last_post < timezone.now() - timedelta(seconds=request.settings['score_reward_new_post_cooldown'])):
  280. request.user.score += request.settings['score_reward_new_post']
  281. # Update last poster data
  282. if not moderation and self.mode not in ['edit_thread', 'edit_post']:
  283. thread.last = now
  284. thread.last_post = post
  285. thread.last_poster = request.user
  286. thread.last_poster_name = request.user.username
  287. thread.last_poster_slug = request.user.username_slug
  288. thread.last_poster_style = request.user.rank.style
  289. # Final update of thread entry
  290. if self.mode != 'edit_post':
  291. thread.save(force_update=True)
  292. # Update forum and monitor
  293. if not moderation:
  294. if self.mode == 'new_thread':
  295. self.request.monitor['threads'] = int(self.request.monitor['threads']) + 1
  296. self.forum.threads += 1
  297. if self.mode in ['new_thread', 'new_post', 'new_post_quick']:
  298. self.request.monitor['posts'] = int(self.request.monitor['posts']) + 1
  299. self.forum.posts += 1
  300. if self.mode in ['new_thread', 'new_post', 'new_post_quick'] or (
  301. self.mode == 'edit_thread'
  302. and self.forum.last_thread_id == thread.pk
  303. and self.forum.last_thread_name != thread.name):
  304. self.forum.last_thread = thread
  305. self.forum.last_thread_name = thread.name
  306. self.forum.last_thread_slug = thread.slug
  307. self.forum.last_thread_date = thread.last
  308. if self.mode in ['new_thread', 'new_post', 'new_post_quick']:
  309. self.forum.last_poster = thread.last_poster
  310. self.forum.last_poster_name = thread.last_poster_name
  311. self.forum.last_poster_slug = thread.last_poster_slug
  312. self.forum.last_poster_style = thread.last_poster_style
  313. if self.mode != 'edit_post':
  314. self.forum.save(force_update=True)
  315. # Update user
  316. if not moderation:
  317. if self.mode == 'new_thread':
  318. request.user.threads += 1
  319. request.user.posts += 1
  320. if self.mode in ['new_thread', 'new_post', 'new_post_quick']:
  321. request.user.last_post = thread.last
  322. request.user.save(force_update=True)
  323. # Notify users about post
  324. if md:
  325. try:
  326. if self.quote and self.quote.user_id:
  327. del md.mentions[self.quote.user.username_slug]
  328. except KeyError:
  329. pass
  330. if md.mentions:
  331. post.notify_mentioned(request, md.mentions)
  332. post.save(force_update=True)
  333. # Set thread watch status
  334. if self.mode == 'new_thread' and request.user.subscribe_start:
  335. WatchedThread.objects.create(
  336. user=request.user,
  337. forum=self.forum,
  338. thread=thread,
  339. last_read=now,
  340. email=(request.user.subscribe_start == 2),
  341. )
  342. if self.mode in ['new_post', 'new_post_quick'] and request.user.subscribe_reply:
  343. try:
  344. watcher = WatchedThread.objects.get(user=request.user, thread=self.thread)
  345. except WatchedThread.DoesNotExist:
  346. WatchedThread.objects.create(
  347. user=request.user,
  348. forum=self.forum,
  349. thread=thread,
  350. last_read=now,
  351. email=(request.user.subscribe_reply == 2),
  352. )
  353. # Set flash and redirect user to his post
  354. if self.mode == 'new_thread':
  355. if moderation:
  356. request.messages.set_flash(Message(_("New thread has been posted. It will be hidden from other members until moderator reviews it.")), 'success', 'threads')
  357. else:
  358. request.messages.set_flash(Message(_("New thread has been posted.")), 'success', 'threads')
  359. return redirect(reverse('thread', kwargs={'thread': thread.pk, 'slug': thread.slug}) + ('#post-%s' % post.pk))
  360. if self.mode in ['new_post', 'new_post_quick']:
  361. thread.email_watchers(request, post)
  362. if moderation:
  363. request.messages.set_flash(Message(_("Your reply has been posted. It will be hidden from other members until moderator reviews it.")), 'success', 'threads_%s' % post.pk)
  364. else:
  365. request.messages.set_flash(Message(_("Your reply has been posted.")), 'success', 'threads_%s' % post.pk)
  366. return self.redirect_to_post(post)
  367. if self.mode == 'edit_thread':
  368. request.messages.set_flash(Message(_("Your thread has been edited.")), 'success', 'threads_%s' % self.post.pk)
  369. if self.mode == 'edit_post':
  370. request.messages.set_flash(Message(_("Your reply has been edited.")), 'success', 'threads_%s' % self.post.pk)
  371. return self.redirect_to_post(self.post)
  372. return redirect(reverse('thread', kwargs={'thread': self.thread.pk, 'slug': self.thread.slug}) + ('#post-%s' % self.post.pk))
  373. message = Message(form.non_field_errors()[0], 'error')
  374. else:
  375. form = self.get_form()
  376. # Merge proxy into forum
  377. self.forum.closed = self.proxy.closed
  378. return request.theme.render_to_response('threads/posting.html',
  379. {
  380. 'mode': self.mode,
  381. 'forum': self.forum,
  382. 'thread': self.thread,
  383. 'post': self.post,
  384. 'quote': self.quote,
  385. 'parents': self.parents,
  386. 'message': message,
  387. 'form': FormLayout(form),
  388. },
  389. context_instance=RequestContext(request));