postsactions.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  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.forum.acl['can_merge_posts']:
  55. actions.append({
  56. 'action': 'merge',
  57. 'icon': 'compress',
  58. 'name': _("Merge posts into one")
  59. })
  60. if self.forum.acl['can_move_posts']:
  61. actions.append({
  62. 'action': 'move',
  63. 'icon': 'arrow-right',
  64. 'name': _("Move posts to other thread")
  65. })
  66. if self.forum.acl['can_split_threads']:
  67. actions.append({
  68. 'action': 'split',
  69. 'icon': 'code-fork',
  70. 'name': _("Split posts to new thread")
  71. })
  72. if self.forum.acl['can_protect_posts']:
  73. actions.append({
  74. 'action': 'unprotect',
  75. 'icon': 'unlock-alt',
  76. 'name': _("Release posts")
  77. })
  78. actions.append({
  79. 'action': 'protect',
  80. 'icon': 'lock',
  81. 'name': _("Protect posts")
  82. })
  83. if self.forum.acl['can_hide_posts']:
  84. actions.append({
  85. 'action': 'unhide',
  86. 'icon': 'eye',
  87. 'name': _("Reveal posts")
  88. })
  89. actions.append({
  90. 'action': 'hide',
  91. 'icon': 'eye-slash',
  92. 'name': _("Hide posts")
  93. })
  94. if self.forum.acl['can_hide_posts'] == 2:
  95. actions.append({
  96. 'action': 'delete',
  97. 'icon': 'times',
  98. 'name': _("Delete posts"),
  99. 'confirmation': _("Are you sure you want to delete selected "
  100. "posts? This action can't be undone.")
  101. })
  102. return actions
  103. @changes_thread_state
  104. def action_merge(self, request, posts):
  105. first_post = posts[0]
  106. changed_posts = len(posts)
  107. if changed_posts < 2:
  108. message = _("You have to select at least two posts to merge.")
  109. raise moderation.ModerationError(message)
  110. for post in posts:
  111. if not post.poster_id or first_post.poster_id != post.poster_id:
  112. message = _("You can't merge posts made by different authors.")
  113. raise moderation.ModerationError(message)
  114. for post in posts[1:]:
  115. post.merge(first_post)
  116. post.delete()
  117. first_post.save()
  118. message = ungettext(
  119. '%(changed)d post was merged.',
  120. '%(changed)d posts were merged.',
  121. changed_posts)
  122. messages.success(request, message % {'changed': changed_posts})
  123. move_posts_full_template = 'misago/thread/move_posts/full.html'
  124. move_posts_modal_template = 'misago/thread/move_posts/modal.html'
  125. @changes_thread_state
  126. def action_move(self, request, posts):
  127. if posts[0].id == self.thread.first_post_id:
  128. message = _("You can't move thread's first post.")
  129. raise moderation.ModerationError(message)
  130. form = MovePostsForm(user=request.user, thread=self.thread)
  131. if 'submit' in request.POST or 'follow' in request.POST:
  132. form = MovePostsForm(request.POST,
  133. user=request.user,
  134. thread=self.thread)
  135. if form.is_valid():
  136. for post in posts:
  137. post.move(form.new_thread)
  138. post.save()
  139. form.new_thread.lock()
  140. form.new_thread.synchronize()
  141. form.new_thread.save()
  142. if form.new_thread.forum != self.forum:
  143. form.new_thread.forum.lock()
  144. form.new_thread.forum.synchronize()
  145. form.new_thread.forum.save()
  146. changed_posts = len(posts)
  147. message = ungettext(
  148. '%(changed)d post was moved to "%(thread)s".',
  149. '%(changed)d posts were moved to "%(thread)s".',
  150. changed_posts)
  151. messages.success(request, message % {
  152. 'changed': changed_posts,
  153. 'thread': form.new_thread.title
  154. })
  155. if 'follow' in request.POST:
  156. return redirect(form.new_thread.get_absolute_url())
  157. else:
  158. return None # trigger thread refresh
  159. if request.is_ajax():
  160. template = self.move_posts_modal_template
  161. else:
  162. template = self.move_posts_full_template
  163. return render(request, template, {
  164. 'form': form,
  165. 'forum': self.forum,
  166. 'thread': self.thread,
  167. 'path': get_forum_path(self.forum),
  168. 'posts': posts
  169. })
  170. split_thread_full_template = 'misago/thread/split/full.html'
  171. split_thread_modal_template = 'misago/thread/split/modal.html'
  172. @changes_thread_state
  173. def action_split(self, request, posts):
  174. if posts[0].id == self.thread.first_post_id:
  175. message = _("You can't split thread's first post.")
  176. raise moderation.ModerationError(message)
  177. form = SplitThreadForm(acl=request.user.acl)
  178. if 'submit' in request.POST or 'follow' in request.POST:
  179. form = SplitThreadForm(request.POST, acl=request.user.acl)
  180. if form.is_valid():
  181. split_thread = Thread()
  182. split_thread.forum = form.cleaned_data['forum']
  183. split_thread.set_title(
  184. form.cleaned_data['thread_title'])
  185. split_thread.starter_name = "-"
  186. split_thread.starter_slug = "-"
  187. split_thread.last_poster_name = "-"
  188. split_thread.last_poster_slug = "-"
  189. split_thread.started_on = timezone.now()
  190. split_thread.last_post_on = timezone.now()
  191. split_thread.save()
  192. for post in posts:
  193. post.move(split_thread)
  194. post.save()
  195. split_thread.synchronize()
  196. split_thread.save()
  197. if split_thread.forum != self.forum:
  198. split_thread.forum.lock()
  199. split_thread.forum.synchronize()
  200. split_thread.forum.save()
  201. changed_posts = len(posts)
  202. message = ungettext(
  203. '%(changed)d post was split to "%(thread)s".',
  204. '%(changed)d posts were split to "%(thread)s".',
  205. changed_posts)
  206. messages.success(request, message % {
  207. 'changed': changed_posts,
  208. 'thread': split_thread.title
  209. })
  210. if 'follow' in request.POST:
  211. return redirect(split_thread.get_absolute_url())
  212. else:
  213. return None # trigger thread refresh
  214. if request.is_ajax():
  215. template = self.split_thread_modal_template
  216. else:
  217. template = self.split_thread_full_template
  218. return render(request, template, {
  219. 'form': form,
  220. 'forum': self.forum,
  221. 'thread': self.thread,
  222. 'path': get_forum_path(self.forum),
  223. 'posts': posts
  224. })
  225. def action_unprotect(self, request, posts):
  226. changed_posts = 0
  227. for post in posts:
  228. if moderation.unprotect_post(request.user, post):
  229. changed_posts += 1
  230. if changed_posts:
  231. message = ungettext(
  232. '%(changed)d post was released from protection.',
  233. '%(changed)d posts were released from protection.',
  234. changed_posts)
  235. messages.success(request, message % {'changed': changed_posts})
  236. else:
  237. message = _("No posts were released from protection.")
  238. messages.info(request, message)
  239. def action_protect(self, request, posts):
  240. changed_posts = 0
  241. for post in posts:
  242. if moderation.protect_post(request.user, post):
  243. changed_posts += 1
  244. if changed_posts:
  245. message = ungettext(
  246. '%(changed)d post was made protected.',
  247. '%(changed)d posts were made protected.',
  248. changed_posts)
  249. messages.success(request, message % {'changed': changed_posts})
  250. else:
  251. message = _("No posts were made protected.")
  252. messages.info(request, message)
  253. @changes_thread_state
  254. def action_unhide(self, request, posts):
  255. changed_posts = 0
  256. for post in posts:
  257. if moderation.unhide_post(request.user, post):
  258. changed_posts += 1
  259. if changed_posts:
  260. message = ungettext(
  261. '%(changed)d post was made visible.',
  262. '%(changed)d posts were made visible.',
  263. changed_posts)
  264. messages.success(request, message % {'changed': changed_posts})
  265. else:
  266. message = _("No posts were made visible.")
  267. messages.info(request, message)
  268. @changes_thread_state
  269. def action_hide(self, request, posts):
  270. changed_posts = 0
  271. for post in posts:
  272. if moderation.hide_post(request.user, post):
  273. changed_posts += 1
  274. if changed_posts:
  275. message = ungettext(
  276. '%(changed)d post was hidden.',
  277. '%(changed)d posts were hidden.',
  278. changed_posts)
  279. messages.success(request, message % {'changed': changed_posts})
  280. else:
  281. message = _("No posts were hidden.")
  282. messages.info(request, message)
  283. @changes_thread_state
  284. def action_delete(self, request, posts):
  285. changed_posts = 0
  286. first_deleted = None
  287. for post in posts:
  288. if moderation.delete_post(request.user, post):
  289. changed_posts += 1
  290. if not first_deleted:
  291. first_deleted = post
  292. if changed_posts:
  293. message = ungettext(
  294. '%(changed)d post was deleted.',
  295. '%(changed)d posts were deleted.',
  296. changed_posts)
  297. messages.success(request, message % {'changed': changed_posts})
  298. return ReloadAfterDelete()
  299. else:
  300. message = _("No posts were deleted.")
  301. messages.info(request, message)