moderation.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556
  1. from rest_framework import serializers
  2. from rest_framework.exceptions import ValidationError
  3. from django.core.exceptions import PermissionDenied
  4. from django.http import Http404
  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. class DeletePostsSerializer(serializers.Serializer):
  23. error_empty_or_required = ugettext_lazy("You have to specify at least one post to delete.")
  24. posts = serializers.ListField(
  25. allow_empty=False,
  26. child=serializers.IntegerField(
  27. error_messages={
  28. 'invalid': ugettext_lazy("One or more post ids received were invalid."),
  29. },
  30. ),
  31. error_messages={
  32. 'required': error_empty_or_required,
  33. 'null': error_empty_or_required,
  34. 'empty': error_empty_or_required,
  35. },
  36. )
  37. def validate_posts(self, data):
  38. if len(data) > POSTS_LIMIT:
  39. message = ungettext(
  40. "No more than %(limit)s post can be deleted at single time.",
  41. "No more than %(limit)s posts can be deleted at single time.",
  42. POSTS_LIMIT,
  43. )
  44. raise ValidationError(message % {'limit': POSTS_LIMIT})
  45. user = self.context['user']
  46. thread = self.context['thread']
  47. posts_queryset = exclude_invisible_posts(user, thread.category, thread.post_set)
  48. posts_queryset = posts_queryset.filter(id__in=data).order_by('id')
  49. posts = []
  50. for post in posts_queryset:
  51. post.category = thread.category
  52. post.thread = thread
  53. if post.is_event:
  54. allow_delete_event(user, post)
  55. else:
  56. allow_delete_best_answer(user, post)
  57. allow_delete_post(user, post)
  58. posts.append(post)
  59. if len(posts) != len(data):
  60. raise PermissionDenied(_("One or more posts to delete could not be found."))
  61. return posts
  62. class MergePostsSerializer(serializers.Serializer):
  63. error_empty_or_required = ugettext_lazy("You have to select at least two posts to merge.")
  64. posts = serializers.ListField(
  65. child=serializers.IntegerField(
  66. error_messages={
  67. 'invalid': ugettext_lazy("One or more post ids received were invalid."),
  68. },
  69. ),
  70. error_messages={
  71. 'null': error_empty_or_required,
  72. 'required': error_empty_or_required,
  73. },
  74. )
  75. def validate_posts(self, data):
  76. data = list(set(data))
  77. if len(data) < 2:
  78. raise ValidationError(self.error_empty_or_required)
  79. if len(data) > POSTS_LIMIT:
  80. message = ungettext(
  81. "No more than %(limit)s post can be merged at single time.",
  82. "No more than %(limit)s posts can be merged at single time.",
  83. POSTS_LIMIT,
  84. )
  85. raise ValidationError(message % {'limit': POSTS_LIMIT})
  86. user = self.context['user']
  87. thread = self.context['thread']
  88. posts_queryset = exclude_invisible_posts(user, thread.category, thread.post_set)
  89. posts_queryset = posts_queryset.filter(id__in=data).order_by('id')
  90. posts = []
  91. for post in posts_queryset:
  92. post.category = thread.category
  93. post.thread = thread
  94. try:
  95. allow_merge_post(user, post)
  96. except PermissionDenied as e:
  97. raise ValidationError(e)
  98. if not posts:
  99. posts.append(post)
  100. continue
  101. authorship_error = _("Posts made by different users can't be merged.")
  102. if post.poster_id != posts[0].poster_id:
  103. raise serializers.ValidationError(authorship_error)
  104. elif (post.poster_id is None and posts[0].poster_id is None and
  105. post.poster_name != posts[0].poster_name):
  106. raise serializers.ValidationError(authorship_error)
  107. if posts[0].is_first_post and post.is_best_answer:
  108. raise serializers.ValidationError(
  109. _("Post marked as best answer can't be merged with thread's first post.")
  110. )
  111. if not posts[0].is_first_post:
  112. if (posts[0].is_hidden != post.is_hidden or
  113. posts[0].is_unapproved != post.is_unapproved):
  114. raise serializers.ValidationError(
  115. _("Posts with different visibility can't be merged.")
  116. )
  117. posts.append(post)
  118. if len(posts) != len(data):
  119. raise ValidationError(_("One or more posts to merge could not be found."))
  120. return posts
  121. class MovePostsSerializer(serializers.Serializer):
  122. error_empty_or_required = ugettext_lazy("You have to specify at least one post to move.")
  123. new_thread = serializers.CharField(
  124. error_messages={
  125. 'required': ugettext_lazy("Enter link to new thread."),
  126. },
  127. )
  128. posts = serializers.ListField(
  129. allow_empty=False,
  130. child=serializers.IntegerField(
  131. error_messages={
  132. 'invalid': ugettext_lazy("One or more post ids received were invalid."),
  133. },
  134. ),
  135. error_messages={
  136. 'empty': error_empty_or_required,
  137. 'null': error_empty_or_required,
  138. 'required': error_empty_or_required,
  139. },
  140. )
  141. def validate_new_thread(self, data):
  142. request = self.context['request']
  143. thread = self.context['thread']
  144. viewmodel = self.context['viewmodel']
  145. new_thread_id = get_thread_id_from_url(request, data)
  146. if not new_thread_id:
  147. raise ValidationError(_("This is not a valid thread link."))
  148. if new_thread_id == thread.pk:
  149. raise ValidationError(_("Thread to move posts to is same as current one."))
  150. try:
  151. new_thread = viewmodel(request, new_thread_id).unwrap()
  152. except Http404:
  153. raise ValidationError(
  154. _(
  155. "The thread you have entered link to doesn't "
  156. "exist or you don't have permission to see it."
  157. )
  158. )
  159. if not new_thread.acl['can_reply']:
  160. raise ValidationError(_("You can't move posts to threads you can't reply."))
  161. return new_thread
  162. def validate_posts(self, data):
  163. data = list(set(data))
  164. if len(data) > POSTS_LIMIT:
  165. message = ungettext(
  166. "No more than %(limit)s post can be moved at single time.",
  167. "No more than %(limit)s posts can be moved at single time.",
  168. POSTS_LIMIT,
  169. )
  170. raise ValidationError(message % {'limit': POSTS_LIMIT})
  171. request = self.context['request']
  172. thread = self.context['thread']
  173. posts_queryset = exclude_invisible_posts(request.user, thread.category, thread.post_set)
  174. posts_queryset = posts_queryset.filter(id__in=data).order_by('id')
  175. posts = []
  176. for post in posts_queryset:
  177. post.category = thread.category
  178. post.thread = thread
  179. try:
  180. allow_move_post(request.user, post)
  181. posts.append(post)
  182. except PermissionDenied as e:
  183. raise ValidationError(e)
  184. if len(posts) != len(data):
  185. raise ValidationError(_("One or more posts to move could not be found."))
  186. return posts
  187. class NewThreadSerializer(serializers.Serializer):
  188. title = serializers.CharField()
  189. category = serializers.IntegerField()
  190. weight = serializers.IntegerField(
  191. required=False,
  192. allow_null=True,
  193. max_value=Thread.WEIGHT_GLOBAL,
  194. min_value=Thread.WEIGHT_DEFAULT,
  195. )
  196. is_hidden = serializers.NullBooleanField(required=False)
  197. is_closed = serializers.NullBooleanField(required=False)
  198. def validate_title(self, title):
  199. return validate_title(title)
  200. def validate_category(self, category_id):
  201. self.category = validate_category(self.context['user'], category_id)
  202. if not can_start_thread(self.context['user'], self.category):
  203. raise ValidationError(
  204. _("You can't create new threads in selected category."))
  205. return self.category
  206. def validate_weight(self, weight):
  207. try:
  208. add_acl(self.context['user'], self.category)
  209. except AttributeError:
  210. return weight # don't validate weight further if category failed
  211. if weight > self.category.acl.get('can_pin_threads', 0):
  212. if weight == 2:
  213. raise ValidationError(
  214. _("You don't have permission to pin threads globally in this category.")
  215. )
  216. else:
  217. raise ValidationError(
  218. _("You don't have permission to pin threads in this category.")
  219. )
  220. return weight
  221. def validate_is_hidden(self, is_hidden):
  222. try:
  223. add_acl(self.context['user'], self.category)
  224. except AttributeError:
  225. return is_hidden # don't validate hidden further if category failed
  226. if is_hidden and not self.category.acl.get('can_hide_threads'):
  227. raise ValidationError(
  228. _("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 DeleteThreadsSerializer(serializers.Serializer):
  280. error_empty_or_required = ugettext_lazy("You have to specify at least one thread to delete.")
  281. threads = serializers.ListField(
  282. allow_empty=False,
  283. child=serializers.IntegerField(
  284. error_messages={
  285. 'invalid': ugettext_lazy("One or more thread ids received were invalid."),
  286. },
  287. ),
  288. error_messages={
  289. 'required': error_empty_or_required,
  290. 'null': error_empty_or_required,
  291. 'empty': error_empty_or_required,
  292. },
  293. )
  294. def validate_threads(self, data):
  295. if len(data) > THREADS_LIMIT:
  296. message = ungettext(
  297. "No more than %(limit)s thread can be deleted at single time.",
  298. "No more than %(limit)s threads can be deleted at single time.",
  299. THREADS_LIMIT,
  300. )
  301. raise ValidationError(message % {'limit': THREADS_LIMIT})
  302. request = self.context['request']
  303. viewmodel = self.context['viewmodel']
  304. threads = []
  305. errors = []
  306. sorted_ids = sorted(data, reverse=True)
  307. for thread_id in sorted_ids:
  308. try:
  309. thread = viewmodel(request, thread_id).unwrap()
  310. allow_delete_thread(request.user, thread)
  311. threads.append(thread)
  312. except PermissionDenied as permission_error:
  313. errors.append({
  314. 'thread': {
  315. 'id': thread.id,
  316. 'title': thread.title
  317. },
  318. 'error': permission_error,
  319. })
  320. except Http404 as e:
  321. pass # skip invisible threads
  322. if errors:
  323. raise ValidationError({'details': errors})
  324. if len(threads) != len(data):
  325. raise ValidationError(_("One or more threads to delete could not be found."))
  326. return threads
  327. class MergeThreadSerializer(serializers.Serializer):
  328. other_thread = serializers.CharField(
  329. error_messages={
  330. 'required': ugettext_lazy("Enter link to new thread."),
  331. },
  332. )
  333. best_answer = serializers.IntegerField(
  334. required=False,
  335. error_messages={
  336. 'invalid': ugettext_lazy("Invalid choice."),
  337. },
  338. )
  339. poll = serializers.IntegerField(
  340. required=False,
  341. error_messages={
  342. 'invalid': ugettext_lazy("Invalid choice."),
  343. },
  344. )
  345. def validate_other_thread(self, data):
  346. request = self.context['request']
  347. thread = self.context['thread']
  348. viewmodel = self.context['viewmodel']
  349. other_thread_id = get_thread_id_from_url(request, data)
  350. if not other_thread_id:
  351. raise ValidationError(_("This is not a valid thread link."))
  352. if other_thread_id == thread.pk:
  353. raise ValidationError(_("You can't merge thread with itself."))
  354. try:
  355. other_thread = viewmodel(request, other_thread_id).unwrap()
  356. allow_merge_thread(request.user, other_thread, otherthread=True)
  357. except PermissionDenied as e:
  358. raise ValidationError(e)
  359. except Http404:
  360. raise ValidationError(
  361. _(
  362. "The thread you have entered link to doesn't "
  363. "exist or you don't have permission to see it."
  364. )
  365. )
  366. if not can_reply_thread(request.user, other_thread):
  367. raise ValidationError(_("You can't merge this thread into thread you can't reply."))
  368. return other_thread
  369. def validate(self, data):
  370. thread = self.context['thread']
  371. other_thread = data['other_thread']
  372. merge_conflict = MergeConflict(data, [thread, other_thread])
  373. merge_conflict.is_valid(raise_exception=True)
  374. data.update(merge_conflict.get_resolution())
  375. self.merge_conflict = merge_conflict.get_conflicting_fields()
  376. return data
  377. class MergeThreadsSerializer(NewThreadSerializer):
  378. error_empty_or_required = ugettext_lazy("You have to select at least two threads to merge.")
  379. threads = serializers.ListField(
  380. allow_empty=False,
  381. min_length=2,
  382. child=serializers.IntegerField(
  383. error_messages={
  384. 'invalid': ugettext_lazy("One or more thread ids received were invalid."),
  385. },
  386. ),
  387. error_messages={
  388. 'empty': error_empty_or_required,
  389. 'null': error_empty_or_required,
  390. 'required': error_empty_or_required,
  391. 'min_length': error_empty_or_required,
  392. },
  393. )
  394. best_answer = serializers.IntegerField(
  395. required=False,
  396. error_messages={
  397. 'invalid': ugettext_lazy("Invalid choice."),
  398. },
  399. )
  400. poll = serializers.IntegerField(
  401. required=False,
  402. error_messages={
  403. 'invalid': ugettext_lazy("Invalid choice."),
  404. },
  405. )
  406. def validate_threads(self, data):
  407. if len(data) > THREADS_LIMIT:
  408. message = ungettext(
  409. "No more than %(limit)s thread can be merged at single time.",
  410. "No more than %(limit)s threads can be merged at single time.",
  411. POSTS_LIMIT,
  412. )
  413. raise ValidationError(message % {'limit': THREADS_LIMIT})
  414. return data
  415. def get_valid_threads(self, threads_ids):
  416. user = self.context['user']
  417. threads_tree_id = trees_map.get_tree_id_for_root(THREADS_ROOT_NAME)
  418. threads_queryset = Thread.objects.filter(
  419. id__in=threads_ids,
  420. category__tree_id=threads_tree_id,
  421. ).select_related('category').order_by('-id')
  422. invalid_threads = []
  423. valid_threads = []
  424. for thread in threads_queryset:
  425. add_acl(user, thread)
  426. if can_see_thread(user, thread):
  427. valid_threads.append(thread)
  428. try:
  429. allow_merge_thread(user, thread)
  430. except PermissionDenied as permission_error:
  431. invalid_threads.append({
  432. 'id': thread.id,
  433. 'status': 403,
  434. 'detail': permission_error
  435. })
  436. not_found_ids = set(threads_ids) - set([t.id for t in valid_threads])
  437. for not_found_id in not_found_ids:
  438. invalid_threads.append({
  439. 'id': not_found_id,
  440. 'status': 404,
  441. 'detail': _(
  442. "Requested thread doesn't exist or you don't have permission to see it."
  443. ),
  444. })
  445. if invalid_threads:
  446. invalid_threads.sort(key=lambda item: item['id'])
  447. raise ValidationError({'merge': invalid_threads})
  448. return valid_threads
  449. def validate(self, data):
  450. data['threads'] = self.get_valid_threads(data['threads'])
  451. merge_conflict = MergeConflict(data, data['threads'])
  452. merge_conflict.is_valid(raise_exception=True)
  453. data.update(merge_conflict.get_resolution())
  454. self.merge_conflict = merge_conflict.get_conflicting_fields()
  455. return data