moderation.py 19 KB

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