moderation.py 16 KB

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