moderation.py 18 KB

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