moderation.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  1. from rest_framework import serializers
  2. from django.core.exceptions import PermissionDenied, ValidationError
  3. from django.http import Http404
  4. from django.utils.translation import ugettext as _, ugettext_lazy, ungettext
  5. from misago.acl import add_acl
  6. from misago.conf import settings
  7. from misago.threads.models import Thread
  8. from misago.threads.permissions import (
  9. allow_delete_event, allow_delete_post, allow_merge_post, allow_merge_thread,
  10. allow_move_post, allow_split_post, can_reply_thread, can_see_thread,
  11. can_start_thread, exclude_invisible_posts)
  12. from misago.threads.pollmergehandler import PollMergeHandler
  13. from misago.threads.utils import get_thread_id_from_url
  14. from misago.threads.validators import validate_category, validate_title
  15. POSTS_LIMIT = settings.MISAGO_POSTS_PER_PAGE + settings.MISAGO_POSTS_TAIL
  16. __all__ = [
  17. 'DeletePostsSerializer',
  18. 'MergePostsSerializer',
  19. 'MergeThreadSerializer',
  20. 'MovePostsSerializer',
  21. 'NewThreadSerializer',
  22. 'SplitPostsSerializer',
  23. ]
  24. class DeletePostsSerializer(serializers.Serializer):
  25. error_empty_or_required = ugettext_lazy("You have to specify at least one post to delete.")
  26. posts = serializers.ListField(
  27. allow_empty=False,
  28. child=serializers.IntegerField(
  29. error_messages={
  30. 'invalid': ugettext_lazy("One or more post ids received were invalid."),
  31. },
  32. ),
  33. error_messages={
  34. 'required': error_empty_or_required,
  35. 'null': error_empty_or_required,
  36. 'empty': error_empty_or_required,
  37. },
  38. )
  39. def validate_posts(self, data):
  40. if len(data) > POSTS_LIMIT:
  41. message = ungettext(
  42. "No more than %(limit)s post can be deleted at single time.",
  43. "No more than %(limit)s posts can be deleted at single time.",
  44. POSTS_LIMIT,
  45. )
  46. raise ValidationError(message % {'limit': POSTS_LIMIT})
  47. user = self.context['user']
  48. thread = self.context['thread']
  49. posts_queryset = exclude_invisible_posts(user, thread.category, thread.post_set)
  50. posts_queryset = posts_queryset.filter(id__in=data).order_by('id')
  51. posts = []
  52. for post in posts_queryset:
  53. post.category = thread.category
  54. post.thread = thread
  55. if post.is_event:
  56. allow_delete_event(user, post)
  57. else:
  58. allow_delete_post(user, post)
  59. posts.append(post)
  60. if len(posts) != len(data):
  61. raise PermissionDenied(_("One or more posts to delete could not be found."))
  62. return posts
  63. class MergePostsSerializer(serializers.Serializer):
  64. error_empty_or_required = ugettext_lazy("You have to select at least two posts to merge.")
  65. posts = serializers.ListField(
  66. child=serializers.IntegerField(
  67. error_messages={
  68. 'invalid': ugettext_lazy("One or more post ids received were invalid."),
  69. },
  70. ),
  71. error_messages={
  72. 'null': error_empty_or_required,
  73. 'required': error_empty_or_required,
  74. },
  75. )
  76. def validate_posts(self, data):
  77. data = list(set(data))
  78. if len(data) < 2:
  79. raise serializers.ValidationError(self.error_empty_or_required)
  80. if len(data) > POSTS_LIMIT:
  81. message = ungettext(
  82. "No more than %(limit)s post can be merged at single time.",
  83. "No more than %(limit)s posts can be merged at single time.",
  84. POSTS_LIMIT,
  85. )
  86. raise serializers.ValidationError(message % {'limit': POSTS_LIMIT})
  87. user = self.context['user']
  88. thread = self.context['thread']
  89. posts_queryset = exclude_invisible_posts(user, thread.category, thread.post_set)
  90. posts_queryset = posts_queryset.filter(id__in=data).order_by('id')
  91. posts = []
  92. for post in posts_queryset:
  93. post.category = thread.category
  94. post.thread = thread
  95. try:
  96. allow_merge_post(user, post)
  97. except PermissionDenied as e:
  98. raise serializers.ValidationError(e)
  99. if not posts:
  100. posts.append(post)
  101. else:
  102. authorship_error = _("Posts made by different users can't be merged.")
  103. if posts[0].poster_id:
  104. if post.poster_id != posts[0].poster_id:
  105. raise serializers.ValidationError(authorship_error)
  106. else:
  107. if post.poster_id or post.poster_name != posts[0].poster_name:
  108. raise serializers.ValidationError(authorship_error)
  109. if posts[0].pk != thread.first_post_id:
  110. if (posts[0].is_hidden != post.is_hidden or
  111. posts[0].is_unapproved != post.is_unapproved):
  112. raise serializers.ValidationError(
  113. _("Posts with different visibility can't be merged.")
  114. )
  115. posts.append(post)
  116. if len(posts) != len(data):
  117. raise serializers.ValidationError(_("One or more posts to merge could not be found."))
  118. return posts
  119. class MovePostsSerializer(serializers.Serializer):
  120. error_empty_or_required = ugettext_lazy("You have to specify at least one post to move.")
  121. new_thread = serializers.CharField(
  122. error_messages={
  123. 'required': ugettext_lazy("Enter link to new thread."),
  124. },
  125. )
  126. posts = serializers.ListField(
  127. allow_empty=False,
  128. child=serializers.IntegerField(
  129. error_messages={
  130. 'invalid': ugettext_lazy("One or more post ids received were invalid."),
  131. },
  132. ),
  133. error_messages={
  134. 'empty': error_empty_or_required,
  135. 'null': error_empty_or_required,
  136. 'required': error_empty_or_required,
  137. },
  138. )
  139. def validate_new_thread(self, data):
  140. request = self.context['request']
  141. thread = self.context['thread']
  142. viewmodel = self.context['viewmodel']
  143. new_thread_id = get_thread_id_from_url(request, data)
  144. if not new_thread_id:
  145. raise serializers.ValidationError(_("This is not a valid thread link."))
  146. if new_thread_id == thread.pk:
  147. raise serializers.ValidationError(_("Thread to move posts to is same as current one."))
  148. try:
  149. new_thread = viewmodel(request, new_thread_id).unwrap()
  150. except Http404:
  151. raise serializers.ValidationError(
  152. _(
  153. "The thread you have entered link to doesn't "
  154. "exist or you don't have permission to see it."
  155. )
  156. )
  157. if not new_thread.acl['can_reply']:
  158. raise serializers.ValidationError(_("You can't move posts to threads you can't reply."))
  159. return new_thread
  160. def validate_posts(self, data):
  161. data = list(set(data))
  162. if len(data) > POSTS_LIMIT:
  163. message = ungettext(
  164. "No more than %(limit)s post can be moved at single time.",
  165. "No more than %(limit)s posts can be moved at single time.",
  166. POSTS_LIMIT,
  167. )
  168. raise serializers.ValidationError(message % {'limit': POSTS_LIMIT})
  169. request = self.context['request']
  170. thread = self.context['thread']
  171. posts_queryset = exclude_invisible_posts(request.user, thread.category, thread.post_set)
  172. posts_queryset = posts_queryset.filter(id__in=data).order_by('id')
  173. posts = []
  174. for post in posts_queryset:
  175. post.category = thread.category
  176. post.thread = thread
  177. try:
  178. allow_move_post(request.user, post)
  179. posts.append(post)
  180. except PermissionDenied as e:
  181. raise serializers.ValidationError(e)
  182. if len(posts) != len(data):
  183. raise serializers.ValidationError(_("One or more posts to move could not be found."))
  184. return posts
  185. class NewThreadSerializer(serializers.Serializer):
  186. title = serializers.CharField()
  187. category = serializers.IntegerField()
  188. weight = serializers.IntegerField(
  189. required=False,
  190. allow_null=True,
  191. max_value=Thread.WEIGHT_GLOBAL,
  192. min_value=Thread.WEIGHT_DEFAULT,
  193. )
  194. is_hidden = serializers.NullBooleanField(required=False)
  195. is_closed = serializers.NullBooleanField(required=False)
  196. def validate_title(self, title):
  197. return validate_title(title)
  198. def validate_category(self, category_id):
  199. self.category = validate_category(self.context['user'], category_id)
  200. if not can_start_thread(self.context['user'], self.category):
  201. raise ValidationError(_("You can't create new threads in selected category."))
  202. return self.category
  203. def validate_weight(self, weight):
  204. try:
  205. add_acl(self.context['user'], self.category)
  206. except AttributeError:
  207. return weight # don't validate weight further if category failed
  208. if weight > self.category.acl.get('can_pin_threads', 0):
  209. if weight == 2:
  210. raise ValidationError(
  211. _("You don't have permission to pin threads globally in this category.")
  212. )
  213. else:
  214. raise ValidationError(
  215. _("You don't have permission to pin threads in this category.")
  216. )
  217. return weight
  218. def validate_is_hidden(self, is_hidden):
  219. try:
  220. add_acl(self.context['user'], self.category)
  221. except AttributeError:
  222. return is_hidden # don't validate hidden further if category failed
  223. if is_hidden and not self.category.acl.get('can_hide_threads'):
  224. raise ValidationError(_("You don't have permission to hide threads in this category."))
  225. return is_hidden
  226. def validate_is_closed(self, is_closed):
  227. try:
  228. add_acl(self.context['user'], self.category)
  229. except AttributeError:
  230. return is_closed # don't validate closed further if category failed
  231. if is_closed and not self.category.acl.get('can_close_threads'):
  232. raise ValidationError(
  233. _("You don't have permission to close threads in this category.")
  234. )
  235. return is_closed
  236. class SplitPostsSerializer(NewThreadSerializer):
  237. error_empty_or_required = ugettext_lazy("You have to specify at least one post to split.")
  238. posts = serializers.ListField(
  239. allow_empty=False,
  240. child=serializers.IntegerField(
  241. error_messages={
  242. 'invalid': ugettext_lazy("One or more post ids received were invalid."),
  243. },
  244. ),
  245. error_messages={
  246. 'empty': error_empty_or_required,
  247. 'null': error_empty_or_required,
  248. 'required': error_empty_or_required,
  249. },
  250. )
  251. def validate_posts(self, data):
  252. if len(data) > POSTS_LIMIT:
  253. message = ungettext(
  254. "No more than %(limit)s post can be split at single time.",
  255. "No more than %(limit)s posts can be split at single time.",
  256. POSTS_LIMIT,
  257. )
  258. raise ValidationError(message % {'limit': POSTS_LIMIT})
  259. thread = self.context['thread']
  260. user = self.context['user']
  261. posts_queryset = exclude_invisible_posts(user, thread.category, thread.post_set)
  262. posts_queryset = posts_queryset.filter(id__in=data).order_by('id')
  263. posts = []
  264. for post in posts_queryset:
  265. post.category = thread.category
  266. post.thread = thread
  267. try:
  268. allow_split_post(user, post)
  269. except PermissionDenied as e:
  270. raise ValidationError(e)
  271. posts.append(post)
  272. if len(posts) != len(data):
  273. raise ValidationError(_("One or more posts to split could not be found."))
  274. return posts
  275. class MergeThreadSerializer(serializers.Serializer):
  276. other_thread = serializers.CharField(
  277. error_messages={
  278. 'required': ugettext_lazy("Enter link to new thread."),
  279. },
  280. )
  281. poll = serializers.IntegerField(
  282. required=False,
  283. error_messages={
  284. 'invalid': ugettext_lazy("Invalid choice."),
  285. },
  286. )
  287. def validate_other_thread(self, data):
  288. request = self.context['request']
  289. thread = self.context['thread']
  290. viewmodel = self.context['viewmodel']
  291. other_thread_id = get_thread_id_from_url(request, data)
  292. if not other_thread_id:
  293. raise ValidationError(_("This is not a valid thread link."))
  294. if other_thread_id == thread.pk:
  295. raise ValidationError(_("You can't merge thread with itself."))
  296. try:
  297. other_thread = viewmodel(request, other_thread_id).unwrap()
  298. allow_merge_thread(request.user, other_thread, otherthread=True)
  299. except PermissionDenied as e:
  300. raise serializers.ValidationError(e)
  301. except Http404:
  302. raise ValidationError(
  303. _(
  304. "The thread you have entered link to doesn't "
  305. "exist or you don't have permission to see it."
  306. )
  307. )
  308. if not can_reply_thread(request.user, other_thread):
  309. raise ValidationError(_("You can't merge this thread into thread you can't reply."))
  310. return other_thread
  311. def validate(self, data):
  312. thread = self.context['thread']
  313. other_thread = data['other_thread']
  314. polls_handler = PollMergeHandler([thread, other_thread])
  315. if len(polls_handler.polls) == 1:
  316. data['poll'] = polls_handler.polls[0]
  317. elif polls_handler.is_merge_conflict():
  318. if 'poll' in data:
  319. polls_handler.set_resolution(data['poll'])
  320. if polls_handler.is_valid():
  321. data['poll'] = polls_handler.get_resolution()
  322. else:
  323. raise serializers.ValidationError({'poll': _("Invalid choice.")})
  324. else:
  325. data['polls'] = polls_handler.get_available_resolutions()
  326. self.polls_handler = polls_handler
  327. return data