diff --git a/frappe/core/doctype/doctype/test_doctype.py b/frappe/core/doctype/doctype/test_doctype.py index 05ecc660e0..fead7672fe 100644 --- a/frappe/core/doctype/doctype/test_doctype.py +++ b/frappe/core/doctype/doctype/test_doctype.py @@ -526,13 +526,14 @@ class TestDocType(FrappeTestCase): self.assertRaises(InvalidFieldNameError, validate_links_table_fieldnames, doc) def test_create_virtual_doctype(self): - """Test virtual DOcTYpe.""" + """Test virtual DocType.""" virtual_doc = new_doctype("Test Virtual Doctype") virtual_doc.is_virtual = 1 - virtual_doc.insert() - virtual_doc.save() + virtual_doc.insert(ignore_if_duplicate=True) + virtual_doc.reload() doc = frappe.get_doc("DocType", "Test Virtual Doctype") + self.assertDictEqual(doc.as_dict(), virtual_doc.as_dict()) self.assertEqual(doc.is_virtual, 1) self.assertFalse(frappe.db.table_exists("Test Virtual Doctype")) diff --git a/frappe/database/database.py b/frappe/database/database.py index d608e30fc7..0ffda1af9d 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -20,6 +20,7 @@ import frappe.defaults import frappe.model.meta from frappe import _ from frappe.database.utils import ( + DefaultOrderBy, EmptyQueryValues, FallBackDateTimeStr, LazyMogrify, @@ -422,7 +423,7 @@ class Database: ignore=None, as_dict=False, debug=False, - order_by="KEEP_DEFAULT_ORDERING", + order_by=DefaultOrderBy, cache=False, for_update=False, *, @@ -492,7 +493,7 @@ class Database: ignore=None, as_dict=False, debug=False, - order_by="KEEP_DEFAULT_ORDERING", + order_by=DefaultOrderBy, update=None, cache=False, for_update=False, @@ -551,7 +552,7 @@ class Database: if (filters is not None) and (filters != doctype or doctype == "DocType"): try: if order_by: - order_by = "modified" if order_by == "KEEP_DEFAULT_ORDERING" else order_by + order_by = "modified" if order_by == DefaultOrderBy else order_by out = self._get_values_from_table( fields=fields, filters=filters, diff --git a/frappe/database/query.py b/frappe/database/query.py index 10423f9ca4..3bf6824ab4 100644 --- a/frappe/database/query.py +++ b/frappe/database/query.py @@ -10,7 +10,7 @@ from pypika.queries import QueryBuilder, Table import frappe from frappe import _ from frappe.database.operator_map import OPERATOR_MAP -from frappe.database.utils import get_doctype_name +from frappe.database.utils import DefaultOrderBy, get_doctype_name from frappe.query_builder import Criterion, Field, Order, functions from frappe.query_builder.functions import Function, SqlFunctions from frappe.query_builder.utils import PseudoColumnMapper @@ -314,7 +314,7 @@ class Engine: return _fields def apply_order_by(self, order_by: str | None): - if not order_by or order_by == "KEEP_DEFAULT_ORDERING": + if not order_by or order_by == DefaultOrderBy: return for declaration in order_by.split(","): if _order_by := declaration.strip(): diff --git a/frappe/database/utils.py b/frappe/database/utils.py index 304fd72be6..d1030ca6d7 100644 --- a/frappe/database/utils.py +++ b/frappe/database/utils.py @@ -17,7 +17,7 @@ QueryValues = tuple | list | dict | NoneType EmptyQueryValues = object() FallBackDateTimeStr = "0001-01-01 00:00:00.000000" - +DefaultOrderBy = "KEEP_DEFAULT_ORDERING" NestedSetHierarchy = ( "ancestors of", "descendants of", diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index e3858a3ff7..81ac1847d9 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -13,9 +13,10 @@ import frappe.permissions import frappe.share from frappe import _ from frappe.core.doctype.server_script.server_script_utils import get_server_script_map -from frappe.database.utils import FallBackDateTimeStr, NestedSetHierarchy +from frappe.database.utils import DefaultOrderBy, FallBackDateTimeStr, NestedSetHierarchy from frappe.model import get_permitted_fields, optional_fields from frappe.model.meta import get_table_columns +from frappe.model.utils import is_virtual_doctype from frappe.model.utils.user_settings import get_user_settings, update_user_settings from frappe.query_builder.utils import Column from frappe.utils import ( @@ -80,7 +81,7 @@ class DatabaseQuery: or_filters=None, docstatus=None, group_by=None, - order_by="KEEP_DEFAULT_ORDERING", + order_by=DefaultOrderBy, limit_start=False, limit_page_length=None, as_list=False, @@ -171,6 +172,21 @@ class DatabaseQuery: if user_settings: self.user_settings = json.loads(user_settings) + if is_virtual_doctype(self.doctype): + from frappe.model.base_document import get_controller + + controller = get_controller(self.doctype) + self.parse_args() + kwargs = { + "as_list": as_list, + "with_comment_count": with_comment_count, + "save_user_settings": save_user_settings, + "save_user_settings_fields": save_user_settings_fields, + "pluck": pluck, + "parent_doctype": parent_doctype, + } | self.__dict__ + return controller.get_list(kwargs) + self.columns = self.get_table_columns() # no table & ignore_ddl, return diff --git a/frappe/model/utils/__init__.py b/frappe/model/utils/__init__.py index bf6804ad05..2935872fc7 100644 --- a/frappe/model/utils/__init__.py +++ b/frappe/model/utils/__init__.py @@ -129,5 +129,7 @@ def get_fetch_values(doctype, fieldname, value): @site_cache() -def is_virtual_doctype(doctype): - return frappe.db.get_value("DocType", doctype, "is_virtual") +def is_virtual_doctype(doctype: str): + if frappe.db.has_column("DocType", "is_virtual"): + return frappe.db.get_value("DocType", doctype, "is_virtual") + return False diff --git a/frappe/tests/test_db_query.py b/frappe/tests/test_db_query.py index dba301109c..96b71f5eb7 100644 --- a/frappe/tests/test_db_query.py +++ b/frappe/tests/test_db_query.py @@ -2,10 +2,13 @@ # License: MIT. See LICENSE import datetime from contextlib import contextmanager +from unittest.mock import MagicMock, patch import frappe +from frappe.core.doctype.doctype.test_doctype import new_doctype from frappe.core.page.permission_manager.permission_manager import add, reset, update from frappe.custom.doctype.property_setter.property_setter import make_property_setter +from frappe.database.utils import DefaultOrderBy from frappe.desk.reportview import get_filters_cond from frappe.handler import execute_cmd from frappe.model.db_query import DatabaseQuery @@ -43,7 +46,7 @@ def setup_patched_blog_post(): yield -class TestReportview(FrappeTestCase): +class TestDBQuery(FrappeTestCase): def setUp(self): frappe.set_user("Administrator") @@ -848,68 +851,6 @@ class TestReportview(FrappeTestCase): fields=["blog_category.description"], ) - def test_reportview_get_permlevel_system_users(self): - with setup_patched_blog_post(), setup_test_user(set_user=True): - frappe.local.request = frappe._dict() - frappe.local.request.method = "POST" - frappe.local.form_dict = frappe._dict( - { - "doctype": "Blog Post", - "fields": ["published", "title", "`tabTest Child`.`test_field`"], - } - ) - - # even if * is passed, fields which are not accessible should be filtered out - response = execute_cmd("frappe.desk.reportview.get") - self.assertListEqual(response["keys"], ["title"]) - frappe.local.form_dict = frappe._dict( - { - "doctype": "Blog Post", - "fields": ["*"], - } - ) - - response = execute_cmd("frappe.desk.reportview.get") - self.assertNotIn("published", response["keys"]) - - def test_reportview_get_admin(self): - # Admin should be able to see access all fields - with setup_patched_blog_post(): - frappe.local.request = frappe._dict() - frappe.local.request.method = "POST" - frappe.local.form_dict = frappe._dict( - { - "doctype": "Blog Post", - "fields": ["published", "title", "`tabTest Child`.`test_field`"], - } - ) - response = execute_cmd("frappe.desk.reportview.get") - self.assertListEqual(response["keys"], ["published", "title", "test_field"]) - - def test_reportview_get_aggregation(self): - # test aggregation based on child table field - frappe.local.request = frappe._dict() - frappe.local.request.method = "POST" - frappe.local.form_dict = frappe._dict( - { - "doctype": "DocType", - "fields": """["`tabDocField`.`label` as field_label","`tabDocField`.`name` as field_name"]""", - "filters": "[]", - "order_by": "_aggregate_column desc", - "start": 0, - "page_length": 20, - "view": "Report", - "with_comment_count": 0, - "group_by": "field_label, field_name", - "aggregate_on_field": "columns", - "aggregate_on_doctype": "DocField", - "aggregate_function": "sum", - } - ) - - response = execute_cmd("frappe.desk.reportview.get") - self.assertListEqual(response["keys"], ["field_label", "field_name", "_aggregate_column"]) - def test_cast_name(self): from frappe.core.doctype.doctype.test_doctype import new_doctype @@ -1007,6 +948,33 @@ class TestReportview(FrappeTestCase): self.assertTrue(dashboard_settings) + def test_virtual_doctype(self): + """Test that virtual doctypes can be queried using get_all""" + + virtual_doctype = new_doctype("Virtual DocType") + virtual_doctype.is_virtual = 1 + virtual_doctype.insert(ignore_if_duplicate=True) + + class VirtualDocType: + @staticmethod + def get_list(args): + ... + + with patch("frappe.controllers", new={frappe.local.site: {"Virtual DocType": VirtualDocType}}): + VirtualDocType.get_list = MagicMock() + + frappe.get_all("Virtual DocType", filters={"name": "test"}, fields=["name"], limit=1) + + call_args = VirtualDocType.get_list.call_args[0][0] + VirtualDocType.get_list.assert_called_once() + self.assertIsInstance(call_args, dict) + self.assertEqual(call_args["doctype"], "Virtual DocType") + self.assertEqual(call_args["filters"], [["Virtual DocType", "name", "=", "test"]]) + self.assertEqual(call_args["fields"], ["name"]) + self.assertEqual(call_args["limit_page_length"], 1) + self.assertEqual(call_args["limit_start"], 0) + self.assertEqual(call_args["order_by"], DefaultOrderBy) + def test_coalesce_with_in_ops(self): self.assertNotIn("ifnull", frappe.get_all("User", {"name": ("in", ["a", "b"])}, run=0)) self.assertIn("ifnull", frappe.get_all("User", {"name": ("in", ["a", None])}, run=0)) @@ -1017,6 +985,129 @@ class TestReportview(FrappeTestCase): self.assertIn("ifnull", frappe.get_all("User", {"name": ("not in", [""])}, run=0)) +class TestReportView(FrappeTestCase): + def test_reportview_get(self): + user = frappe.get_doc("User", "test@example.com") + add_child_table_to_blog_post() + + user_roles = frappe.get_roles() + user.remove_roles(*user_roles) + user.add_roles("Blogger") + + make_property_setter("Blog Post", "published", "permlevel", 1, "Int") + reset("Blog Post") + add("Blog Post", "Website Manager", 1) + update("Blog Post", "Website Manager", 1, "write", 1) + + frappe.set_user(user.name) + + frappe.local.request = frappe._dict() + frappe.local.request.method = "POST" + + frappe.local.form_dict = frappe._dict( + { + "doctype": "Blog Post", + "fields": ["published", "title", "`tabTest Child`.`test_field`"], + } + ) + + # even if * is passed, fields which are not accessible should be filtered out + response = execute_cmd("frappe.desk.reportview.get") + self.assertListEqual(response["keys"], ["title"]) + frappe.local.form_dict = frappe._dict( + { + "doctype": "Blog Post", + "fields": ["*"], + } + ) + + response = execute_cmd("frappe.desk.reportview.get") + self.assertNotIn("published", response["keys"]) + + frappe.set_user("Administrator") + user.add_roles("Website Manager") + frappe.set_user(user.name) + + frappe.set_user("Administrator") + + # Admin should be able to see access all fields + frappe.local.form_dict = frappe._dict( + { + "doctype": "Blog Post", + "fields": ["published", "title", "`tabTest Child`.`test_field`"], + } + ) + + response = execute_cmd("frappe.desk.reportview.get") + self.assertListEqual(response["keys"], ["published", "title", "test_field"]) + + # reset user roles + user.remove_roles("Blogger", "Website Manager") + user.add_roles(*user_roles) + + def test_reportview_get_aggregation(self): + # test aggregation based on child table field + frappe.local.request = frappe._dict() + frappe.local.request.method = "POST" + frappe.local.form_dict = frappe._dict( + { + "doctype": "DocType", + "fields": """["`tabDocField`.`label` as field_label","`tabDocField`.`name` as field_name"]""", + "filters": "[]", + "order_by": "_aggregate_column desc", + "start": 0, + "page_length": 20, + "view": "Report", + "with_comment_count": 0, + "group_by": "field_label, field_name", + "aggregate_on_field": "columns", + "aggregate_on_doctype": "DocField", + "aggregate_function": "sum", + } + ) + + response = execute_cmd("frappe.desk.reportview.get") + self.assertListEqual(response["keys"], ["field_label", "field_name", "_aggregate_column"]) + + def test_reportview_get_permlevel_system_users(self): + with setup_patched_blog_post(), setup_test_user(set_user=True): + frappe.local.request = frappe._dict() + frappe.local.request.method = "POST" + frappe.local.form_dict = frappe._dict( + { + "doctype": "Blog Post", + "fields": ["published", "title", "`tabTest Child`.`test_field`"], + } + ) + + # even if * is passed, fields which are not accessible should be filtered out + response = execute_cmd("frappe.desk.reportview.get") + self.assertListEqual(response["keys"], ["title"]) + frappe.local.form_dict = frappe._dict( + { + "doctype": "Blog Post", + "fields": ["*"], + } + ) + + response = execute_cmd("frappe.desk.reportview.get") + self.assertNotIn("published", response["keys"]) + + def test_reportview_get_admin(self): + # Admin should be able to see access all fields + with setup_patched_blog_post(): + frappe.local.request = frappe._dict() + frappe.local.request.method = "POST" + frappe.local.form_dict = frappe._dict( + { + "doctype": "Blog Post", + "fields": ["published", "title", "`tabTest Child`.`test_field`"], + } + ) + response = execute_cmd("frappe.desk.reportview.get") + self.assertListEqual(response["keys"], ["published", "title", "test_field"]) + + def add_child_table_to_blog_post(): child_table = frappe.get_doc( { @@ -1040,7 +1131,7 @@ def create_event(subject="_Test Event", starts_on=None): from frappe.utils import get_datetime - event = frappe.get_doc( + return frappe.get_doc( { "doctype": "Event", "subject": subject, @@ -1049,8 +1140,6 @@ def create_event(subject="_Test Event", starts_on=None): } ).insert(ignore_permissions=True) - return event - def create_nested_doctype(): if frappe.db.exists("DocType", "Nested DocType"):