from django.core.exceptions import PermissionDenied from django.db import transaction from django.http import Http404 from rest_framework.response import Response ALLOWED_OPS = ("add", "remove", "replace") class InvalidAction(Exception): pass 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): if not isinstance(request.data, list): return Response( {"detail": "PATCH request should be list of operations"}, status=400 ) detail = [] is_errored = False patch = {"id": target.pk} for action in request.data: try: 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) def dispatch_bulk(self, request, targets): is_errored = False result = [] for target in targets: detail = [] patch = {"id": target.pk} for action in request.data["ops"]: try: self.validate_action(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]) break if detail: patch["detail"] = detail result.append(patch) if is_errored: return Response(result, status=400) else: return Response(result) def validate_action(self, action): if not action.get("op"): raise InvalidAction("undefined op") 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 dispatch_action(self, patch, request, target, action): for handler in self._actions: if action["op"] == handler["op"] and action["path"] == handler["path"]: with transaction.atomic(): patch.update(handler["handler"](request, target, action["value"]))