feat: run_doc_method v2
This commit is contained in:
parent
d117e2c08b
commit
232f080044
2 changed files with 73 additions and 8 deletions
|
|
@ -8,11 +8,12 @@ Note:
|
|||
internal implementation can change without treating it as "breaking change".
|
||||
"""
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from werkzeug.routing import Rule
|
||||
|
||||
import frappe
|
||||
from frappe import _, is_whitelisted
|
||||
from frappe import _, get_newargs, is_whitelisted
|
||||
from frappe.core.doctype.server_script.server_script_utils import get_server_script_map
|
||||
from frappe.handler import is_valid_http_method, run_server_script
|
||||
from frappe.utils.data import sbool
|
||||
|
|
@ -122,8 +123,47 @@ def execute_doc_method(doctype: str, name: str, method: str | None = None):
|
|||
return doc.run_method(method, **frappe.form_dict)
|
||||
|
||||
|
||||
def run_doc_method(method: str, document: dict[str, Any] | str, kwargs=None):
|
||||
"""run a whitelisted controller method on in-memory document.
|
||||
|
||||
|
||||
This is useful for building clients that don't necessarily encode all the business logic but
|
||||
call server side function on object to validate and modify the doc.
|
||||
|
||||
The doc CAN exists in DB too and can write to DB as well if method is POST.
|
||||
"""
|
||||
|
||||
if isinstance(document, str):
|
||||
document = frappe.parse_json(document)
|
||||
|
||||
if kwargs is None:
|
||||
kwargs = {}
|
||||
|
||||
doc = frappe.get_doc(document)
|
||||
doc._original_modified = doc.modified
|
||||
doc.check_if_latest()
|
||||
|
||||
if not doc.has_permission("read"):
|
||||
raise frappe.PermissionError
|
||||
|
||||
method_obj = getattr(doc, method)
|
||||
fn = getattr(method_obj, "__func__", method_obj)
|
||||
is_whitelisted(fn)
|
||||
is_valid_http_method(fn)
|
||||
|
||||
new_kwargs = get_newargs(fn, kwargs)
|
||||
response = doc.run_method(method, **new_kwargs)
|
||||
frappe.response.docs.append(doc) # send modified document and result both.
|
||||
return response
|
||||
|
||||
|
||||
url_rules = [
|
||||
Rule("/method/<method>", endpoint=handle_rpc_call),
|
||||
Rule(
|
||||
"/method/run_doc_method",
|
||||
methods=["GET", "POST"],
|
||||
endpoint=lambda: frappe.call(run_doc_method, **frappe.form_dict),
|
||||
),
|
||||
Rule("/method/<doctype>/<method>", endpoint=handle_rpc_call),
|
||||
Rule("/document/<doctype>", methods=["GET"], endpoint=document_list),
|
||||
Rule("/document/<doctype>", methods=["POST"], endpoint=create_doc),
|
||||
|
|
|
|||
|
|
@ -215,7 +215,6 @@ class TestResourceAPI(FrappeAPITestCase):
|
|||
|
||||
@parameterize("", "v1", "v2")
|
||||
def test_create_document(self):
|
||||
# test 7: POST method on {self.resource_path} to create doc
|
||||
data = {"description": frappe.mock("paragraph"), "sid": self.sid}
|
||||
response = self.post(self.resource_path(self.DOCTYPE), data)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
|
@ -225,7 +224,6 @@ class TestResourceAPI(FrappeAPITestCase):
|
|||
|
||||
@parameterize("", "v1", "v2")
|
||||
def test_update_document(self):
|
||||
# test 8: PUT method on {self.resource_path} to update doc
|
||||
generated_desc = frappe.mock("paragraph")
|
||||
data = {"description": generated_desc, "sid": self.sid}
|
||||
random_doc = choice(self.GENERATED_DOCUMENTS)
|
||||
|
|
@ -239,7 +237,6 @@ class TestResourceAPI(FrappeAPITestCase):
|
|||
|
||||
@parameterize("", "v1", "v2")
|
||||
def test_delete_document(self):
|
||||
# test 9: DELETE method on {self.resource_path}
|
||||
doc_to_delete = choice(self.GENERATED_DOCUMENTS)
|
||||
response = self.delete(self.resource_path(self.DOCTYPE, doc_to_delete))
|
||||
self.assertEqual(response.status_code, 202)
|
||||
|
|
@ -331,22 +328,23 @@ class TestResourceAPIV2(FrappeAPITestCase):
|
|||
class TestMethodAPIV2(FrappeAPITestCase):
|
||||
version = "v2"
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.post(self.method_path("login"), {"sid": self.sid})
|
||||
return super().setUp()
|
||||
|
||||
def test_ping(self):
|
||||
# test 2: test for /api/method/ping
|
||||
response = self.get(self.method_path("ping"))
|
||||
response = self.get(self.method_path("frappe.ping"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIsInstance(response.json, dict)
|
||||
self.assertEqual(response.json["data"], "pong")
|
||||
|
||||
def test_get_user_info(self):
|
||||
# test 3: test for /api/method/frappe.realtime.get_user_info
|
||||
response = self.get(self.method_path("frappe.realtime.get_user_info"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIsInstance(response.json, dict)
|
||||
self.assertIn(response.json.get("data").get("user"), ("Administrator", "Guest"))
|
||||
|
||||
def test_auth_cycle(self):
|
||||
# test 4: Pass authorization token in request
|
||||
global authorization_token
|
||||
|
||||
generate_admin_keys()
|
||||
|
|
@ -375,6 +373,33 @@ class TestMethodAPIV2(FrappeAPITestCase):
|
|||
)
|
||||
self.assertEqual(expanded_response.data, shorthand_response.data)
|
||||
|
||||
def test_run_doc_method_in_memory(self):
|
||||
dns = frappe.get_doc("Document Naming Settings")
|
||||
|
||||
# Check that simple API can be called.
|
||||
response = self.get(
|
||||
self.method_path("run_doc_method"),
|
||||
{
|
||||
"sid": self.sid,
|
||||
"document": dns.as_dict(),
|
||||
"method": "get_transactions_and_prefixes",
|
||||
},
|
||||
)
|
||||
self.assertTrue(response.json["data"])
|
||||
self.assertGreaterEqual(len(response.json["docs"]), 1)
|
||||
|
||||
# Call with known and unknown arguments, only known should get passed
|
||||
response = self.get(
|
||||
self.method_path("run_doc_method"),
|
||||
{
|
||||
"sid": self.sid,
|
||||
"document": dns.as_dict(),
|
||||
"method": "get_options",
|
||||
"kwargs": {"doctype": "Webhook", "unknown": "what"},
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class TestReadOnlyMode(FrappeAPITestCase):
|
||||
"""During migration if read only mode can be enabled.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue