patch.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. from __future__ import unicode_literals
  2. from rest_framework import serializers
  3. from rest_framework.response import Response
  4. from rest_framework.settings import api_settings
  5. from django.core.exceptions import PermissionDenied, ValidationError
  6. from django.db import transaction
  7. from django.http import Http404
  8. from django.utils import six
  9. from django.utils.translation import gettext as _
  10. ALLOWED_OPS = ('add', 'remove', 'replace')
  11. class InvalidAction(Exception):
  12. pass
  13. HANDLED_EXCEPTIONS = (
  14. serializers.ValidationError,
  15. ValidationError,
  16. InvalidAction,
  17. PermissionDenied,
  18. Http404,
  19. )
  20. class ApiPatch(object):
  21. def __init__(self):
  22. self._actions = []
  23. def add(self, path, handler):
  24. self._actions.append({
  25. 'op': 'add',
  26. 'path': path,
  27. 'handler': handler,
  28. })
  29. def remove(self, path, handler):
  30. self._actions.append({
  31. 'op': 'remove',
  32. 'path': path,
  33. 'handler': handler,
  34. })
  35. def replace(self, path, handler):
  36. self._actions.append({
  37. 'op': 'replace',
  38. 'path': path,
  39. 'handler': handler,
  40. })
  41. def dispatch(self, request, target):
  42. response = {'id': target.pk}
  43. try:
  44. self.validate_actions(request.data)
  45. except HANDLED_EXCEPTIONS as exception:
  46. return self.handle_exception(exception)
  47. for action in request.data:
  48. try:
  49. self.dispatch_action(response, request, target, action)
  50. except HANDLED_EXCEPTIONS as exception:
  51. return self.handle_exception(exception)
  52. return Response(response)
  53. def dispatch_bulk(self, request, targets):
  54. try:
  55. self.validate_actions(request.data['ops'])
  56. except HANDLED_EXCEPTIONS as exception:
  57. return self.handle_exception(exception)
  58. result = []
  59. for target in targets:
  60. data = {'id': target.pk, 'status': 200, 'patch': {}}
  61. for action in request.data['ops']:
  62. try:
  63. self.dispatch_action(data['patch'], request, target, action)
  64. except HANDLED_EXCEPTIONS as exception:
  65. data, status = self.get_error_data_status(exception)
  66. data.update({'id': target.pk, 'status': status, })
  67. break
  68. result.append(data)
  69. # sort result items by id then cast id and status to string
  70. # so we are compliant with our bulk actions spec
  71. result.sort(key=lambda item: item['id'])
  72. for data in result:
  73. data['id'] = str(data['id'])
  74. data['status'] = str(data['status'])
  75. # always returning 200 on op error saves us logic duplication
  76. # in the frontend, were we need to do success handling in both
  77. # success and error handles
  78. return Response(result)
  79. def validate_actions(self, actions):
  80. if not isinstance(actions, list):
  81. raise InvalidAction(_("PATCH request should be a list of operations."))
  82. reduced_actions = []
  83. for action in actions:
  84. self.validate_action(action)
  85. reduced_action = self.reduce_action(action)
  86. if reduced_action in reduced_actions:
  87. raise InvalidAction(
  88. _('"%(op)s" op for "%(path)s" path is repeated.') % reduced_action)
  89. reduced_actions.append(reduced_action)
  90. def validate_action(self, action):
  91. if not action.get('op'):
  92. raise InvalidAction(_('"op" parameter must be defined.'))
  93. if action.get('op') not in ALLOWED_OPS:
  94. raise InvalidAction(_('"%s" op is unsupported.') % action.get('op'))
  95. if not action.get('path'):
  96. raise InvalidAction(_('"%s" op has to specify path.') % action.get('op'))
  97. if 'value' not in action:
  98. raise InvalidAction(_('"%s" op has to specify value.') % action.get('op'))
  99. def reduce_action(self, action):
  100. return {'op': action['op'], 'path': action['path']}
  101. def dispatch_action(self, data, request, target, action):
  102. for handler in self._actions:
  103. if action['op'] == handler['op'] and action['path'] == handler['path']:
  104. with transaction.atomic():
  105. data.update(handler['handler'](request, target, action['value']))
  106. def handle_exception(self, exception):
  107. data, status = self.get_error_data_status(exception)
  108. return Response(data, status=status)
  109. def get_error_data_status(self, exception):
  110. if isinstance(exception, InvalidAction):
  111. return {api_settings.NON_FIELD_ERRORS_KEY: [six.text_type(exception)]}, 400
  112. if isinstance(exception, serializers.ValidationError):
  113. return {'value': exception.detail}, 400
  114. if isinstance(exception, ValidationError):
  115. return {'value': exception.messages}, 400
  116. if isinstance(exception, PermissionDenied):
  117. return {'detail': six.text_type(exception)}, 403
  118. if isinstance(exception, Http404):
  119. return {'detail': 'NOT FOUND'}, 404