123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155 |
- from __future__ import unicode_literals
- from rest_framework import serializers
- from rest_framework.response import Response
- from rest_framework.settings import api_settings
- from django.core.exceptions import PermissionDenied, ValidationError
- from django.db import transaction
- from django.http import Http404
- from django.utils import six
- from django.utils.translation import gettext as _
- ALLOWED_OPS = ('add', 'remove', 'replace')
- class InvalidAction(Exception):
- pass
- HANDLED_EXCEPTIONS = (
- serializers.ValidationError,
- ValidationError,
- InvalidAction,
- PermissionDenied,
- Http404,
- )
- class ApiPatch(object):
- def __init__(self):
- self._actions = []
- def add(self, path, handler):
- self._actions.append({
- 'op': 'add',
- 'path': path,
- 'handler': handler,
- })
- def remove(self, path, handler):
- self._actions.append({
- 'op': 'remove',
- 'path': path,
- 'handler': handler,
- })
- def replace(self, path, handler):
- self._actions.append({
- 'op': 'replace',
- 'path': path,
- 'handler': handler,
- })
- def dispatch(self, request, target):
- response = {'id': target.pk}
- try:
- self.validate_actions(request.data)
- except HANDLED_EXCEPTIONS as exception:
- return self.handle_exception(exception)
- for action in request.data:
- try:
- self.dispatch_action(response, request, target, action)
- except HANDLED_EXCEPTIONS as exception:
- return self.handle_exception(exception)
- return Response(response)
- def dispatch_bulk(self, request, targets):
- try:
- self.validate_actions(request.data['ops'])
- except HANDLED_EXCEPTIONS as exception:
- return self.handle_exception(exception)
- result = []
- for target in targets:
- data = {'id': target.pk, 'status': 200, 'patch': {}}
- for action in request.data['ops']:
- try:
- self.dispatch_action(data['patch'], request, target, action)
- except HANDLED_EXCEPTIONS as exception:
- data, status = self.get_error_data_status(exception)
- data.update({'id': target.pk, 'status': status, })
- break
- result.append(data)
- # sort result items by id then cast id and status to string
- # so we are compliant with our bulk actions spec
- result.sort(key=lambda item: item['id'])
- for data in result:
- data['id'] = str(data['id'])
- data['status'] = str(data['status'])
- # 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_actions(self, actions):
- if not isinstance(actions, list):
- raise InvalidAction(_("PATCH request should be a list of operations."))
- reduced_actions = []
- for action in actions:
- self.validate_action(action)
- reduced_action = self.reduce_action(action)
- if reduced_action in reduced_actions:
- raise InvalidAction(
- _('"%(op)s" op for "%(path)s" path is repeated.') % reduced_action)
- reduced_actions.append(reduced_action)
- def validate_action(self, action):
- if not action.get('op'):
- raise InvalidAction(_('"op" parameter must be defined.'))
- if action.get('op') not in ALLOWED_OPS:
- raise InvalidAction(_('"%s" op is unsupported.') % action.get('op'))
- if not action.get('path'):
- raise InvalidAction(_('"%s" op has to specify path.') % action.get('op'))
- if 'value' not in action:
- raise InvalidAction(_('"%s" op has to specify value.') % action.get('op'))
- def reduce_action(self, action):
- return {'op': action['op'], 'path': action['path']}
- def dispatch_action(self, data, request, target, action):
- for handler in self._actions:
- if action['op'] == handler['op'] and action['path'] == handler['path']:
- with transaction.atomic():
- data.update(handler['handler'](request, target, action['value']))
- def handle_exception(self, exception):
- data, status = self.get_error_data_status(exception)
- return Response(data, status=status)
- def get_error_data_status(self, exception):
- if isinstance(exception, InvalidAction):
- return {api_settings.NON_FIELD_ERRORS_KEY: [six.text_type(exception)]}, 400
- if isinstance(exception, serializers.ValidationError):
- return {'value': exception.detail}, 400
- if isinstance(exception, ValidationError):
- return {'value': exception.messages}, 400
- if isinstance(exception, PermissionDenied):
- return {'detail': six.text_type(exception)}, 403
- if isinstance(exception, Http404):
- return {'detail': 'NOT FOUND'}, 404
|