Merge pull request #34027 from sumitbhanushali/expand-link
feat: add support of expand in rest api v1/document_list
This commit is contained in:
commit
6dfcc2bf4c
4 changed files with 164 additions and 5 deletions
|
|
@ -4,6 +4,7 @@ from werkzeug.routing import Rule
|
|||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import attach_expanded_links
|
||||
from frappe.utils.data import sbool
|
||||
|
||||
|
||||
|
|
@ -11,6 +12,9 @@ def document_list(doctype: str):
|
|||
if frappe.form_dict.get("fields"):
|
||||
frappe.form_dict["fields"] = json.loads(frappe.form_dict["fields"])
|
||||
|
||||
if frappe.form_dict.get("expand"):
|
||||
frappe.form_dict["expand"] = json.loads(frappe.form_dict["expand"])
|
||||
|
||||
# set limit of records for frappe.get_list
|
||||
frappe.form_dict.setdefault(
|
||||
"limit_page_length",
|
||||
|
|
@ -74,7 +78,35 @@ def read_doc(doctype: str, name: str):
|
|||
doc = frappe.get_doc(doctype, name)
|
||||
doc.check_permission("read")
|
||||
doc.apply_fieldlevel_read_permissions()
|
||||
return doc
|
||||
doc_dict = doc.as_dict()
|
||||
if sbool(frappe.form_dict.get("expand_links")):
|
||||
get_values_for_link_and_dynamic_link_fields(doc_dict)
|
||||
get_values_for_table_and_multiselect_fields(doc_dict)
|
||||
|
||||
return doc_dict
|
||||
|
||||
|
||||
def get_values_for_link_and_dynamic_link_fields(doc_dict):
|
||||
meta = frappe.get_meta(doc_dict.doctype)
|
||||
link_fields = meta.get_link_fields() + meta.get_dynamic_link_fields()
|
||||
|
||||
for field in link_fields:
|
||||
if not (doc_fieldvalue := getattr(doc_dict, field.fieldname, None)):
|
||||
continue
|
||||
|
||||
doctype = field.options if field.fieldtype == "Link" else doc_dict.get(field.options)
|
||||
|
||||
link_doc = frappe.get_doc(doctype, doc_fieldvalue)
|
||||
doc_dict.update({field.fieldname: link_doc})
|
||||
|
||||
|
||||
def get_values_for_table_and_multiselect_fields(doc_dict):
|
||||
meta = frappe.get_meta(doc_dict.doctype)
|
||||
table_fields = meta.get_table_fields()
|
||||
|
||||
for field in table_fields:
|
||||
table_link_fieldnames = [f.fieldname for f in frappe.get_meta(field.options).get_link_fields()]
|
||||
attach_expanded_links(field.options, doc_dict.get(field.fieldname), table_link_fieldnames)
|
||||
|
||||
|
||||
def execute_doc_method(doctype: str, name: str, method: str | None = None):
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ from frappe import _
|
|||
from frappe.desk.reportview import validate_args
|
||||
from frappe.model.db_query import check_parent_permission
|
||||
from frappe.model.utils import is_virtual_doctype
|
||||
from frappe.utils import get_safe_filters
|
||||
from frappe.utils import attach_expanded_links, get_safe_filters
|
||||
from frappe.utils.caching import http_cache
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -37,6 +37,7 @@ def get_list(
|
|||
debug: bool = False,
|
||||
as_dict: bool = True,
|
||||
or_filters=None,
|
||||
expand=None,
|
||||
):
|
||||
"""Return a list of records by filters, fields, ordering and limit.
|
||||
|
||||
|
|
@ -64,7 +65,17 @@ def get_list(
|
|||
)
|
||||
|
||||
validate_args(args)
|
||||
return frappe.get_list(**args)
|
||||
_list = frappe.get_list(**args)
|
||||
|
||||
if not expand:
|
||||
return _list
|
||||
|
||||
if fields and not fields[0] == "*":
|
||||
expand = [f for f in expand if f in fields]
|
||||
|
||||
attach_expanded_links(doctype, _list, expand)
|
||||
|
||||
return _list
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
|
|
|||
|
|
@ -138,13 +138,24 @@ class FrappeAPITestCase(IntegrationTestCase):
|
|||
class TestResourceAPI(FrappeAPITestCase):
|
||||
DOCTYPE = "ToDo"
|
||||
GENERATED_DOCUMENTS: typing.ClassVar[list] = []
|
||||
TEST_USER = "test@restapi.com"
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.GENERATED_DOCUMENTS = []
|
||||
user = frappe.get_doc(
|
||||
{"doctype": "User", "email": cls.TEST_USER, "first_name": "Test User", "send_welcome_email": 0}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
for _ in range(20):
|
||||
doc = frappe.get_doc({"doctype": "ToDo", "description": frappe.mock("paragraph")}).insert()
|
||||
cls.GENERATED_DOCUMENTS = []
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "ToDo",
|
||||
"description": frappe.mock("paragraph"),
|
||||
"allocated_to": user.name,
|
||||
}
|
||||
).insert()
|
||||
cls.GENERATED_DOCUMENTS.append(doc.name)
|
||||
frappe.db.commit()
|
||||
|
||||
|
|
@ -153,6 +164,7 @@ class TestResourceAPI(FrappeAPITestCase):
|
|||
frappe.db.commit()
|
||||
for name in cls.GENERATED_DOCUMENTS:
|
||||
frappe.delete_doc_if_exists(cls.DOCTYPE, name)
|
||||
frappe.delete_doc_if_exists("User", cls.TEST_USER)
|
||||
frappe.db.commit()
|
||||
|
||||
def test_unauthorized_call(self):
|
||||
|
|
@ -167,6 +179,35 @@ class TestResourceAPI(FrappeAPITestCase):
|
|||
self.assertIsInstance(response.json, dict)
|
||||
self.assertIn("data", response.json)
|
||||
|
||||
def test_get_list_expand(self):
|
||||
response = self.get(
|
||||
self.resource(self.DOCTYPE),
|
||||
{
|
||||
"sid": self.sid,
|
||||
"filters": json.dumps([["name", "=", self.GENERATED_DOCUMENTS[0]]]),
|
||||
"fields": json.dumps(["allocated_to", "name"]),
|
||||
"expand": json.dumps(["allocated_to"]),
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIsInstance(response.json, dict)
|
||||
self.assertIn("data", response.json)
|
||||
self.assertIn("allocated_to", response.json["data"][0])
|
||||
self.assertIsInstance(response.json["data"][0]["allocated_to"], dict)
|
||||
self.assertIn("name", response.json["data"][0]["allocated_to"])
|
||||
|
||||
def test_get_doc_expand(self):
|
||||
response = self.get(
|
||||
self.resource(self.DOCTYPE, self.GENERATED_DOCUMENTS[0]),
|
||||
{
|
||||
"expand_links": json.dumps(True),
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIsInstance(response.json, dict)
|
||||
self.assertIn("data", response.json)
|
||||
self.assertIsInstance(response.json["data"]["allocated_to"], dict)
|
||||
|
||||
def test_get_list_limit(self):
|
||||
# test 3: fetch data with limit
|
||||
response = self.get(self.resource(self.DOCTYPE), {"sid": self.sid, "limit": 2})
|
||||
|
|
|
|||
|
|
@ -2836,3 +2836,78 @@ def map_trackers(url_trackers: dict, create: bool = False):
|
|||
frappe_trackers["utm_content"] = url_content
|
||||
|
||||
return frappe_trackers
|
||||
|
||||
|
||||
def attach_expanded_links(doctype: str, docs: list, fields_to_expand: list):
|
||||
"""
|
||||
Expands specified link or dynamic link fields in a list of documents by replacing
|
||||
their linked values (names) with full document records.
|
||||
|
||||
This function takes a list of documents and a list of link fieldnames that should be
|
||||
expanded. For each specified field, it retrieves all referenced linked records from
|
||||
the corresponding doctypes and replaces the link value in each document with the
|
||||
full linked record (as a dict).
|
||||
|
||||
Args:
|
||||
doctype (str): The parent doctype of the provided documents.
|
||||
docs (list[dict]): A list of document dictionaries whose link fields are to be expanded.
|
||||
fields_to_expand (list[str]): A list of fieldnames corresponding to link or dynamic
|
||||
link fields that should be expanded.
|
||||
|
||||
Returns:
|
||||
None: The function modifies the `docs` list in place.
|
||||
|
||||
Example:
|
||||
>>> docs = [{"customer": "CUST-001"}, {"customer": "CUST-002"}]
|
||||
>>> attach_expanded_links("Sales Invoice", docs, ["customer"])
|
||||
>>> docs[0]["customer"]
|
||||
{
|
||||
"name": "CUST-001",
|
||||
"customer_name": "John Doe",
|
||||
"customer_group": "Retail",
|
||||
...
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
if not fields_to_expand:
|
||||
return
|
||||
|
||||
meta = frappe.get_meta(doctype)
|
||||
|
||||
link_fields = {f.fieldname: f for f in meta.get_link_fields() + meta.get_dynamic_link_fields()}
|
||||
|
||||
doctype_values = defaultdict(set)
|
||||
field_to_doctype = {}
|
||||
|
||||
for fieldname in fields_to_expand:
|
||||
if fieldname not in link_fields:
|
||||
continue
|
||||
e = link_fields[fieldname]
|
||||
link_doctype = e.options
|
||||
field_to_doctype[fieldname] = link_doctype
|
||||
|
||||
for li in docs:
|
||||
val = li.get(fieldname)
|
||||
if val:
|
||||
doctype_values[link_doctype].add(val)
|
||||
|
||||
doctype_title_maps = {}
|
||||
|
||||
for link_doctype, values in doctype_values.items():
|
||||
records = frappe.get_list(
|
||||
link_doctype,
|
||||
filters={"name": ["in", list(values)]},
|
||||
fields=["*"],
|
||||
)
|
||||
doctype_title_maps[link_doctype] = {r["name"]: r for r in records}
|
||||
|
||||
for li in docs:
|
||||
for fieldname in fields_to_expand:
|
||||
if fieldname not in field_to_doctype:
|
||||
continue
|
||||
link_doctype = field_to_doctype[fieldname]
|
||||
val = li.get(fieldname)
|
||||
val_title = doctype_title_maps.get(link_doctype, {}).get(val)
|
||||
if val and val_title:
|
||||
li[fieldname] = val_title
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue