posting.py 22 KB

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