moderation.py 18 KB

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