postsactions.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  1. from django.contrib import messages
  2. from django.db.transaction import atomic
  3. from django.http import Http404
  4. from django.shortcuts import redirect, render
  5. from django.utils import timezone
  6. from django.utils.translation import ungettext, ugettext_lazy, ugettext as _
  7. from misago.forums.lists import get_forum_path
  8. from misago.threads import moderation
  9. from misago.threads.forms.moderation import MovePostsForm, SplitThreadForm
  10. from misago.threads.models import Thread
  11. from misago.threads.paginator import Paginator
  12. from misago.threads.views.generic.actions import ActionsBase, ReloadAfterDelete
  13. __all__ = ['PostsActions']
  14. def thread_aware_posts(f):
  15. def decorator(self, request, posts):
  16. for post in posts:
  17. post.thread = self.thread
  18. return f(self, request, posts)
  19. return decorator
  20. def changes_thread_state(f):
  21. @thread_aware_posts
  22. def decorator(self, request, posts):
  23. with atomic():
  24. self.thread.lock()
  25. response = f(self, request, posts)
  26. self.thread.synchronize()
  27. self.thread.save()
  28. self.forum.lock()
  29. self.forum.synchronize()
  30. self.forum.save()
  31. return response
  32. return decorator
  33. class PostsActions(ActionsBase):
  34. select_items_message = ugettext_lazy(
  35. "You have to select at least one post.")
  36. is_mass_action = True
  37. def redirect_after_deletion(self, request, queryset):
  38. paginator = Paginator(queryset, 10, 3)
  39. current_page = int(request.resolver_match.kwargs.get('page', 0))
  40. if paginator.num_pages < current_page:
  41. namespace = request.resolver_match.namespace
  42. url_name = request.resolver_match.url_name
  43. kwars = request.resolver_match.kwargs
  44. kwars['page'] = paginator.num_pages
  45. if kwars['page'] == 1:
  46. del kwars['page']
  47. return redirect('%s:%s' % (namespace, url_name), **kwars)
  48. else:
  49. return redirect(request.path)
  50. def get_available_actions(self, kwargs):
  51. self.thread = kwargs['thread']
  52. self.forum = self.thread.forum
  53. actions = []
  54. if self.thread.acl['can_review']:
  55. if self.thread.has_moderated_posts:
  56. actions.append({
  57. 'action': 'approve',
  58. 'icon': 'check',
  59. 'name': _("Approve posts")
  60. })
  61. if self.forum.acl['can_merge_posts']:
  62. actions.append({
  63. 'action': 'merge',
  64. 'icon': 'compress',
  65. 'name': _("Merge posts into one")
  66. })
  67. if self.forum.acl['can_move_posts']:
  68. actions.append({
  69. 'action': 'move',
  70. 'icon': 'arrow-right',
  71. 'name': _("Move posts to other thread")
  72. })
  73. if self.forum.acl['can_split_threads']:
  74. actions.append({
  75. 'action': 'split',
  76. 'icon': 'code-fork',
  77. 'name': _("Split posts to new thread")
  78. })
  79. if self.forum.acl['can_protect_posts']:
  80. actions.append({
  81. 'action': 'unprotect',
  82. 'icon': 'unlock-alt',
  83. 'name': _("Release posts")
  84. })
  85. actions.append({
  86. 'action': 'protect',
  87. 'icon': 'lock',
  88. 'name': _("Protect posts")
  89. })
  90. if self.forum.acl['can_hide_posts']:
  91. actions.append({
  92. 'action': 'unhide',
  93. 'icon': 'eye',
  94. 'name': _("Reveal posts")
  95. })
  96. actions.append({
  97. 'action': 'hide',
  98. 'icon': 'eye-slash',
  99. 'name': _("Hide posts")
  100. })
  101. if self.forum.acl['can_hide_posts'] == 2:
  102. actions.append({
  103. 'action': 'delete',
  104. 'icon': 'times',
  105. 'name': _("Delete posts"),
  106. 'confirmation': _("Are you sure you want to delete selected "
  107. "posts? This action can't be undone.")
  108. })
  109. return actions
  110. @changes_thread_state
  111. def action_approve(self, request, posts):
  112. changed_posts = 0
  113. for post in posts:
  114. if moderation.approve_post(request.user, post):
  115. changed_posts += 1
  116. if changed_posts:
  117. message = ungettext(
  118. '%(changed)d post was approved.',
  119. '%(changed)d posts were approved.',
  120. changed_posts)
  121. messages.success(request, message % {'changed': changed_posts})
  122. else:
  123. message = _("No posts were approved.")
  124. messages.info(request, message)
  125. @changes_thread_state
  126. def action_merge(self, request, posts):
  127. first_post = posts[0]
  128. changed_posts = len(posts)
  129. if changed_posts < 2:
  130. message = _("You have to select at least two posts to merge.")
  131. raise moderation.ModerationError(message)
  132. for post in posts:
  133. if not post.poster_id or first_post.poster_id != post.poster_id:
  134. message = _("You can't merge posts made by different authors.")
  135. raise moderation.ModerationError(message)
  136. for post in posts[1:]:
  137. post.merge(first_post)
  138. post.delete()
  139. first_post.save()
  140. message = ungettext(
  141. '%(changed)d post was merged.',
  142. '%(changed)d posts were merged.',
  143. changed_posts)
  144. messages.success(request, message % {'changed': changed_posts})
  145. move_posts_full_template = 'misago/thread/move_posts/full.html'
  146. move_posts_modal_template = 'misago/thread/move_posts/modal.html'
  147. @changes_thread_state
  148. def action_move(self, request, posts):
  149. if posts[0].id == self.thread.first_post_id:
  150. message = _("You can't move thread's first post.")
  151. raise moderation.ModerationError(message)
  152. form = MovePostsForm(user=request.user, thread=self.thread)
  153. if 'submit' in request.POST or 'follow' in request.POST:
  154. form = MovePostsForm(request.POST,
  155. user=request.user,
  156. thread=self.thread)
  157. if form.is_valid():
  158. for post in posts:
  159. post.move(form.new_thread)
  160. post.save()
  161. form.new_thread.lock()
  162. form.new_thread.synchronize()
  163. form.new_thread.save()
  164. if form.new_thread.forum != self.forum:
  165. form.new_thread.forum.lock()
  166. form.new_thread.forum.synchronize()
  167. form.new_thread.forum.save()
  168. changed_posts = len(posts)
  169. message = ungettext(
  170. '%(changed)d post was moved to "%(thread)s".',
  171. '%(changed)d posts were moved to "%(thread)s".',
  172. changed_posts)
  173. messages.success(request, message % {
  174. 'changed': changed_posts,
  175. 'thread': form.new_thread.title
  176. })
  177. if 'follow' in request.POST:
  178. return redirect(form.new_thread.get_absolute_url())
  179. else:
  180. return None # trigger thread refresh
  181. if request.is_ajax():
  182. template = self.move_posts_modal_template
  183. else:
  184. template = self.move_posts_full_template
  185. return render(request, template, {
  186. 'form': form,
  187. 'forum': self.forum,
  188. 'thread': self.thread,
  189. 'path': get_forum_path(self.forum),
  190. 'posts': posts
  191. })
  192. split_thread_full_template = 'misago/thread/split/full.html'
  193. split_thread_modal_template = 'misago/thread/split/modal.html'
  194. @changes_thread_state
  195. def action_split(self, request, posts):
  196. if posts[0].id == self.thread.first_post_id:
  197. message = _("You can't split thread's first post.")
  198. raise moderation.ModerationError(message)
  199. form = SplitThreadForm(acl=request.user.acl)
  200. if 'submit' in request.POST or 'follow' in request.POST:
  201. form = SplitThreadForm(request.POST, acl=request.user.acl)
  202. if form.is_valid():
  203. split_thread = Thread()
  204. split_thread.forum = form.cleaned_data['forum']
  205. split_thread.set_title(
  206. form.cleaned_data['thread_title'])
  207. split_thread.starter_name = "-"
  208. split_thread.starter_slug = "-"
  209. split_thread.last_poster_name = "-"
  210. split_thread.last_poster_slug = "-"
  211. split_thread.started_on = timezone.now()
  212. split_thread.last_post_on = timezone.now()
  213. split_thread.save()
  214. for post in posts:
  215. post.move(split_thread)
  216. post.save()
  217. split_thread.synchronize()
  218. split_thread.save()
  219. if split_thread.forum != self.forum:
  220. split_thread.forum.lock()
  221. split_thread.forum.synchronize()
  222. split_thread.forum.save()
  223. changed_posts = len(posts)
  224. message = ungettext(
  225. '%(changed)d post was split to "%(thread)s".',
  226. '%(changed)d posts were split to "%(thread)s".',
  227. changed_posts)
  228. messages.success(request, message % {
  229. 'changed': changed_posts,
  230. 'thread': split_thread.title
  231. })
  232. if 'follow' in request.POST:
  233. return redirect(split_thread.get_absolute_url())
  234. else:
  235. return None # trigger thread refresh
  236. if request.is_ajax():
  237. template = self.split_thread_modal_template
  238. else:
  239. template = self.split_thread_full_template
  240. return render(request, template, {
  241. 'form': form,
  242. 'forum': self.forum,
  243. 'thread': self.thread,
  244. 'path': get_forum_path(self.forum),
  245. 'posts': posts
  246. })
  247. def action_unprotect(self, request, posts):
  248. changed_posts = 0
  249. for post in posts:
  250. if moderation.unprotect_post(request.user, post):
  251. changed_posts += 1
  252. if changed_posts:
  253. message = ungettext(
  254. '%(changed)d post was released from protection.',
  255. '%(changed)d posts were released from protection.',
  256. changed_posts)
  257. messages.success(request, message % {'changed': changed_posts})
  258. else:
  259. message = _("No posts were released from protection.")
  260. messages.info(request, message)
  261. def action_protect(self, request, posts):
  262. changed_posts = 0
  263. for post in posts:
  264. if moderation.protect_post(request.user, post):
  265. changed_posts += 1
  266. if changed_posts:
  267. message = ungettext(
  268. '%(changed)d post was made protected.',
  269. '%(changed)d posts were made protected.',
  270. changed_posts)
  271. messages.success(request, message % {'changed': changed_posts})
  272. else:
  273. message = _("No posts were made protected.")
  274. messages.info(request, message)
  275. @changes_thread_state
  276. def action_unhide(self, request, posts):
  277. changed_posts = 0
  278. for post in posts:
  279. if moderation.unhide_post(request.user, post):
  280. changed_posts += 1
  281. if changed_posts:
  282. message = ungettext(
  283. '%(changed)d post was made visible.',
  284. '%(changed)d posts were made visible.',
  285. changed_posts)
  286. messages.success(request, message % {'changed': changed_posts})
  287. else:
  288. message = _("No posts were made visible.")
  289. messages.info(request, message)
  290. @changes_thread_state
  291. def action_hide(self, request, posts):
  292. changed_posts = 0
  293. for post in posts:
  294. if moderation.hide_post(request.user, post):
  295. changed_posts += 1
  296. if changed_posts:
  297. message = ungettext(
  298. '%(changed)d post was hidden.',
  299. '%(changed)d posts were hidden.',
  300. changed_posts)
  301. messages.success(request, message % {'changed': changed_posts})
  302. else:
  303. message = _("No posts were hidden.")
  304. messages.info(request, message)
  305. @changes_thread_state
  306. def action_delete(self, request, posts):
  307. changed_posts = 0
  308. first_deleted = None
  309. for post in posts:
  310. if moderation.delete_post(request.user, post):
  311. changed_posts += 1
  312. if not first_deleted:
  313. first_deleted = post
  314. if changed_posts:
  315. message = ungettext(
  316. '%(changed)d post was deleted.',
  317. '%(changed)d posts were deleted.',
  318. changed_posts)
  319. messages.success(request, message % {'changed': changed_posts})
  320. return ReloadAfterDelete()
  321. else:
  322. message = _("No posts were deleted.")
  323. messages.info(request, message)