diff --git a/frappe/core/doctype/patch_log/patch_log.json b/frappe/core/doctype/patch_log/patch_log.json index 12cfb2a05f..53e85b99d3 100644 --- a/frappe/core/doctype/patch_log/patch_log.json +++ b/frappe/core/doctype/patch_log/patch_log.json @@ -22,19 +22,21 @@ "default": "0", "fieldname": "skipped", "fieldtype": "Check", - "label": "Skipped" + "label": "Skipped", + "read_only": 1 }, { "depends_on": "eval:doc.skipped == 1", "fieldname": "traceback", "fieldtype": "Code", - "label": "Traceback" + "label": "Traceback", + "read_only": 1 } ], "icon": "fa fa-cog", "idx": 1, "links": [], - "modified": "2023-05-04 23:56:02.270262", + "modified": "2023-05-10 19:27:10.883330", "modified_by": "Administrator", "module": "Core", "name": "Patch Log", diff --git a/frappe/database/query.py b/frappe/database/query.py index 3bf6824ab4..595bd5a3ff 100644 --- a/frappe/database/query.py +++ b/frappe/database/query.py @@ -88,9 +88,12 @@ class Engine: if not self.fields: self.fields = [getattr(self.table, "name")] + self.query._child_queries = [] for field in self.fields: if isinstance(field, DynamicTableField): self.query = field.apply_select(self.query) + elif isinstance(field, ChildQuery): + self.query._child_queries.append(field) else: self.query = self.query.select(field) @@ -301,6 +304,9 @@ class Engine: for field in fields: if isinstance(field, Criterion): _fields.append(field) + elif isinstance(field, dict): + for child_field, fields in field.items(): + _fields.append(ChildQuery(child_field, fields, self.doctype)) elif isinstance(field, str): if "," in field: field = field.casefold() if "`" not in field else field @@ -457,6 +463,35 @@ class LinkTableField(DynamicTableField): return query +class ChildQuery: + def __init__( + self, + fieldname: str, + fields: list, + parent_doctype: str, + ) -> None: + field = frappe.get_meta(parent_doctype).get_field(fieldname) + if field.fieldtype not in frappe.model.table_fields: + return + self.fieldname = fieldname + self.fields = fields + self.parent_doctype = parent_doctype + self.doctype = field.options + + def get_query(self, parent_names=None) -> QueryBuilder: + filters = { + "parenttype": self.parent_doctype, + "parentfield": self.fieldname, + "parent": ["in", parent_names], + } + return frappe.qb.get_query( + self.doctype, + fields=self.fields + ["parent", "parentfield"], + filters=filters, + order_by="idx asc", + ) + + def literal_eval_(literal): try: return literal_eval(literal) diff --git a/frappe/modules/patch_handler.py b/frappe/modules/patch_handler.py index 45fc9fb4ce..8b25ffcb8e 100644 --- a/frappe/modules/patch_handler.py +++ b/frappe/modules/patch_handler.py @@ -65,9 +65,9 @@ def run_all(skip_failing: bool = False, patch_type: PatchType | None = None) -> except Exception: if not skip_failing: raise - else: - print("Failed to execute patch") - update_patch_log(patch, skipped=True) + + print("Failed to execute patch") + update_patch_log(patch, skipped=True) patches = get_all_patches(patch_type=patch_type) @@ -214,7 +214,8 @@ def update_patch_log(patchmodule, skipped=False): traceback = frappe.get_traceback(with_context=True) patch.skipped = 1 patch.traceback = traceback - print(traceback) + print(traceback, end="\n\n") + patch.insert(ignore_permissions=True) diff --git a/frappe/public/js/frappe/views/kanban/kanban_board.bundle.js b/frappe/public/js/frappe/views/kanban/kanban_board.bundle.js index 1810101820..979f4fd6fb 100644 --- a/frappe/public/js/frappe/views/kanban/kanban_board.bundle.js +++ b/frappe/public/js/frappe/views/kanban/kanban_board.bundle.js @@ -317,6 +317,7 @@ frappe.provide("frappe.views"); return state.columns; }, make_columns); prepare(); + make_columns(); store.watch((state, getters) => { return state.cur_list; }, setup_restore_columns); diff --git a/frappe/query_builder/utils.py b/frappe/query_builder/utils.py index bfc2c49b8e..af2c871e83 100644 --- a/frappe/query_builder/utils.py +++ b/frappe/query_builder/utils.py @@ -81,8 +81,27 @@ def patch_query_execute(): """ def execute_query(query, *args, **kwargs): + child_queries = query._child_queries if isinstance(query._child_queries, list) else [] query, params = prepare_query(query) - return frappe.db.sql(query, params, *args, **kwargs) # nosemgrep + result = frappe.db.sql(query, params, *args, **kwargs) # nosemgrep + execute_child_queries(child_queries, result) + return result + + def execute_child_queries(queries, result): + if not result or not isinstance(result[0], dict) or not result[0].name: + return + parent_names = [d.name for d in result] + for child_query in queries: + data = child_query.get_query(parent_names).run(as_dict=1) + for row in result: + row[child_query.fieldname] = [] + for d in data: + if str(d.parent) == str(row.name) and d.parentfield == child_query.fieldname: + if "parent" not in child_query.fields: + del d["parent"] + if "parentfield" not in child_query.fields: + del d["parentfield"] + row[child_query.fieldname].append(d) def prepare_query(query): import inspect diff --git a/frappe/tests/test_query.py b/frappe/tests/test_query.py index 82218e5952..dfebf5e890 100644 --- a/frappe/tests/test_query.py +++ b/frappe/tests/test_query.py @@ -419,3 +419,37 @@ class TestQuery(FrappeTestCase): frappe.db.sql("delete from `tabDocType` where `name` = 'Test Tree DocType'") frappe.db.sql_ddl("drop table if exists `tabTest Tree DocType`") + + def test_child_field_syntax(self): + note1 = frappe.get_doc( + doctype="Note", title="Note 1", seen_by=[{"user": "Administrator"}] + ).insert() + note2 = frappe.get_doc( + doctype="Note", title="Note 2", seen_by=[{"user": "Administrator"}, {"user": "Guest"}] + ).insert() + + result = frappe.qb.get_query( + "Note", + filters={"name": ["in", [note1.name, note2.name]]}, + fields=["name", {"seen_by": ["*"]}], + order_by="title asc", + ).run(as_dict=1) + + self.assertTrue(isinstance(result[0].seen_by, list)) + self.assertTrue(isinstance(result[1].seen_by, list)) + self.assertEqual(len(result[0].seen_by), 1) + self.assertEqual(len(result[1].seen_by), 2) + self.assertEqual(result[0].seen_by[0].user, "Administrator") + + result = frappe.qb.get_query( + "Note", + filters={"name": ["in", [note1.name, note2.name]]}, + fields=["name", {"seen_by": ["user"]}], + order_by="title asc", + ).run(as_dict=1) + + self.assertEqual(len(result[0].seen_by[0].keys()), 1) + self.assertEqual(result[1].seen_by[1].user, "Guest") + + note1.delete() + note2.delete()