From 9d8c2b5eff16187d443915de15e101e50b589ae4 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Fri, 8 Apr 2016 17:07:13 +0530 Subject: [PATCH] [enhancements] added frappe.utils.evalute_filter(doc, filter) and ability to hook multiple doc events at the same time --- frappe/__init__.py | 49 ++++++++++++++++++++++-------- frappe/model/db_query.py | 51 ++----------------------------- frappe/model/document.py | 2 +- frappe/test_runner.py | 1 + frappe/tests/test_utils.py | 36 ++++++++++++++++++++++ frappe/utils/data.py | 62 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 139 insertions(+), 62 deletions(-) create mode 100644 frappe/tests/test_utils.py diff --git a/frappe/__init__.py b/frappe/__init__.py index 8eadea9b18..c506c8775e 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -677,6 +677,22 @@ def get_installed_apps(sort=False, frappe_last=False): return installed +def get_doc_hooks(): + '''Returns hooked methods for given doc. It will expand the dict tuple if required.''' + if not hasattr(local, 'doc_events_hooks'): + hooks = get_hooks('doc_events', {}) + out = {} + for key, value in hooks.iteritems(): + if isinstance(key, tuple): + for doctype in key: + append_hook(out, doctype, value) + else: + append_hook(out, key, value) + + local.doc_events_hooks = out + + return local.doc_events_hooks + def get_hooks(hook=None, default=None, app_name=None): """Get hooks via `app/hooks.py` @@ -700,19 +716,6 @@ def get_hooks(hook=None, default=None, app_name=None): append_hook(hooks, key, getattr(app_hooks, key)) return hooks - def append_hook(target, key, value): - if isinstance(value, dict): - target.setdefault(key, {}) - for inkey in value: - append_hook(target[key], inkey, value[inkey]) - else: - append_to_list(target, key, value) - - def append_to_list(target, key, value): - target.setdefault(key, []) - if not isinstance(value, list): - value = [value] - target[key].extend(value) if app_name: hooks = _dict(load_app_hooks(app_name)) @@ -724,6 +727,26 @@ def get_hooks(hook=None, default=None, app_name=None): else: return hooks +def append_hook(target, key, value): + '''appends a hook to the the target dict. + + If the hook key, exists, it will make it a key. + + If the hook value is a dict, like doc_events, it will + listify the values against the key. + ''' + if isinstance(value, dict): + # dict? make a list of values against each key + target.setdefault(key, {}) + for inkey in value: + append_hook(target[key], inkey, value[inkey]) + else: + # make a list + target.setdefault(key, []) + if not isinstance(value, list): + value = [value] + target[key].extend(value) + def setup_module_map(): """Rebuild map of all modules (internal).""" _cache = cache() diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index c7ac6deab7..1dc9e8713c 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -8,7 +8,7 @@ import frappe, json import frappe.defaults import frappe.share import frappe.permissions -from frappe.utils import flt, cint, getdate, get_datetime, get_time +from frappe.utils import flt, cint, getdate, get_datetime, get_time, make_filter_tuple, get_filter from frappe import _ from frappe.model import optional_fields @@ -141,15 +141,9 @@ class DatabaseQuery(object): fdict = filters filters = [] for key, value in fdict.iteritems(): - filters.append(self.make_filter_tuple(key, value)) + filters.append(make_filter_tuple(self.doctype, key, value)) setattr(self, filter_name, filters) - def make_filter_tuple(self, key, value): - if isinstance(value, (list, tuple)): - return [self.doctype, key, value[0], value[1]] - else: - return [self.doctype, key, "=", value] - def extract_tables(self): """extract tables from fields""" self.tables = ['`tab' + self.doctype + '`'] @@ -239,7 +233,7 @@ class DatabaseQuery(object): ifnull(`tabDocType`.`fieldname`, fallback) operator "value" """ - f = self.get_filter(f) + f = get_filter(self.doctype, f) tname = ('`tab' + f.doctype + '`') if not tname in self.tables: @@ -296,45 +290,6 @@ class DatabaseQuery(object): return condition - def get_filter(self, f): - """Returns a _dict like - - { - "doctype": "DocType", - "fieldname": "fieldname", - "operator": "=", - "value": "value" - } - - """ - if isinstance(f, dict): - key, value = f.items()[0] - f = self.make_filter_tuple(key, value) - - if not isinstance(f, (list, tuple)): - frappe.throw("Filter must be a tuple or list (in a list)") - - if len(f) == 3: - f = (self.doctype, f[0], f[1], f[2]) - - elif len(f) != 4: - frappe.throw("Filter must have 4 values (doctype, fieldname, operator, value): {0}".format(str(f))) - - if not f[2]: - # if operator is missing - f[2] = "=" - - valid_operators = ("=", "!=", ">", "<", ">=", "<=", "like", "not like", "in", "not in") - if f[2] not in valid_operators: - frappe.throw("Operator must be one of {0}".format(", ".join(valid_operators))) - - return frappe._dict({ - "doctype": f[0], - "fieldname": f[1], - "operator": f[2], - "value": f[3] - }) - def build_match_conditions(self, as_condition=True): """add match conditions if applicable""" self.match_filters = [] diff --git a/frappe/model/document.py b/frappe/model/document.py index da959ee1ea..ec64112207 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -741,7 +741,7 @@ class Document(BaseDocument): def composer(self, *args, **kwargs): hooks = [] method = f.__name__ - doc_events = frappe.get_hooks("doc_events", {}) + doc_events = frappe.get_doc_hooks() for handler in doc_events.get(self.doctype, {}).get(method, []) \ + doc_events.get("*", {}).get(method, []): hooks.append(frappe.get_attr(handler)) diff --git a/frappe/test_runner.py b/frappe/test_runner.py index 33a7f41160..12651f65dc 100644 --- a/frappe/test_runner.py +++ b/frappe/test_runner.py @@ -266,6 +266,7 @@ def make_test_objects(doctype, test_records, verbose=None): if docstatus == 1: d.submit() + except frappe.NameError: pass diff --git a/frappe/tests/test_utils.py b/frappe/tests/test_utils.py new file mode 100644 index 0000000000..713c8a636c --- /dev/null +++ b/frappe/tests/test_utils.py @@ -0,0 +1,36 @@ +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt +from __future__ import unicode_literals + +import unittest + +from frappe.utils import evaluate_filters + +class TestFilters(unittest.TestCase): + def test_simple_dict(self): + self.assertTrue(evaluate_filters({'doctype': 'User', 'status': 'Open'}, {'status': 'Open'})) + self.assertFalse(evaluate_filters({'doctype': 'User', 'status': 'Open'}, {'status': 'Closed'})) + + def test_multiple_dict(self): + self.assertTrue(evaluate_filters({'doctype': 'User', 'status': 'Open', 'name': 'Test 1'}, + {'status': 'Open', 'name':'Test 1'})) + self.assertFalse(evaluate_filters({'doctype': 'User', 'status': 'Open', 'name': 'Test 1'}, + {'status': 'Closed', 'name': 'Test 1'})) + + def test_list_filters(self): + self.assertTrue(evaluate_filters({'doctype': 'User', 'status': 'Open', 'name': 'Test 1'}, + [{'status': 'Open'}, {'name':'Test 1'}])) + self.assertFalse(evaluate_filters({'doctype': 'User', 'status': 'Open', 'name': 'Test 1'}, + [{'status': 'Open'}, {'name':'Test 2'}])) + + def test_list_filters_as_list(self): + self.assertTrue(evaluate_filters({'doctype': 'User', 'status': 'Open', 'name': 'Test 1'}, + [['status', '=', 'Open'], ['name', '=', 'Test 1']])) + self.assertFalse(evaluate_filters({'doctype': 'User', 'status': 'Open', 'name': 'Test 1'}, + [['status', '=', 'Open'], ['name', '=', 'Test 2']])) + + def test_lt_gt(self): + self.assertTrue(evaluate_filters({'doctype': 'User', 'status': 'Open', 'age': 20}, + {'status': 'Open', 'age': ('>', 10)})) + self.assertFalse(evaluate_filters({'doctype': 'User', 'status': 'Open', 'age': 20}, + {'status': 'Open', 'age': ('>', 30)})) diff --git a/frappe/utils/data.py b/frappe/utils/data.py index 720b694f7f..2712914b71 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -613,6 +613,23 @@ operator_map = { "None": lambda (a, b): (not a) and True or False } +def evaluate_filters(doc, filters): + '''Returns true if doc matches filters''' + if isinstance(filters, dict): + for key, value in filters.iteritems(): + f = get_filter(None, {key:value}) + if not compare(doc.get(f.fieldname), f.operator, f.value): + return False + + elif isinstance(filters, (list, tuple)): + for d in filters: + f = get_filter(None, d) + if not compare(doc.get(f.fieldname), f.operator, f.value): + return False + + return True + + def compare(val1, condition, val2): ret = False if condition in operator_map: @@ -620,6 +637,51 @@ def compare(val1, condition, val2): return ret +def get_filter(doctype, f): + """Returns a _dict like + + { + "doctype": + "fieldname": + "operator": + "value": + } + """ + if isinstance(f, dict): + key, value = f.items()[0] + f = make_filter_tuple(doctype, key, value) + + if not isinstance(f, (list, tuple)): + frappe.throw("Filter must be a tuple or list (in a list)") + + if len(f) == 3: + f = (doctype, f[0], f[1], f[2]) + + elif len(f) != 4: + frappe.throw("Filter must have 4 values (doctype, fieldname, operator, value): {0}".format(str(f))) + + if not f[2]: + # if operator is missing + f[2] = "=" + + valid_operators = ("=", "!=", ">", "<", ">=", "<=", "like", "not like", "in", "not in") + if f[2] not in valid_operators: + frappe.throw("Operator must be one of {0}".format(", ".join(valid_operators))) + + return frappe._dict({ + "doctype": f[0], + "fieldname": f[1], + "operator": f[2], + "value": f[3] + }) + +def make_filter_tuple(doctype, key, value): + '''return a filter tuple like [doctype, key, operator, value]''' + if isinstance(value, (list, tuple)): + return [doctype, key, value[0], value[1]] + else: + return [doctype, key, "=", value] + def scrub_urls(html): html = expand_relative_urls(html) # encoding should be responsibility of the composer