apipatch.py 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. from django.core.exceptions import PermissionDenied
  2. from django.db import transaction
  3. from django.http import Http404
  4. from rest_framework.response import Response
  5. ALLOWED_OPS = ("add", "remove", "replace")
  6. class InvalidAction(Exception):
  7. pass
  8. class ApiPatch:
  9. def __init__(self):
  10. self._actions = []
  11. def add(self, path, handler):
  12. self._actions.append({"op": "add", "path": path, "handler": handler})
  13. def remove(self, path, handler):
  14. self._actions.append({"op": "remove", "path": path, "handler": handler})
  15. def replace(self, path, handler):
  16. self._actions.append({"op": "replace", "path": path, "handler": handler})
  17. def dispatch(self, request, target):
  18. if not isinstance(request.data, list):
  19. return Response(
  20. {"detail": "PATCH request should be list of operations"}, status=400
  21. )
  22. detail = []
  23. is_errored = False
  24. patch = {"id": target.pk}
  25. for action in request.data:
  26. try:
  27. self.validate_action(action)
  28. self.dispatch_action(patch, request, target, action)
  29. detail.append("ok")
  30. except Http404:
  31. is_errored = True
  32. detail.append("NOT FOUND")
  33. break
  34. except (InvalidAction, PermissionDenied) as e:
  35. is_errored = True
  36. detail.append(e.args[0])
  37. break
  38. patch["detail"] = detail
  39. if is_errored:
  40. return Response(patch, status=400)
  41. return Response(patch)
  42. def dispatch_bulk(self, request, targets):
  43. is_errored = False
  44. result = []
  45. for target in targets:
  46. detail = []
  47. patch = {"id": target.pk}
  48. for action in request.data["ops"]:
  49. try:
  50. self.validate_action(action)
  51. self.dispatch_action(patch, request, target, action)
  52. except Http404:
  53. is_errored = True
  54. detail.append("NOT FOUND")
  55. break
  56. except (InvalidAction, PermissionDenied) as e:
  57. is_errored = True
  58. detail.append(e.args[0])
  59. break
  60. if detail:
  61. patch["detail"] = detail
  62. result.append(patch)
  63. if is_errored:
  64. return Response(result, status=400)
  65. return Response(result)
  66. def validate_action(self, action):
  67. if not action.get("op"):
  68. raise InvalidAction("undefined op")
  69. if action.get("op") not in ALLOWED_OPS:
  70. raise InvalidAction('"%s" op is unsupported' % action.get("op"))
  71. if not action.get("path"):
  72. raise InvalidAction('"%s" op has to specify path' % action.get("op"))
  73. if "value" not in action:
  74. raise InvalidAction('"%s" op has to specify value' % action.get("op"))
  75. def dispatch_action(self, patch, request, target, action):
  76. for handler in self._actions:
  77. if action["op"] == handler["op"] and action["path"] == handler["path"]:
  78. with transaction.atomic():
  79. patch.update(handler["handler"](request, target, action["value"]))