patch.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  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. patch = {'id': target.pk, 'status': 200}
  64. for action in request.data['ops']:
  65. try:
  66. self.dispatch_action(patch, request, target, action)
  67. except HANDLED_EXCEPTIONS as exception:
  68. detail, status = self.get_error_detail_code(exception)
  69. patch = {
  70. 'id': target.pk,
  71. 'detail': detail,
  72. 'status': status,
  73. }
  74. break
  75. result.append(patch)
  76. # always returning 200 on op error saves us logic duplication
  77. # in the frontend, were we need to do success handling in both
  78. # success and error handles
  79. return Response(result)
  80. def validate_action(self, action):
  81. if not action.get('op'):
  82. raise InvalidAction(_('"op" parameter must be defined.'))
  83. if action.get('op') not in ALLOWED_OPS:
  84. raise InvalidAction(_('"%s" op is unsupported.') % action.get('op'))
  85. if not action.get('path'):
  86. raise InvalidAction(_('"%s" op has to specify path.') % action.get('op'))
  87. if 'value' not in action:
  88. raise InvalidAction(_('"%s" op has to specify value.') % action.get('op'))
  89. def dispatch_action(self, patch, request, target, action):
  90. for handler in self._actions:
  91. if action['op'] == handler['op'] and action['path'] == handler['path']:
  92. with transaction.atomic():
  93. patch.update(handler['handler'](request, target, action['value']))
  94. def get_error_detail_code(self, exception):
  95. if isinstance(exception, InvalidAction):
  96. return six.text_type(exception), 400
  97. if isinstance(exception, ApiValidationError):
  98. return exception.detail, 400
  99. if isinstance(exception, ValidationError):
  100. return exception.messages, 400
  101. if isinstance(exception, PermissionDenied):
  102. return six.text_type(exception), 403
  103. if isinstance(exception, Http404):
  104. return six.text_type(exception) or "NOT FOUND", 404