patch.py 4.6 KB

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