Merge branch 'develop' into private-custom-html-block

This commit is contained in:
Shariq Ansari 2023-05-30 12:09:41 +05:30 committed by GitHub
commit 96e45d9d2a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 174 additions and 79 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -23,6 +23,7 @@ NestedSetHierarchy = (
"descendants of",
"not ancestors of",
"not descendants of",
"descendants of (inclusive)",
)

View file

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

View file

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

View file

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

View file

@ -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 "";
}

View file

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

View file

@ -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 = []