moderation.py 18 KB

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