* refactored code to add support in filters to get descendant or ancestor for tree type documents * added semicolon and comments to imporve code quality * refactored code to add not ancestor and not descendants of filters * added test cases for ancestors of and descendants of * Translate labelsversion-14
@@ -45,8 +45,8 @@ def get_bootinfo(): | |||||
bootinfo.all_domains = [d.get("name") for d in frappe.get_all("Domain")] | bootinfo.all_domains = [d.get("name") for d in frappe.get_all("Domain")] | ||||
bootinfo.module_app = frappe.local.module_app | bootinfo.module_app = frappe.local.module_app | ||||
bootinfo.single_types = frappe.db.sql_list("""select name from tabDocType | |||||
where issingle=1""") | |||||
bootinfo.single_types = [d.name for d in frappe.get_all('DocType', {'issingle': 1})] | |||||
bootinfo.nested_set_doctypes = [d.parent for d in frappe.get_all('DocField', {'fieldname': 'lft'}, ['parent'])] | |||||
add_home_page(bootinfo, doclist) | add_home_page(bootinfo, doclist) | ||||
bootinfo.page_info = get_allowed_pages() | bootinfo.page_info = get_allowed_pages() | ||||
load_translations(bootinfo) | load_translations(bootinfo) | ||||
@@ -328,7 +328,39 @@ class DatabaseQuery(object): | |||||
can_be_null = True | can_be_null = True | ||||
# prepare in condition | # prepare in condition | ||||
if f.operator.lower() in ('in', 'not in'): | |||||
if f.operator.lower() in ('ancestors of', 'descendants of', 'not ancestors of', 'not descendants of'): | |||||
values = f.value or '' | |||||
# TODO: handle list and tuple | |||||
# if not isinstance(values, (list, tuple)): | |||||
# values = values.split(",") | |||||
ref_doctype = f.doctype | |||||
if frappe.get_meta(f.doctype).get_field(f.fieldname) is not None : | |||||
ref_doctype = frappe.get_meta(f.doctype).get_field(f.fieldname).options | |||||
result=[] | |||||
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.db.sql_list("""select name from `tab{0}` | |||||
where lft>%s and rgt<%s order by lft asc""".format(ref_doctype), (lft, rgt)) | |||||
else : | |||||
# Get ancestor elements of a DocType with a tree structure | |||||
result = frappe.db.sql_list("""select name from `tab{0}` | |||||
where lft<%s and rgt>%s order by lft desc""".format(ref_doctype), (lft, rgt)) | |||||
fallback = "''" | |||||
value = (frappe.db.escape((v or '').strip(), percent=False) for v in result) | |||||
value = '("{0}")'.format('", "'.join(value)) | |||||
# changing operator to IN as the above code fetches all the parent / child values and convert into tuple | |||||
# which can be directly used with IN operator to query. | |||||
f.operator = 'not in' if f.operator.lower() in ('not ancestors of', 'not descendants of') else 'in' | |||||
elif f.operator.lower() in ('in', 'not in'): | |||||
values = f.value or '' | values = f.value or '' | ||||
if not isinstance(values, (list, tuple)): | if not isinstance(values, (list, tuple)): | ||||
values = values.split(",") | values = values.split(",") | ||||
@@ -14,6 +14,10 @@ | |||||
<option value=">=">{%= __(">=") %}</option> | <option value=">=">{%= __(">=") %}</option> | ||||
<option value="<=">{%= __("<=") %}</option> | <option value="<=">{%= __("<=") %}</option> | ||||
<option value="Between">{%= __("Between") %}</option> | <option value="Between">{%= __("Between") %}</option> | ||||
<option value="descendants of">{%= __("Descendants Of") %}</option> | |||||
<option value="not descendants of">{%= __("Not Descendants Of") %}</option> | |||||
<option value="ancestors of">{%= __("Ancestors Of") %}</option> | |||||
<option value="not ancestors of">{%= __("Not Ancestors Of") %}</option> | |||||
</select> | </select> | ||||
</div> | </div> | ||||
<div class="col-sm-6 col-xs-12"> | <div class="col-sm-6 col-xs-12"> | ||||
@@ -17,7 +17,9 @@ frappe.ui.Filter = class { | |||||
["<", "<"], | ["<", "<"], | ||||
[">=", ">="], | [">=", ">="], | ||||
["<=", "<="], | ["<=", "<="], | ||||
["Between", __("Between")] | |||||
["Between", __("Between")], | |||||
["descendants of", __("Descendants Of")], | |||||
["ancestors of", __("Ancestors Of")] | |||||
]; | ]; | ||||
this.invalid_condition_map = { | this.invalid_condition_map = { | ||||
Date: ['like', 'not like'], | Date: ['like', 'not like'], | ||||
@@ -185,7 +187,7 @@ frappe.ui.Filter = class { | |||||
make_field(df, old_fieldtype) { | make_field(df, old_fieldtype) { | ||||
let old_text = this.field ? this.field.get_value() : null; | let old_text = this.field ? this.field.get_value() : null; | ||||
this.hide_invalid_conditions(df.fieldtype, df.original_type); | this.hide_invalid_conditions(df.fieldtype, df.original_type); | ||||
this.hide_nested_set_conditions(df); | |||||
let field_area = this.filter_edit_area.find('.filter-field').empty().get(0); | let field_area = this.filter_edit_area.find('.filter-field').empty().get(0); | ||||
let f = frappe.ui.form.make_control({ | let f = frappe.ui.form.make_control({ | ||||
df: df, | df: df, | ||||
@@ -216,7 +218,6 @@ frappe.ui.Filter = class { | |||||
this.hidden | this.hidden | ||||
]; | ]; | ||||
} | } | ||||
get_selected_value() { | get_selected_value() { | ||||
return this.utils.get_selected_value(this.field, this.get_condition()); | return this.utils.get_selected_value(this.field, this.get_condition()); | ||||
} | } | ||||
@@ -229,6 +230,7 @@ frappe.ui.Filter = class { | |||||
let $condition_field = this.filter_edit_area.find('.condition'); | let $condition_field = this.filter_edit_area.find('.condition'); | ||||
$condition_field.val(condition); | $condition_field.val(condition); | ||||
if(trigger_change) $condition_field.change(); | if(trigger_change) $condition_field.change(); | ||||
} | } | ||||
make_tag() { | make_tag() { | ||||
@@ -290,6 +292,20 @@ frappe.ui.Filter = class { | |||||
); | ); | ||||
} | } | ||||
} | } | ||||
hide_nested_set_conditions(df) { | |||||
if ( !( df.fieldtype == "Link" && frappe.boot.nested_set_doctypes.includes(df.options))) { | |||||
this.filter_edit_area.find(`.condition option[value="descendants of"]`).hide(); | |||||
this.filter_edit_area.find(`.condition option[value="not descendants of"]`).hide(); | |||||
this.filter_edit_area.find(`.condition option[value="ancestors of"]`).hide(); | |||||
this.filter_edit_area.find(`.condition option[value="not ancestors of"]`).hide(); | |||||
}else { | |||||
this.filter_edit_area.find(`.condition option[value="descendants of"]`).show(); | |||||
this.filter_edit_area.find(`.condition option[value="not descendants of"]`).show(); | |||||
this.filter_edit_area.find(`.condition option[value="ancestors of"]`).show(); | |||||
this.filter_edit_area.find(`.condition option[value="not ancestors of"]`).show(); | |||||
} | |||||
} | |||||
}; | }; | ||||
frappe.ui.filter_utils = { | frappe.ui.filter_utils = { | ||||
@@ -374,7 +390,7 @@ frappe.ui.filter_utils = { | |||||
} else if(['Text','Small Text','Text Editor','Code','Tag','Comments', | } else if(['Text','Small Text','Text Editor','Code','Tag','Comments', | ||||
'Dynamic Link','Read Only','Assign'].indexOf(df.fieldtype)!=-1) { | 'Dynamic Link','Read Only','Assign'].indexOf(df.fieldtype)!=-1) { | ||||
df.fieldtype = 'Data'; | df.fieldtype = 'Data'; | ||||
} else if(df.fieldtype=='Link' && ['=', '!='].indexOf(condition)==-1) { | |||||
} else if(df.fieldtype=='Link' && ['=', '!=', 'descendants of', 'ancestors of', 'not descendants of', 'not ancestors of'].indexOf(condition)==-1) { | |||||
df.fieldtype = 'Data'; | df.fieldtype = 'Data'; | ||||
} | } | ||||
if(df.fieldtype==="Data" && (df.options || "").toLowerCase()==="email") { | if(df.fieldtype==="Data" && (df.options || "").toLowerCase()==="email") { | ||||
@@ -214,6 +214,87 @@ class TestReportview(unittest.TestCase): | |||||
], order_by='creation') | ], order_by='creation') | ||||
self.assertTrue('DefaultValue' in [d['name'] for d in out]) | self.assertTrue('DefaultValue' in [d['name'] for d in out]) | ||||
def test_of_not_of_descendant_ancestors(self): | |||||
clear_user_permissions_for_doctype("File") | |||||
delete_test_file_hierarchy() # delete already existing folders | |||||
from frappe.core.doctype.file.file import create_new_folder | |||||
create_new_folder('level1-A', 'Home') | |||||
create_new_folder('level2-A', 'Home/level1-A') | |||||
create_new_folder('level2-B', 'Home/level1-A') | |||||
create_new_folder('level3-A', 'Home/level1-A/level2-A') | |||||
create_new_folder('level1-B', 'Home') | |||||
create_new_folder('level2-A', 'Home/level1-B') | |||||
# in descendants filter | |||||
data = frappe.get_all('File', {'name': ('descendants of', 'Home/level1-A/level2-A')}) | |||||
self.assertTrue({"name": "Home/level1-A/level2-A/level3-A"} in data) | |||||
data = frappe.get_all('File', {'name': ('descendants of', 'Home/level1-A')}) | |||||
self.assertTrue({"name": "Home/level1-A/level2-A/level3-A"} in data) | |||||
self.assertTrue({"name": "Home/level1-A/level2-A"} in data) | |||||
self.assertTrue({"name": "Home/level1-A/level2-B"} in data) | |||||
self.assertFalse({"name": "Home/level1-B"} in data) | |||||
self.assertFalse({"name": "Home/level1-A"} in data) | |||||
self.assertFalse({"name": "Home"} in data) | |||||
# in ancestors of filter | |||||
data = frappe.get_all('File', {'name': ('ancestors of', 'Home/level1-A/level2-A')}) | |||||
self.assertFalse({"name": "Home/level1-A/level2-A/level3-A"} in data) | |||||
self.assertFalse({"name": "Home/level1-A/level2-A"} in data) | |||||
self.assertFalse({"name": "Home/level1-A/level2-B"} in data) | |||||
self.assertFalse({"name": "Home/level1-B"} in data) | |||||
self.assertTrue({"name": "Home/level1-A"} in data) | |||||
self.assertTrue({"name": "Home"} in data) | |||||
data = frappe.get_all('File', {'name': ('ancestors of', 'Home/level1-A')}) | |||||
self.assertFalse({"name": "Home/level1-A/level2-A/level3-A"} in data) | |||||
self.assertFalse({"name": "Home/level1-A/level2-A"} in data) | |||||
self.assertFalse({"name": "Home/level1-A/level2-B"} in data) | |||||
self.assertFalse({"name": "Home/level1-B"} in data) | |||||
self.assertFalse({"name": "Home/level1-A"} in data) | |||||
self.assertTrue({"name": "Home"} in data) | |||||
# not descendants filter | |||||
data = frappe.get_all('File', {'name': ('not descendants of', 'Home/level1-A/level2-A')}) | |||||
self.assertFalse({"name": "Home/level1-A/level2-A/level3-A"} in data) | |||||
self.assertTrue({"name": "Home/level1-A/level2-A"} in data) | |||||
self.assertTrue({"name": "Home/level1-A/level2-B"} in data) | |||||
self.assertTrue({"name": "Home/level1-A"} in data) | |||||
self.assertTrue({"name": "Home"} in data) | |||||
data = frappe.get_all('File', {'name': ('not descendants of', 'Home/level1-A')}) | |||||
self.assertFalse({"name": "Home/level1-A/level2-A/level3-A"} in data) | |||||
self.assertFalse({"name": "Home/level1-A/level2-A"} in data) | |||||
self.assertFalse({"name": "Home/level1-A/level2-B"} in data) | |||||
self.assertTrue({"name": "Home/level1-B"} in data) | |||||
self.assertTrue({"name": "Home/level1-A"} in data) | |||||
self.assertTrue({"name": "Home"} in data) | |||||
# not ancestors of filter | |||||
data = frappe.get_all('File', {'name': ('not ancestors of', 'Home/level1-A/level2-A')}) | |||||
self.assertTrue({"name": "Home/level1-A/level2-A/level3-A"} in data) | |||||
self.assertTrue({"name": "Home/level1-A/level2-A"} in data) | |||||
self.assertTrue({"name": "Home/level1-A/level2-B"} in data) | |||||
self.assertTrue({"name": "Home/level1-B"} in data) | |||||
self.assertTrue({"name": "Home/level1-A"} not in data) | |||||
self.assertTrue({"name": "Home"} not in data) | |||||
data = frappe.get_all('File', {'name': ('not ancestors of', 'Home/level1-A')}) | |||||
self.assertTrue({"name": "Home/level1-A/level2-A/level3-A"} in data) | |||||
self.assertTrue({"name": "Home/level1-A/level2-A"} in data) | |||||
self.assertTrue({"name": "Home/level1-A/level2-B"} in data) | |||||
self.assertTrue({"name": "Home/level1-B"} in data) | |||||
self.assertTrue({"name": "Home/level1-A"} in data) | |||||
self.assertFalse({"name": "Home"} in data) | |||||
data = frappe.get_all('File', {'name': ('ancestors of', 'Home')}) | |||||
self.assertTrue(len(data) == 0) | |||||
self.assertTrue(len(frappe.get_all('File', {'name': ('not ancestors of', 'Home')})) == len(frappe.get_all('File'))) | |||||
def create_event(subject="_Test Event", starts_on=None): | def create_event(subject="_Test Event", starts_on=None): | ||||
""" create a test event """ | """ create a test event """ | ||||
@@ -818,7 +818,8 @@ def get_filter(doctype, f): | |||||
# if operator is missing | # if operator is missing | ||||
f.operator = "=" | f.operator = "=" | ||||
valid_operators = ("=", "!=", ">", "<", ">=", "<=", "like", "not like", "in", "not in", "between") | |||||
valid_operators = ("=", "!=", ">", "<", ">=", "<=", "like", "not like", "in", "not in", | |||||
"between", "descendants of", "ancestors of", "not descendants of", "not ancestors of") | |||||
if f.operator.lower() not in valid_operators: | if f.operator.lower() not in valid_operators: | ||||
frappe.throw(frappe._("Operator must be one of {0}").format(", ".join(valid_operators))) | frappe.throw(frappe._("Operator must be one of {0}").format(", ".join(valid_operators))) | ||||