patch.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  1. from rest_framework import serializers
  2. from rest_framework.response import Response
  3. from django.contrib.auth import get_user_model
  4. from django.core.exceptions import PermissionDenied, ValidationError
  5. from django.http import Http404
  6. from django.shortcuts import get_object_or_404
  7. from django.utils import six
  8. from django.utils.translation import ugettext as _
  9. from misago.acl import add_acl
  10. from misago.api.patch import ApiPatch
  11. from misago.categories.models import Category
  12. from misago.categories.permissions import allow_browse_category, allow_see_category
  13. from misago.categories.serializers import CategorySerializer
  14. from misago.conf import settings
  15. from misago.core.shortcuts import get_int_or_404
  16. from misago.threads.moderation import threads as moderation
  17. from misago.threads.participants import (
  18. add_participant, change_owner, make_participants_aware, remove_participant)
  19. from misago.threads.permissions import (
  20. allow_add_participant, allow_add_participants, allow_approve_thread, allow_change_owner, allow_edit_thread,
  21. allow_pin_thread, allow_hide_thread, allow_move_thread, allow_remove_participant, allow_start_thread,
  22. allow_unhide_thread)
  23. from misago.threads.serializers import ThreadParticipantSerializer
  24. from misago.threads.validators import validate_title
  25. PATCH_LIMIT = settings.MISAGO_THREADS_PER_PAGE + settings.MISAGO_THREADS_TAIL
  26. UserModel = get_user_model()
  27. thread_patch_dispatcher = ApiPatch()
  28. def patch_acl(request, thread, value):
  29. """useful little op that updates thread acl to current state"""
  30. if value:
  31. add_acl(request.user, thread)
  32. return {'acl': thread.acl}
  33. else:
  34. return {'acl': None}
  35. thread_patch_dispatcher.add('acl', patch_acl)
  36. def patch_title(request, thread, value):
  37. try:
  38. value_cleaned = six.text_type(value).strip()
  39. except (TypeError, ValueError):
  40. raise ValidationError(_("Invalid thread title."))
  41. validate_title(value_cleaned)
  42. allow_edit_thread(request.user, thread)
  43. moderation.change_thread_title(request, thread, value_cleaned)
  44. return {'title': thread.title}
  45. thread_patch_dispatcher.replace('title', patch_title)
  46. def patch_weight(request, thread, value):
  47. allow_pin_thread(request.user, thread)
  48. if not thread.acl.get('can_pin_globally') and thread.weight == 2:
  49. raise PermissionDenied(_("You can't change globally pinned threads weights in this category."))
  50. if value == 2:
  51. if thread.acl.get('can_pin_globally'):
  52. moderation.pin_thread_globally(request, thread)
  53. else:
  54. raise PermissionDenied(_("You can't pin threads globally in this category."))
  55. elif value == 1:
  56. moderation.pin_thread_locally(request, thread)
  57. elif value == 0:
  58. moderation.unpin_thread(request, thread)
  59. return {'weight': thread.weight}
  60. thread_patch_dispatcher.replace('weight', patch_weight)
  61. def patch_move(request, thread, value):
  62. allow_move_thread(request.user, thread)
  63. category_pk = get_int_or_404(value)
  64. new_category = get_object_or_404(
  65. Category.objects.all_categories().select_related('parent'), pk=category_pk
  66. )
  67. add_acl(request.user, new_category)
  68. allow_see_category(request.user, new_category)
  69. allow_browse_category(request.user, new_category)
  70. allow_start_thread(request.user, new_category)
  71. if new_category == thread.category:
  72. raise ValidationError(_("You can't move thread to the category it's already in."))
  73. moderation.move_thread(request, thread, new_category)
  74. return {'category': CategorySerializer(new_category).data}
  75. thread_patch_dispatcher.replace('category', patch_move)
  76. def patch_flatten_categories(request, thread, value):
  77. try:
  78. return {'category': thread.category_id}
  79. except AttributeError:
  80. return {'category': thread.category_id}
  81. thread_patch_dispatcher.replace('flatten-categories', patch_flatten_categories)
  82. def patch_is_unapproved(request, thread, value):
  83. allow_approve_thread(request.user, thread)
  84. if value:
  85. raise PermissionDenied(_("Content approval can't be reversed."))
  86. moderation.approve_thread(request, thread)
  87. return {
  88. 'is_unapproved': thread.is_unapproved,
  89. 'has_unapproved_posts': thread.has_unapproved_posts,
  90. }
  91. thread_patch_dispatcher.replace('is-unapproved', patch_is_unapproved)
  92. def patch_is_closed(request, thread, value):
  93. if thread.acl.get('can_close'):
  94. if value:
  95. moderation.close_thread(request, thread)
  96. else:
  97. moderation.open_thread(request, thread)
  98. return {'is_closed': thread.is_closed}
  99. else:
  100. if value:
  101. raise PermissionDenied(_("You don't have permission to close this thread."))
  102. else:
  103. raise PermissionDenied(_("You don't have permission to open this thread."))
  104. thread_patch_dispatcher.replace('is-closed', patch_is_closed)
  105. def patch_is_hidden(request, thread, value):
  106. if value:
  107. allow_hide_thread(request.user, thread)
  108. moderation.hide_thread(request, thread)
  109. else:
  110. allow_unhide_thread(request.user, thread)
  111. moderation.unhide_thread(request, thread)
  112. return {'is_hidden': thread.is_hidden}
  113. thread_patch_dispatcher.replace('is-hidden', patch_is_hidden)
  114. def patch_subscription(request, thread, value):
  115. request.user.subscription_set.filter(thread=thread).delete()
  116. if value == 'notify':
  117. thread.subscription = request.user.subscription_set.create(
  118. thread=thread,
  119. category=thread.category,
  120. last_read_on=thread.last_post_on,
  121. send_email=False,
  122. )
  123. return {'subscription': False}
  124. elif value == 'email':
  125. thread.subscription = request.user.subscription_set.create(
  126. thread=thread,
  127. category=thread.category,
  128. last_read_on=thread.last_post_on,
  129. send_email=True,
  130. )
  131. return {'subscription': True}
  132. else:
  133. return {'subscription': None}
  134. thread_patch_dispatcher.replace('subscription', patch_subscription)
  135. def patch_add_participant(request, thread, value):
  136. allow_add_participants(request.user, thread)
  137. try:
  138. username = six.text_type(value).strip().lower()
  139. if not username:
  140. raise ValidationError(_("You have to enter new participant's username."))
  141. participant = UserModel.objects.get(slug=username)
  142. except UserModel.DoesNotExist:
  143. raise ValidationError(_("No user with such name exists."))
  144. if participant in [p.user for p in thread.participants_list]:
  145. raise ValidationError(_("This user is already thread participant."))
  146. allow_add_participant(request.user, participant)
  147. add_participant(request, thread, participant)
  148. make_participants_aware(request.user, thread)
  149. participants = ThreadParticipantSerializer(thread.participants_list, many=True)
  150. return {'participants': participants.data}
  151. thread_patch_dispatcher.add('participants', patch_add_participant)
  152. def patch_remove_participant(request, thread, value):
  153. try:
  154. user_id = int(value)
  155. except (ValueError, TypeError):
  156. user_id = 0
  157. for participant in thread.participants_list:
  158. if participant.user_id == user_id:
  159. break
  160. else:
  161. raise ValidationError(_("Participant doesn't exist."))
  162. allow_remove_participant(request.user, thread, participant.user)
  163. remove_participant(request, thread, participant.user)
  164. if len(thread.participants_list) == 1:
  165. return {'deleted': True}
  166. else:
  167. make_participants_aware(request.user, thread)
  168. participants = ThreadParticipantSerializer(thread.participants_list, many=True)
  169. return {
  170. 'deleted': False,
  171. 'participants': participants.data,
  172. }
  173. thread_patch_dispatcher.remove('participants', patch_remove_participant)
  174. def patch_replace_owner(request, thread, value):
  175. try:
  176. user_id = int(value)
  177. except (ValueError, TypeError):
  178. user_id = 0
  179. for participant in thread.participants_list:
  180. if participant.user_id == user_id:
  181. if participant.is_owner:
  182. raise ValidationError(_("This user already is thread owner."))
  183. else:
  184. break
  185. else:
  186. raise ValidationError(_("Participant doesn't exist."))
  187. allow_change_owner(request.user, thread)
  188. change_owner(request, thread, participant.user)
  189. make_participants_aware(request.user, thread)
  190. participants = ThreadParticipantSerializer(thread.participants_list, many=True)
  191. return {'participants': participants.data}
  192. thread_patch_dispatcher.replace('owner', patch_replace_owner)
  193. def thread_patch_endpoint(request, thread):
  194. old_title = thread.title
  195. old_is_hidden = thread.is_hidden
  196. old_is_unapproved = thread.is_unapproved
  197. old_category = thread.category
  198. response = thread_patch_dispatcher.dispatch(request, thread)
  199. # diff thread's state against pre-patch and resync category if necessary
  200. hidden_changed = old_is_hidden != thread.is_hidden
  201. unapproved_changed = old_is_unapproved != thread.is_unapproved
  202. category_changed = old_category != thread.category
  203. title_changed = old_title != thread.title
  204. if thread.category.last_thread_id != thread.pk:
  205. title_changed = False # don't trigger resync on simple title change
  206. if hidden_changed or unapproved_changed or category_changed:
  207. thread.category.synchronize()
  208. thread.category.save()
  209. if category_changed:
  210. old_category.synchronize()
  211. old_category.save()
  212. elif title_changed:
  213. thread.category.last_thread_title = thread.title
  214. thread.category.last_thread_slug = thread.slug
  215. thread.category.save(update_fields=['last_thread_title', 'last_thread_slug'])
  216. return response
  217. def bulk_patch_endpoint(request, viewmodel):
  218. serializer = BulkPatchSerializer(data=request.data)
  219. if not serializer.is_valid():
  220. return Response(serializer.errors, status=400)
  221. threads = clean_threads_for_patch(request, viewmodel, serializer.data['ids'])
  222. old_titles = [t.title for t in threads]
  223. old_is_hidden = [t.is_hidden for t in threads]
  224. old_is_unapproved = [t.is_unapproved for t in threads]
  225. old_category = [t.category_id for t in threads]
  226. response = thread_patch_dispatcher.dispatch_bulk(request, threads)
  227. new_titles = [t.title for t in threads]
  228. new_is_hidden = [t.is_hidden for t in threads]
  229. new_is_unapproved = [t.is_unapproved for t in threads]
  230. new_category = [t.category_id for t in threads]
  231. # sync titles
  232. if new_titles != old_titles:
  233. for i, t in enumerate(threads):
  234. if t.title != old_titles[i] and t.category.last_thread_id == t.pk:
  235. t.category.last_thread_title = t.title
  236. t.category.last_thread_slug = t.slug
  237. t.category.save(update_fields=['last_thread_title', 'last_thread_slug'])
  238. # sync categories
  239. sync_categories = []
  240. if new_is_hidden != old_is_hidden:
  241. for i, t in enumerate(threads):
  242. if t.is_hidden != old_is_hidden[i] and t.category_id not in sync_categories:
  243. sync_categories.append(t.category_id)
  244. if new_is_unapproved != old_is_unapproved:
  245. for i, t in enumerate(threads):
  246. if t.is_unapproved != old_is_unapproved[i] and t.category_id not in sync_categories:
  247. sync_categories.append(t.category_id)
  248. if new_category != old_category:
  249. for i, t in enumerate(threads):
  250. if t.category_id != old_category[i]:
  251. if t.category_id not in sync_categories:
  252. sync_categories.append(t.category_id)
  253. if old_category[i] not in sync_categories:
  254. sync_categories.append(old_category[i])
  255. if sync_categories:
  256. for category in Category.objects.filter(id__in=sync_categories):
  257. category.synchronize()
  258. category.save()
  259. return response
  260. def clean_threads_for_patch(request, viewmodel, threads_ids):
  261. threads = []
  262. for thread_id in sorted(set(threads_ids), reverse=True):
  263. threads.append(viewmodel(request, thread_id).unwrap())
  264. return threads
  265. class BulkPatchSerializer(serializers.Serializer):
  266. ids = serializers.ListField(
  267. child=serializers.IntegerField(min_value=1),
  268. max_length=PATCH_LIMIT,
  269. min_length=1,
  270. )
  271. ops = serializers.ListField(
  272. child=serializers.DictField(),
  273. min_length=1,
  274. max_length=10,
  275. )