patch.py 11 KB

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