moderation.py 19 KB

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