moderation.py 18 KB

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