moderation.py 19 KB

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