feat: run_doc_method v2

This commit is contained in:
Ankush Menat 2023-09-05 16:57:58 +05:30
parent d117e2c08b
commit 232f080044
2 changed files with 73 additions and 8 deletions

View file

@ -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),

View file

@ -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.