posting.py 21 KB

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