|
@@ -1,8 +1,13 @@
|
|
|
|
+from __future__ import unicode_literals
|
|
|
|
+
|
|
|
|
+from rest_framework.exceptions import ValidationError as ApiValidationError
|
|
from rest_framework.response import Response
|
|
from rest_framework.response import Response
|
|
|
|
|
|
-from django.core.exceptions import PermissionDenied
|
|
|
|
|
|
+from django.core.exceptions import PermissionDenied, ValidationError
|
|
from django.db import transaction
|
|
from django.db import transaction
|
|
from django.http import Http404
|
|
from django.http import Http404
|
|
|
|
+from django.utils import six
|
|
|
|
+from django.utils.translation import gettext as _
|
|
|
|
|
|
|
|
|
|
ALLOWED_OPS = ('add', 'remove', 'replace')
|
|
ALLOWED_OPS = ('add', 'remove', 'replace')
|
|
@@ -12,6 +17,15 @@ class InvalidAction(Exception):
|
|
pass
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
+HANDLED_EXCEPTIONS = (
|
|
|
|
+ ApiValidationError,
|
|
|
|
+ ValidationError,
|
|
|
|
+ InvalidAction,
|
|
|
|
+ PermissionDenied,
|
|
|
|
+ Http404,
|
|
|
|
+)
|
|
|
|
+
|
|
|
|
+
|
|
class ApiPatch(object):
|
|
class ApiPatch(object):
|
|
def __init__(self):
|
|
def __init__(self):
|
|
self._actions = []
|
|
self._actions = []
|
|
@@ -40,77 +54,81 @@ class ApiPatch(object):
|
|
def dispatch(self, request, target):
|
|
def dispatch(self, request, target):
|
|
if not isinstance(request.data, list):
|
|
if not isinstance(request.data, list):
|
|
return Response({
|
|
return Response({
|
|
- 'detail': "PATCH request should be list of operations",
|
|
|
|
|
|
+ 'detail': _("PATCH request should be list of operations."),
|
|
}, status=400)
|
|
}, status=400)
|
|
|
|
|
|
- detail = []
|
|
|
|
- is_errored = False
|
|
|
|
-
|
|
|
|
- patch = {'id': target.pk}
|
|
|
|
|
|
+ response = {'id': target.pk}
|
|
for action in request.data:
|
|
for action in request.data:
|
|
try:
|
|
try:
|
|
self.validate_action(action)
|
|
self.validate_action(action)
|
|
- self.dispatch_action(patch, request, target, action)
|
|
|
|
- detail.append('ok')
|
|
|
|
- except Http404:
|
|
|
|
- is_errored = True
|
|
|
|
- detail.append('NOT FOUND')
|
|
|
|
- break
|
|
|
|
- except (InvalidAction, PermissionDenied) as e:
|
|
|
|
- is_errored = True
|
|
|
|
- detail.append(e.args[0])
|
|
|
|
- break
|
|
|
|
-
|
|
|
|
- patch['detail'] = detail
|
|
|
|
- if is_errored:
|
|
|
|
- return Response(patch, status=400)
|
|
|
|
- else:
|
|
|
|
- return Response(patch)
|
|
|
|
|
|
+ self.dispatch_action(response, request, target, action)
|
|
|
|
+ except HANDLED_EXCEPTIONS as exception:
|
|
|
|
+ detail, status = self.get_error_detail_code(exception)
|
|
|
|
+ return Response({'detail': detail}, status=status)
|
|
|
|
+
|
|
|
|
+ return Response(response)
|
|
|
|
|
|
def dispatch_bulk(self, request, targets):
|
|
def dispatch_bulk(self, request, targets):
|
|
- is_errored = False
|
|
|
|
result = []
|
|
result = []
|
|
|
|
|
|
- for target in targets:
|
|
|
|
- detail = []
|
|
|
|
|
|
+ for action in request.data['ops']:
|
|
|
|
+ try:
|
|
|
|
+ self.validate_action(action)
|
|
|
|
+ except InvalidAction as exception:
|
|
|
|
+ detail, status = self.get_error_detail_code(exception)
|
|
|
|
+ return Response({'detail': detail}, status=status)
|
|
|
|
|
|
- patch = {'id': target.pk}
|
|
|
|
|
|
+ for target in targets:
|
|
|
|
+ patch = {'id': target.pk, 'status': 200}
|
|
for action in request.data['ops']:
|
|
for action in request.data['ops']:
|
|
try:
|
|
try:
|
|
- self.validate_action(action)
|
|
|
|
self.dispatch_action(patch, request, target, action)
|
|
self.dispatch_action(patch, request, target, action)
|
|
- except Http404:
|
|
|
|
- is_errored = True
|
|
|
|
- detail.append('NOT FOUND')
|
|
|
|
- break
|
|
|
|
- except (InvalidAction, PermissionDenied) as e:
|
|
|
|
- is_errored = True
|
|
|
|
- detail.append(e.args[0])
|
|
|
|
|
|
+ except HANDLED_EXCEPTIONS as exception:
|
|
|
|
+ detail, status = self.get_error_detail_code(exception)
|
|
|
|
+ patch = {
|
|
|
|
+ 'id': target.pk,
|
|
|
|
+ 'detail': detail,
|
|
|
|
+ 'status': status,
|
|
|
|
+ }
|
|
break
|
|
break
|
|
- if detail:
|
|
|
|
- patch['detail'] = detail
|
|
|
|
result.append(patch)
|
|
result.append(patch)
|
|
|
|
|
|
- if is_errored:
|
|
|
|
- return Response(result, status=400)
|
|
|
|
- else:
|
|
|
|
- return Response(result)
|
|
|
|
|
|
+ # always returning 200 on op error saves us logic duplication
|
|
|
|
+ # in the frontend, were we need to do success handling in both
|
|
|
|
+ # success and error handles
|
|
|
|
+ return Response(result)
|
|
|
|
|
|
def validate_action(self, action):
|
|
def validate_action(self, action):
|
|
if not action.get('op'):
|
|
if not action.get('op'):
|
|
- raise InvalidAction(u"undefined op")
|
|
|
|
|
|
+ raise InvalidAction(_('"op" parameter must be defined.'))
|
|
|
|
|
|
if action.get('op') not in ALLOWED_OPS:
|
|
if action.get('op') not in ALLOWED_OPS:
|
|
- raise InvalidAction(u'"%s" op is unsupported' % action.get('op'))
|
|
|
|
|
|
+ raise InvalidAction(_('"%s" op is unsupported.') % action.get('op'))
|
|
|
|
|
|
if not action.get('path'):
|
|
if not action.get('path'):
|
|
- raise InvalidAction(u'"%s" op has to specify path' % action.get('op'))
|
|
|
|
|
|
+ raise InvalidAction(_('"%s" op has to specify path.') % action.get('op'))
|
|
|
|
|
|
if 'value' not in action:
|
|
if 'value' not in action:
|
|
- raise InvalidAction(u'"%s" op has to specify value' % action.get('op'))
|
|
|
|
|
|
+ raise InvalidAction(_('"%s" op has to specify value.') % action.get('op'))
|
|
|
|
|
|
def dispatch_action(self, patch, request, target, action):
|
|
def dispatch_action(self, patch, request, target, action):
|
|
for handler in self._actions:
|
|
for handler in self._actions:
|
|
if action['op'] == handler['op'] and action['path'] == handler['path']:
|
|
if action['op'] == handler['op'] and action['path'] == handler['path']:
|
|
with transaction.atomic():
|
|
with transaction.atomic():
|
|
patch.update(handler['handler'](request, target, action['value']))
|
|
patch.update(handler['handler'](request, target, action['value']))
|
|
|
|
+
|
|
|
|
+ def get_error_detail_code(self, exception):
|
|
|
|
+ if isinstance(exception, InvalidAction):
|
|
|
|
+ return six.text_type(exception), 400
|
|
|
|
+
|
|
|
|
+ if isinstance(exception, ApiValidationError):
|
|
|
|
+ return exception.detail, 400
|
|
|
|
+
|
|
|
|
+ if isinstance(exception, ValidationError):
|
|
|
|
+ return exception.messages, 400
|
|
|
|
+
|
|
|
|
+ if isinstance(exception, PermissionDenied):
|
|
|
|
+ return six.text_type(exception), 403
|
|
|
|
+
|
|
|
|
+ if isinstance(exception, Http404):
|
|
|
|
+ return six.text_type(exception) or "NOT FOUND", 404
|