diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index 9a0613e6ca..91a317dbff 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -1679,7 +1679,9 @@ def make_module_and_roles(doc, perm_fieldname="permissions"): for role in list(set(roles)): if frappe.db.table_exists("Role", cached=False) and not frappe.db.exists("Role", role): - r = frappe.get_doc(dict(doctype="Role", role_name=role, desk_access=1)) + r = frappe.new_doc("Role") + r.role_name = role + r.desk_access = 1 r.flags.ignore_mandatory = r.flags.ignore_permissions = True r.insert() except frappe.DoesNotExistError as e: diff --git a/frappe/core/doctype/server_script/server_script.json b/frappe/core/doctype/server_script/server_script.json index 3aedd4f542..67cb6e75ea 100644 --- a/frappe/core/doctype/server_script/server_script.json +++ b/frappe/core/doctype/server_script/server_script.json @@ -9,6 +9,7 @@ "script_type", "reference_doctype", "event_frequency", + "cron_format", "doctype_event", "api_method", "allow_guest", @@ -99,7 +100,7 @@ "fieldtype": "Select", "label": "Event Frequency", "mandatory_depends_on": "eval:doc.script_type == \"Scheduler Event\"", - "options": "All\nHourly\nDaily\nWeekly\nMonthly\nYearly\nHourly Long\nDaily Long\nWeekly Long\nMonthly Long" + "options": "All\nHourly\nDaily\nWeekly\nMonthly\nYearly\nHourly Long\nDaily Long\nWeekly Long\nMonthly Long\nCron" }, { "fieldname": "module", @@ -132,6 +133,12 @@ "fieldname": "rate_limit_seconds", "fieldtype": "Int", "label": "Time Window (Seconds)" + }, + { + "depends_on": "eval:doc.event_frequency==='Cron'", + "fieldname": "cron_format", + "fieldtype": "Data", + "label": "Cron Format" } ], "index_web_pages_for_search": 1, @@ -141,7 +148,7 @@ "link_fieldname": "server_script" } ], - "modified": "2023-05-16 11:03:58.282680", + "modified": "2023-05-27 16:33:16.595424", "modified_by": "Administrator", "module": "Core", "name": "Server Script", diff --git a/frappe/core/doctype/server_script/server_script.py b/frappe/core/doctype/server_script/server_script.py index a9b870e240..07808d619b 100644 --- a/frappe/core/doctype/server_script/server_script.py +++ b/frappe/core/doctype/server_script/server_script.py @@ -52,11 +52,16 @@ class ServerScript(Document): def sync_scheduler_events(self): """Create or update Scheduled Job Type documents for Scheduler Event Server Scripts""" if not self.disabled and self.event_frequency and self.script_type == "Scheduler Event": - setup_scheduler_events(script_name=self.name, frequency=self.event_frequency) + cron_format = self.cron_format if self.event_frequency == "Cron" else None + setup_scheduler_events( + script_name=self.name, frequency=self.event_frequency, cron_format=cron_format + ) def clear_scheduled_events(self): - """Deletes existing scheduled jobs by Server Script if self.event_frequency has changed""" - if self.script_type == "Scheduler Event" and self.has_value_changed("event_frequency"): + """Deletes existing scheduled jobs by Server Script if self.event_frequency or self.cron_format has changed""" + if self.script_type == "Scheduler Event" and ( + self.has_value_changed("event_frequency") or self.has_value_changed("cron_format") + ): for scheduled_job in self.scheduled_jobs: frappe.delete_doc("Scheduled Job Type", scheduled_job.name) @@ -171,7 +176,7 @@ class ServerScript(Document): return items -def setup_scheduler_events(script_name, frequency): +def setup_scheduler_events(script_name: str, frequency: str, cron_format: str | None = None): """Creates or Updates Scheduled Job Type documents based on the specified script name and frequency Args: @@ -188,6 +193,7 @@ def setup_scheduler_events(script_name, frequency): "method": method, "frequency": frequency, "server_script": script_name, + "cron_format": cron_format, } ).insert() @@ -200,6 +206,7 @@ def setup_scheduler_events(script_name, frequency): return doc.frequency = frequency + doc.cron_format = cron_format doc.save() frappe.msgprint(_("Scheduled execution for script {0} has updated").format(script_name)) diff --git a/frappe/core/doctype/server_script/test_server_script.py b/frappe/core/doctype/server_script/test_server_script.py index 3d11a02ca4..4371806b32 100644 --- a/frappe/core/doctype/server_script/test_server_script.py +++ b/frappe/core/doctype/server_script/test_server_script.py @@ -3,6 +3,7 @@ import requests import frappe +from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs from frappe.frappeclient import FrappeClient, FrappeException from frappe.tests.utils import FrappeTestCase from frappe.utils import get_site_url @@ -283,3 +284,37 @@ frappe.qb.from_(todo).select(todo.name).where(todo.name == "{todo.name}").run() script1.delete() script2.delete() frappe.db.commit() + + def test_server_script_scheduled(self): + scheduled_script = frappe.get_doc( + doctype="Server Script", + name="scheduled_script_wo_cron", + script_type="Scheduler Event", + script="""frappe.flags = {"test": True}""", + event_frequency="Hourly", + ).insert() + + cron_script = frappe.get_doc( + doctype="Server Script", + name="scheduled_script_w_cron", + script_type="Scheduler Event", + script="""frappe.flags = {"test": True}""", + event_frequency="Cron", + cron_format="0 0 1 1 *", # 1st january + ).insert() + + # Ensure that jobs remain in DB after migrate + sync_jobs() + self.assertTrue(frappe.db.exists("Scheduled Job Type", {"server_script": scheduled_script.name})) + + cron_job_name = frappe.db.get_value("Scheduled Job Type", {"server_script": cron_script.name}) + self.assertTrue(cron_job_name) + + cron_job = frappe.get_doc("Scheduled Job Type", cron_job_name) + self.assertEqual(cron_job.next_execution.day, 1) + self.assertEqual(cron_job.next_execution.month, 1) + + cron_script.cron_format = "0 0 2 1 *" # 2nd january + cron_script.save() + cron_job.reload() + self.assertEqual(cron_job.next_execution.day, 2) diff --git a/frappe/database/query.py b/frappe/database/query.py index 25fa6a0528..02beff9afc 100644 --- a/frappe/database/query.py +++ b/frappe/database/query.py @@ -1,4 +1,3 @@ -import itertools import re from ast import literal_eval from types import BuiltinFunctionType @@ -190,15 +189,18 @@ class Engine: if _operator in OPERATOR_MAP["nested_set"]: hierarchy = _operator docname = _value - result = get_nested_set_hierarchy_result(self.doctype, docname, hierarchy) + + _df = frappe.get_meta(self.doctype).get_field(field) + ref_doctype = _df.options if _df else self.doctype + + nodes = get_nested_set_hierarchy_result(ref_doctype, docname, hierarchy) operator_fn = ( OPERATOR_MAP["not in"] if hierarchy in ("not ancestors of", "not descendants of") else OPERATOR_MAP["in"] ) - if result: - result = list(itertools.chain.from_iterable(result)) - self.query = self.query.where(operator_fn(_field, result)) + if nodes: + self.query = self.query.where(operator_fn(_field, nodes)) else: self.query = self.query.where(operator_fn(_field, ("",))) return @@ -513,22 +515,25 @@ def has_function(field): return True -def get_nested_set_hierarchy_result(doctype: str, name: str, hierarchy: str): +def get_nested_set_hierarchy_result(doctype: str, name: str, hierarchy: str) -> list[str]: + """Get matching nodes based on operator.""" table = frappe.qb.DocType(doctype) try: lft, rgt = frappe.qb.from_(table).select("lft", "rgt").where(table.name == name).run()[0] except IndexError: lft, rgt = None, None - if hierarchy in ("descendants of", "not descendants of"): + if hierarchy in ("descendants of", "not descendants of", "descendants of (inclusive)"): result = ( frappe.qb.from_(table) .select(table.name) .where(table.lft > lft) .where(table.rgt < rgt) .orderby(table.lft, order=Order.asc) - .run() + .run(pluck=True) ) + if hierarchy == "descendants of (inclusive)": + result += [name] else: # Get ancestor elements of a DocType with a tree structure result = ( @@ -537,6 +542,6 @@ def get_nested_set_hierarchy_result(doctype: str, name: str, hierarchy: str): .where(table.lft < lft) .where(table.rgt > rgt) .orderby(table.lft, order=Order.desc) - .run() + .run(pluck=True) ) return result diff --git a/frappe/database/utils.py b/frappe/database/utils.py index d1030ca6d7..61dd0016c5 100644 --- a/frappe/database/utils.py +++ b/frappe/database/utils.py @@ -23,6 +23,7 @@ NestedSetHierarchy = ( "descendants of", "not ancestors of", "not descendants of", + "descendants of (inclusive)", ) diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index 340b3a97f4..ca1969abcf 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -730,18 +730,30 @@ class DatabaseQuery: lft, rgt = frappe.db.get_value(ref_doctype, f.value, ["lft", "rgt"]) # Get descendants elements of a DocType with a tree structure - if f.operator.lower() in ("descendants of", "not descendants of"): - result = frappe.get_all( - ref_doctype, filters={"lft": [">", lft], "rgt": ["<", rgt]}, order_by="`lft` ASC" + if f.operator.lower() in ( + "descendants of", + "not descendants of", + "descendants of (inclusive)", + ): + nodes = frappe.get_all( + ref_doctype, + filters={"lft": [">", lft], "rgt": ["<", rgt]}, + order_by="`lft` ASC", + pluck="name", ) + if f.operator.lower() == "descendants of (inclusive)": + nodes += [f.value] else: # Get ancestor elements of a DocType with a tree structure - result = frappe.get_all( - ref_doctype, filters={"lft": ["<", lft], "rgt": [">", rgt]}, order_by="`lft` DESC" + nodes = frappe.get_all( + ref_doctype, + filters={"lft": ["<", lft], "rgt": [">", rgt]}, + order_by="`lft` DESC", + pluck="name", ) fallback = "''" - value = [frappe.db.escape((cstr(v.name) or "").strip(), percent=False) for v in result] + value = [frappe.db.escape((cstr(v)).strip(), percent=False) for v in nodes] if len(value): value = f"({', '.join(value)})" else: diff --git a/frappe/public/js/frappe/list/base_list.js b/frappe/public/js/frappe/list/base_list.js index e6ed0df7f4..da47417942 100644 --- a/frappe/public/js/frappe/list/base_list.js +++ b/frappe/public/js/frappe/list/base_list.js @@ -678,7 +678,9 @@ class FilterArea { if ( fields_dict[fieldname] && (condition === "=" || - (condition === "like" && fields_dict[fieldname]?.df?.fieldtype != "Link")) + (condition === "like" && fields_dict[fieldname]?.df?.fieldtype != "Link") || + (condition === "descendants of (inclusive)" && + fields_dict[fieldname]?.df?.fieldtype == "Link")) ) { // standard filter out.promise = out.promise.then(() => fields_dict[fieldname].set_value(value)); @@ -788,6 +790,13 @@ class FilterArea { options = options.join("\n"); } } + if ( + df.fieldtype == "Link" && + df.options && + frappe.boot.treeviews.includes(df.options) + ) { + condition = "descendants of (inclusive)"; + } return { fieldtype: fieldtype, diff --git a/frappe/public/js/frappe/ui/filters/filter.js b/frappe/public/js/frappe/ui/filters/filter.js index 5902c136bd..dd8ce7f6dd 100644 --- a/frappe/public/js/frappe/ui/filters/filter.js +++ b/frappe/public/js/frappe/ui/filters/filter.js @@ -30,6 +30,7 @@ frappe.ui.Filter = class { this.nested_set_conditions = [ ["descendants of", __("Descendants Of")], + ["descendants of (inclusive)", __("Descendants Of (inclusive)")], ["not descendants of", __("Not Descendants Of")], ["ancestors of", __("Ancestors Of")], ["not ancestors of", __("Not Ancestors Of")], @@ -524,6 +525,7 @@ frappe.ui.filter_utils = { "=", "!=", "descendants of", + "descendants of (inclusive)", "ancestors of", "not descendants of", "not ancestors of", diff --git a/frappe/public/js/frappe/ui/like.js b/frappe/public/js/frappe/ui/like.js index aa007cf138..6828c77dfd 100644 --- a/frappe/public/js/frappe/ui/like.js +++ b/frappe/public/js/frappe/ui/like.js @@ -89,7 +89,7 @@ frappe.ui.click_toggle_like = function () { return false; }; -frappe.ui.setup_like_popover = ($parent, selector, check_not_liked = true) => { +frappe.ui.setup_like_popover = ($parent, selector) => { if (frappe.dom.is_touchscreen()) { return; } @@ -109,20 +109,6 @@ frappe.ui.setup_like_popover = ($parent, selector, check_not_liked = true) => { liked_by = liked_by ? decodeURI(liked_by) : "[]"; liked_by = JSON.parse(liked_by); - const user = frappe.session.user; - // hack - if (check_not_liked) { - if (target_element.parents(".liked-by").find(".not-liked").length) { - if (liked_by.indexOf(user) !== -1) { - liked_by.splice(liked_by.indexOf(user), 1); - } - } else { - if (liked_by.indexOf(user) === -1) { - liked_by.push(user); - } - } - } - if (!liked_by.length) { return ""; } diff --git a/frappe/tests/test_nestedset.py b/frappe/tests/test_nestedset.py index ef63fb66c2..340b53bf38 100644 --- a/frappe/tests/test_nestedset.py +++ b/frappe/tests/test_nestedset.py @@ -51,35 +51,35 @@ records = [ }, ] +TEST_DOCTYPE = "Test Tree DocType" + class NestedSetTestUtil: def setup_test_doctype(self): - frappe.db.sql("delete from `tabDocType` where `name` = 'Test Tree DocType'") - frappe.db.sql_ddl("drop table if exists `tabTest Tree DocType`") + frappe.db.delete("DocType", TEST_DOCTYPE) + frappe.db.sql_ddl(f"drop table if exists `tab{TEST_DOCTYPE}`") - self.tree_doctype = new_doctype( - "Test Tree DocType", is_tree=True, autoname="field:some_fieldname" - ) + self.tree_doctype = new_doctype(TEST_DOCTYPE, is_tree=True, autoname="field:some_fieldname") self.tree_doctype.insert() for record in records: - d = frappe.new_doc("Test Tree DocType") + d = frappe.new_doc(TEST_DOCTYPE) d.update(record) d.insert() def teardown_test_doctype(self): self.tree_doctype.delete() - frappe.db.sql_ddl("drop table if exists `tabTest Tree DocType`") + frappe.db.sql_ddl(f"drop table if exists `{TEST_DOCTYPE}`") def move_it_back(self): - parent_1 = frappe.get_doc("Test Tree DocType", "Parent 1") + parent_1 = frappe.get_doc(TEST_DOCTYPE, "Parent 1") parent_1.parent_test_tree_doctype = "Root Node" parent_1.save() def get_no_of_children(self, record_name: str) -> int: if not record_name: - return frappe.db.count("Test Tree DocType") - return len(get_descendants_of("Test Tree DocType", record_name, ignore_permissions=True)) + return frappe.db.count(TEST_DOCTYPE) + return len(get_descendants_of(TEST_DOCTYPE, record_name, ignore_permissions=True)) class TestNestedSet(FrappeTestCase): @@ -101,18 +101,18 @@ class TestNestedSet(FrappeTestCase): global records min_lft = 1 - max_rgt = frappe.qb.from_("Test Tree DocType").select(Max(Field("rgt"))).run(pluck=True)[0] + max_rgt = frappe.qb.from_(TEST_DOCTYPE).select(Max(Field("rgt"))).run(pluck=True)[0] for record in records: lft, rgt, parent_test_tree_doctype = frappe.db.get_value( - "Test Tree DocType", + TEST_DOCTYPE, record["some_fieldname"], ["lft", "rgt", "parent_test_tree_doctype"], ) if parent_test_tree_doctype: parent_lft, parent_rgt = frappe.db.get_value( - "Test Tree DocType", parent_test_tree_doctype, ["lft", "rgt"] + TEST_DOCTYPE, parent_test_tree_doctype, ["lft", "rgt"] ) else: # root @@ -138,19 +138,19 @@ class TestNestedSet(FrappeTestCase): self.assertTrue(parent_rgt == (parent_lft + 1 + (2 * no_of_children))) def test_recursion(self): - leaf_node = frappe.get_doc("Test Tree DocType", {"some_fieldname": "Parent 2"}) + leaf_node = frappe.get_doc(TEST_DOCTYPE, {"some_fieldname": "Parent 2"}) leaf_node.parent_test_tree_doctype = "Child 3" self.assertRaises(NestedSetRecursionError, leaf_node.save) leaf_node.reload() def test_rebuild_tree(self): - rebuild_tree("Test Tree DocType", "parent_test_tree_doctype") + rebuild_tree(TEST_DOCTYPE, "parent_test_tree_doctype") self.test_basic_tree() def test_move_group_into_another(self): - old_lft, old_rgt = frappe.db.get_value("Test Tree DocType", "Parent 2", ["lft", "rgt"]) + old_lft, old_rgt = frappe.db.get_value(TEST_DOCTYPE, "Parent 2", ["lft", "rgt"]) - parent_1 = frappe.get_doc("Test Tree DocType", "Parent 1") + parent_1 = frappe.get_doc(TEST_DOCTYPE, "Parent 1") lft, rgt = parent_1.lft, parent_1.rgt parent_1.parent_test_tree_doctype = "Parent 2" @@ -158,7 +158,7 @@ class TestNestedSet(FrappeTestCase): self.test_basic_tree() # after move - new_lft, new_rgt = frappe.db.get_value("Test Tree DocType", "Parent 2", ["lft", "rgt"]) + new_lft, new_rgt = frappe.db.get_value(TEST_DOCTYPE, "Parent 2", ["lft", "rgt"]) # lft should reduce self.assertEqual(old_lft - new_lft, rgt - lft + 1) @@ -170,12 +170,10 @@ class TestNestedSet(FrappeTestCase): self.test_basic_tree() def test_move_leaf_into_another_group(self): - child_2 = frappe.get_doc("Test Tree DocType", "Child 2") + child_2 = frappe.get_doc(TEST_DOCTYPE, "Child 2") # assert that child 2 is not already under parent 1 - parent_lft_old, parent_rgt_old = frappe.db.get_value( - "Test Tree DocType", "Parent 2", ["lft", "rgt"] - ) + parent_lft_old, parent_rgt_old = frappe.db.get_value(TEST_DOCTYPE, "Parent 2", ["lft", "rgt"]) self.assertTrue((parent_lft_old > child_2.lft) and (parent_rgt_old > child_2.rgt)) child_2.parent_test_tree_doctype = "Parent 2" @@ -183,22 +181,20 @@ class TestNestedSet(FrappeTestCase): self.test_basic_tree() # assert that child 2 is under parent 1 - parent_lft_new, parent_rgt_new = frappe.db.get_value( - "Test Tree DocType", "Parent 2", ["lft", "rgt"] - ) + parent_lft_new, parent_rgt_new = frappe.db.get_value(TEST_DOCTYPE, "Parent 2", ["lft", "rgt"]) self.assertFalse((parent_lft_new > child_2.lft) and (parent_rgt_new > child_2.rgt)) def test_delete_leaf(self): global records el = {"some_fieldname": "Child 1", "parent_test_tree_doctype": "Parent 1", "is_group": 0} - child_1 = frappe.get_doc("Test Tree DocType", "Child 1") + child_1 = frappe.get_doc(TEST_DOCTYPE, "Child 1") child_1.delete() records.remove(el) self.test_basic_tree() - n = frappe.new_doc("Test Tree DocType") + n = frappe.new_doc(TEST_DOCTYPE) n.update(el) n.insert() records.append(el) @@ -208,10 +204,10 @@ class TestNestedSet(FrappeTestCase): def test_delete_group(self): # cannot delete group with child, but can delete leaf with self.assertRaises(NestedSetChildExistsError): - frappe.delete_doc("Test Tree DocType", "Parent 1") + frappe.delete_doc(TEST_DOCTYPE, "Parent 1") def test_remove_subtree(self): - remove_subtree("Test Tree DocType", "Parent 2") + remove_subtree(TEST_DOCTYPE, "Parent 2") self.test_basic_tree() def test_rename_nestedset(self): @@ -223,7 +219,7 @@ class TestNestedSet(FrappeTestCase): def test_merge_groups(self): global records el = {"some_fieldname": "Parent 2", "parent_test_tree_doctype": "Root Node", "is_group": 1} - frappe.rename_doc("Test Tree DocType", "Parent 2", "Parent 1", merge=True) + frappe.rename_doc(TEST_DOCTYPE, "Parent 2", "Parent 1", merge=True) records.remove(el) self.test_basic_tree() @@ -232,7 +228,7 @@ class TestNestedSet(FrappeTestCase): el = {"some_fieldname": "Child 3", "parent_test_tree_doctype": "Parent 2", "is_group": 0} frappe.rename_doc( - "Test Tree DocType", + TEST_DOCTYPE, "Child 3", "Child 2", merge=True, @@ -242,17 +238,17 @@ class TestNestedSet(FrappeTestCase): def test_merge_leaf_into_group(self): with self.assertRaises(NestedSetInvalidMergeError): - frappe.rename_doc("Test Tree DocType", "Child 1", "Parent 1", merge=True) + frappe.rename_doc(TEST_DOCTYPE, "Child 1", "Parent 1", merge=True) def test_merge_group_into_leaf(self): with self.assertRaises(NestedSetInvalidMergeError): - frappe.rename_doc("Test Tree DocType", "Parent 1", "Child 1", merge=True) + frappe.rename_doc(TEST_DOCTYPE, "Parent 1", "Child 1", merge=True) def test_root_deletion(self): for doc in ["Child 3", "Child 2", "Child 1", "Parent 2", "Parent 1"]: - frappe.delete_doc("Test Tree DocType", doc) + frappe.delete_doc(TEST_DOCTYPE, doc) - root_node = frappe.get_doc("Test Tree DocType", "Root Node") + root_node = frappe.get_doc(TEST_DOCTYPE, "Root Node") # root deletion with allow_root_deletion # patched as delete_doc create a new instance of Root Node (using get_doc) @@ -263,4 +259,40 @@ class TestNestedSet(FrappeTestCase): # root deletion without allow_root_deletion root_node.delete() - self.assertFalse(frappe.db.exists("Test Tree DocType", "Root Node")) + self.assertFalse(frappe.db.exists(TEST_DOCTYPE, "Root Node")) + + def test_desc_filters(self): + + linked_doctype = ( + new_doctype( + fields=[ + { + "fieldname": "link_field", + "fieldtype": "Link", + "options": TEST_DOCTYPE, + } + ] + ) + .insert() + .name + ) + + record = "Child 1" + + exclusive_filter = {"name": ("descendants of", record)} + inclusive_filter = {"name": ("descendants of (inclusive)", record)} + exclusive_link = {"link_field": ("descendants of", record)} + inclusive_link = {"link_field": ("descendants of (inclusive)", record)} + + # db_query + self.assertNotIn(record, frappe.get_all(TEST_DOCTYPE, exclusive_filter, run=0)) + self.assertIn(record, frappe.get_all(TEST_DOCTYPE, inclusive_filter, run=0)) + self.assertNotIn(record, frappe.get_all(linked_doctype, exclusive_link, run=0)) + self.assertIn(record, frappe.get_all(linked_doctype, inclusive_link, run=0)) + + # QB + self.assertNotIn(record, str(frappe.qb.get_query(TEST_DOCTYPE, filters=exclusive_filter))) + self.assertIn(record, str(frappe.qb.get_query(TEST_DOCTYPE, filters=inclusive_filter))) + + self.assertNotIn(record, str(frappe.qb.get_query(table=linked_doctype, filters=exclusive_link))) + self.assertIn(record, str(frappe.qb.get_query(table=linked_doctype, filters=inclusive_link))) diff --git a/frappe/utils/data.py b/frappe/utils/data.py index a51cdee04a..3fe854bbf6 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -1766,6 +1766,7 @@ def get_filter(doctype: str, f: dict | list | tuple, filters_config=None) -> "fr "fieldtype": } """ + from frappe.database.utils import NestedSetHierarchy from frappe.model import child_table_fields, default_fields, optional_fields if isinstance(f, dict): @@ -1805,14 +1806,10 @@ def get_filter(doctype: str, f: dict | list | tuple, filters_config=None) -> "fr "not in", "is", "between", - "descendants of", - "ancestors of", - "not descendants of", - "not ancestors of", "timespan", "previous", "next", - ) + ) + NestedSetHierarchy if filters_config: additional_operators = []