def test_self_reference(self): with self.assertRaises(CaseFilterError): build_filter_from_ast(None, parse_xpath("name = other_property")) with self.assertRaises(CaseFilterError): build_filter_from_ast(None, parse_xpath("name > other_property")) with self.assertRaises(CaseFilterError): build_filter_from_ast(None, parse_xpath("parent/name > other_property"))
def xpath_query(self, domain, xpath): """Search for cases using an XPath predicate expression. Enter an arbitrary XPath predicate in the context of the case. Also supports related case lookups. e.g you can do things like: - case properties: "first_name = 'dolores' and last_name = 'abernathy'" - date ranges: "first_came_online >= '2017-08-12' or died <= '2020-11-15" - numeric ranges: "age >= 100 and height < 1.25" - related cases: "mother/first_name = 'maeve' or parent/parent/host/age = 13" """ from corehq.apps.case_search.filter_dsl import ( CaseFilterError, build_filter_from_ast, ) try: return self.filter( build_filter_from_ast(domain, parse_xpath(xpath))) except (TypeError, RuntimeError) as e: raise CaseFilterError( _("Malformed search query: {search_query}").format( search_query=e), None, )
def test_simple_filter(self): parsed = parse_xpath("name = 'farid'") expected_filter = { "nested": { "path": "case_properties", "query": { "bool": { "filter": [ { "bool": { "filter": ( { "term": { "case_properties.key.exact": "name" } }, { "term": { "case_properties.value.exact": "farid" } } ) } } ], "must": { "match_all": {} } } } } } built_filter = build_filter_from_ast("domain", parsed) self.checkQuery(expected_filter, built_filter, is_raw_query=True)
def test_date_comparison(self): parsed = parse_xpath("dob >= '2017-02-12'") expected_filter = { "nested": { "path": "case_properties", "query": { "bool": { "filter": [ { "term": { "case_properties.key.exact": "dob" } } ], "must": { "range": { "case_properties.value.date": { "gte": "2017-02-12" } } } } } } } self.checkQuery(expected_filter, build_filter_from_ast("domain", parsed), is_raw_query=True)
def test_numeric_comparison(self): parsed = parse_xpath("number <= '100.32'") expected_filter = { "nested": { "path": "case_properties", "query": { "bool": { "filter": [ { "term": { "case_properties.key.exact": "number" } } ], "must": { "range": { "case_properties.value.numeric": { "lte": 100.32 } } } } } } } self.checkQuery(expected_filter, build_filter_from_ast("domain", parsed), is_raw_query=True)
def build_filter_from_xpath(domain, xpath, fuzzy=False): """Given an xpath expression this function will generate an Elasticsearch filter""" error_message = _( "We didn't understand what you were trying to do with {}. " "Please try reformatting your query. " "The operators we accept are: {}") context = SearchFilterContext(domain, fuzzy) try: return build_filter_from_ast(parse_xpath(xpath), context) except TypeError as e: text_error = re.search(r"Unknown text '(.+)'", str(e)) if text_error: # This often happens if there is a bad operator (e.g. a ~ b) bad_part = text_error.groups()[0] raise CaseFilterError( error_message.format(bad_part, ", ".join(ALL_OPERATORS)), bad_part) raise CaseFilterError(_("Malformed search query"), None) except RuntimeError as e: # eulxml passes us string errors from YACC lex_token_error = re.search(r"LexToken\((\w+),\w?'(.+)'", str(e)) if lex_token_error: bad_part = lex_token_error.groups()[1] raise CaseFilterError( error_message.format(bad_part, ", ".join(ALL_OPERATORS)), bad_part) raise CaseFilterError(_("Malformed search query"), None)
def build_filter_from_xpath(domain, xpath): error_message = _( "We didn't understand what you were trying to do with {}. " "Please try reformatting your query. " "The operators we accept are: {}") try: return build_filter_from_ast(domain, parse_xpath(xpath)) except TypeError as e: text_error = re.search(r"Unknown text '(.+)'", six.text_type(e)) if text_error: # This often happens if there is a bad operator (e.g. a ~ b) bad_part = text_error.groups()[0] raise CaseFilterError( error_message.format(bad_part, ", ".join(ALL_OPERATORS)), bad_part) raise CaseFilterError(_("Malformed search query"), None) except RuntimeError as e: # eulxml passes us string errors from YACC lex_token_error = re.search(r"LexToken\((\w+),\w?'(.+)'", six.text_type(e)) if lex_token_error: bad_part = lex_token_error.groups()[1] raise CaseFilterError( error_message.format(bad_part, ", ".join(ALL_OPERATORS)), bad_part) raise CaseFilterError(_("Malformed search query"), None)
def test_numeric_equality_negative(self): parsed = parse_xpath("number = -100.32") expected_filter = { "nested": { "path": "case_properties", "query": { "bool": { "filter": [ { "bool": { "filter": ( { "term": { "case_properties.key.exact": "number" } }, { "term": { "case_properties.value.exact": -100.32 } } ) } } ], "must": { "match_all": {} } } } } } built_filter = build_filter_from_ast("domain", parsed) self.checkQuery(expected_filter, built_filter, is_raw_query=True)
def test_simple_filter(self): parsed = parse_xpath("name = 'farid'") expected_filter = { "nested": { "path": "case_properties", "query": { "filtered": { "query": { "match_all": {} }, "filter": { "and": ({ "term": { "case_properties.key.exact": "name" } }, { "term": { "case_properties.value.exact": "farid" } }) } } } } } built_filter = build_filter_from_ast("domain", parsed) self.assertEqual(expected_filter, built_filter)
def test_numeric_equality_negative(self): parsed = parse_xpath("number = -100.32") expected_filter = { "nested": { "path": "case_properties", "query": { "filtered": { "query": { "match_all": {} }, "filter": { "and": ({ "term": { "case_properties.key.exact": "number" } }, { "term": { "case_properties.value.exact": -100.32, } }) } } } } } built_filter = build_filter_from_ast("domain", parsed) self.assertEqual(expected_filter, built_filter)
def test_date_comparison(self): parsed = parse_xpath("dob >= '2017-02-12'") expected_filter = { "nested": { "path": "case_properties", "query": { "filtered": { "filter": { "term": { "case_properties.key.exact": "dob" } }, "query": { "range": { "case_properties.value.date": { "gte": "2017-02-12", } } } } } } } self.assertEqual(expected_filter, build_filter_from_ast("domain", parsed))
def test_nested_parent_lookups(self): parsed = parse_xpath("father/mother/house = 'Tyrell'") expected_filter = { "nested": { "path": "indices", "query": { "filtered": { "query": { "match_all": {}, }, "filter": { "and": ({ "terms": { "indices.referenced_id": [self.parent_case_id], } }, { "term": { "indices.identifier": "father" } }) } } } } } built_filter = build_filter_from_ast(self.domain, parsed) self.assertEqual(expected_filter, built_filter) self.assertEqual([self.child_case_id], CaseSearchES().filter(built_filter).values_list( '_id', flat=True))
def test_nested_parent_lookups(self): parsed = parse_xpath("father/mother/house = 'Tyrell'") expected_filter = { "nested": { "path": "indices", "query": { "filtered": { "query": { "match_all": { }, }, "filter": { "and": ( { "terms": { "indices.referenced_id": [self.parent_case_id], } }, { "term": { "indices.identifier": "father" } } ) } } } } } built_filter = build_filter_from_ast(self.domain, parsed) self.assertEqual(expected_filter, built_filter) self.assertEqual([self.child_case_id], CaseSearchES().filter(built_filter).values_list('_id', flat=True))
def test_numeric_comparison(self): parsed = parse_xpath("number <= '100.32'") expected_filter = { "nested": { "path": "case_properties", "query": { "filtered": { "filter": { "term": { "case_properties.key.exact": "number" } }, "query": { "range": { "case_properties.value.numeric": { "lte": 100.32, } } } } } } } self.assertEqual(expected_filter, build_filter_from_ast("domain", parsed))
def test_parent_lookups(self): parsed = parse_xpath("father/name = 'Mace'") # return all the cases who's parent (relationship named 'father') has case property 'name' = 'Mace' expected_filter = { "nested": { "path": "indices", "query": { "filtered": { "query": { "match_all": { }, }, "filter": { "and": ( { "terms": { "indices.referenced_id": [self.parent_case_id], } }, { "term": { "indices.identifier": "father" } } ) } } } } } built_filter = build_filter_from_ast(self.domain, parsed) self.assertEqual(expected_filter, built_filter) self.assertEqual([self.child_case_id], CaseSearchES().filter(built_filter).values_list('_id', flat=True))
def test_simple_filter(self): parsed = parse_xpath("name = 'farid'") expected_filter = { "nested": { "path": "case_properties", "query": { "filtered": { "query": { "match_all": {} }, "filter": { "and": ( { "term": { "case_properties.key.exact": "name" } }, { "term": { "case_properties.value.exact": "farid" } } ) } } } } } built_filter = build_filter_from_ast("domain", parsed) self.assertEqual(expected_filter, built_filter)
def test_parent_lookups(self): parsed = parse_xpath("father/name = 'Mace'") # return all the cases who's parent (relationship named 'father') has case property 'name' = 'Mace' expected_filter = { "nested": { "path": "indices", "query": { "filtered": { "query": { "match_all": {}, }, "filter": { "and": ({ "terms": { "indices.referenced_id": [self.parent_case_id], } }, { "term": { "indices.identifier": "father" } }) } } } } } built_filter = build_filter_from_ast(self.domain, parsed) self.assertEqual(expected_filter, built_filter) self.assertEqual([self.child_case_id], CaseSearchES().filter(built_filter).values_list( '_id', flat=True))
def test_case_property_existence(self): parsed = parse_xpath("property != ''") expected_filter = { "not": { "or": ( { "not": { "nested": { "path": "case_properties", "query": { "filtered": { "query": { "match_all": { } }, "filter": { "term": { "case_properties.key.exact": "property" } } } } } } }, { "nested": { "path": "case_properties", "query": { "filtered": { "query": { "match_all": { } }, "filter": { "and": ( { "term": { "case_properties.key.exact": "property" } }, { "term": { "case_properties.value.exact": "" } } ) } } } } } ) } } self.assertEqual(expected_filter, build_filter_from_ast("domain", parsed))
def build_filter_from_xpath(domain, xpath): error_message = _( "We didn't understand what you were trying to do with {}. " "Please try reformatting your query. " "The operators we accept are: {}" ) try: return build_filter_from_ast(domain, parse_xpath(xpath)) except TypeError as e: text_error = re.search(r"Unknown text '(.+)'", six.text_type(e)) if text_error: # This often happens if there is a bad operator (e.g. a ~ b) bad_part = text_error.groups()[0] raise CaseFilterError(error_message.format(bad_part, ", ".join(ALL_OPERATORS)), bad_part) raise CaseFilterError(_("Malformed search query"), None) except RuntimeError as e: # eulxml passes us string errors from YACC lex_token_error = re.search(r"LexToken\((\w+),\w?'(.+)'", six.text_type(e)) if lex_token_error: bad_part = lex_token_error.groups()[1] raise CaseFilterError(error_message.format(bad_part, ", ".join(ALL_OPERATORS)), bad_part) raise CaseFilterError(_("Malformed search query"), None)
def get_properties_from_xpath(xpath): return get_properties_from_ast(parse_xpath(xpath))
def test_date_string(self): the_date = '2021-01-01' node = parse_xpath(f"date('{the_date}')") result = date(node, SearchFilterContext("domain")) eq(result, the_date)
def test_arg_validation(self): node = parse_xpath("today('utc')") with self.assertRaises(XPathFunctionException): today(node, SearchFilterContext("domain"))
def test_date_today(self): node = parse_xpath("date(today())") result = date(node, SearchFilterContext("domain")) eq(result, '2021-08-02')
def test_date_int(self): node = parse_xpath("date(15)") result = date(node, SearchFilterContext("domain")) eq(result, '1970-01-16')
def _do_test(expression, expected): node = parse_xpath(expression) result = date_add(node, SearchFilterContext("domain")) eq(result, expected)
def test_get_properties_from_ast(self, expression, expected_values): self.assertEqual(set(expected_values), set(get_properties_from_ast(parse_xpath(expression))))
def test_nested_filter(self): parsed = parse_xpath("(name = 'farid' or name = 'leila') and dob <= '2017-02-11'") expected_filter = { "bool": { "filter": [ { "bool": { "should": [ { "nested": { "path": "case_properties", "query": { "bool": { "filter": [ { "bool": { "filter": [ { "term": { "case_properties.key.exact": "name" } }, { "term": { "case_properties.value.exact": "farid" } } ] } } ], "must": { "match_all": {} } } } } }, { "nested": { "path": "case_properties", "query": { "bool": { "filter": [ { "bool": { "filter": [ { "term": { "case_properties.key.exact": "name" } }, { "term": { "case_properties.value.exact": "leila" } } ] } } ], "must": { "match_all": {} } } } } } ] } }, { "nested": { "path": "case_properties", "query": { "bool": { "filter": [ { "term": { "case_properties.key.exact": "dob" } } ], "must": { "range": { "case_properties.value.date": { "lte": "2017-02-11" } } } } } } } ] } } built_filter = build_filter_from_ast("domain", parsed) self.checkQuery(expected_filter, built_filter, is_raw_query=True)
def _do_test(expression): node = parse_xpath(expression) with assert_raises(XPathFunctionException): date_add(node, SearchFilterContext("domain"))
def test_case_property_existence(self): parsed = parse_xpath("property != ''") expected_filter = { "bool": { "must_not": { "bool": { "should": [ { "bool": { "must_not": { "nested": { "path": "case_properties", "query": { "bool": { "filter": [ { "term": { "case_properties.key.exact": "property" } } ], "must": { "match_all": {} } } } } } } }, { "nested": { "path": "case_properties", "query": { "bool": { "filter": [ { "bool": { "filter": [ { "term": { "case_properties.key.exact": "property" } }, { "term": { "case_properties.value.exact": "" } } ] } } ], "must": { "match_all": {} } } } } } ] } } } } self.checkQuery(expected_filter, build_filter_from_ast("domain", parsed), is_raw_query=True)
def test_nested_filter(self): parsed = parse_xpath("(name = 'farid' or name = 'leila') and dob <= '2017-02-11'") expected_filter = { "and": ( { "or": ( { "nested": { "path": "case_properties", "query": { "filtered": { "query": { "match_all": { } }, "filter": { "and": ( { "term": { "case_properties.key.exact": "name" } }, { "term": { "case_properties.value.exact": "farid" } } ) } } } } }, { "nested": { "path": "case_properties", "query": { "filtered": { "query": { "match_all": { } }, "filter": { "and": ( { "term": { "case_properties.key.exact": "name" } }, { "term": { "case_properties.value.exact": "leila" } } ) } } } } } ) }, { "nested": { "path": "case_properties", "query": { "filtered": { "filter": { "term": { "case_properties.key.exact": "dob" } }, "query": { "range": { "case_properties.value.date": { "lte": "2017-02-11" } } } } } } } ) } built_filter = build_filter_from_ast("domain", parsed) self.assertEqual(expected_filter, built_filter)
def _check(query, msg): node = parse_xpath(query) with assert_raises(XPathFunctionException, msg=msg): _parse_normalize_subcase_query(node)
def test_today_domain_tz(self): node = parse_xpath("today()") result = today(node, SearchFilterContext(self.domain_name)) eq(result, '2021-08-03')
def test_nested_filter(self): parsed = parse_xpath( "(name = 'farid' or name = 'leila') and dob <= '2017-02-11'") expected_filter = { "and": ({ "or": ({ "nested": { "path": "case_properties", "query": { "filtered": { "query": { "match_all": {} }, "filter": { "and": ({ "term": { "case_properties.key.exact": "name" } }, { "term": { "case_properties.value.exact": "farid" } }) } } } } }, { "nested": { "path": "case_properties", "query": { "filtered": { "query": { "match_all": {} }, "filter": { "and": ({ "term": { "case_properties.key.exact": "name" } }, { "term": { "case_properties.value.exact": "leila" } }) } } } } }) }, { "nested": { "path": "case_properties", "query": { "filtered": { "filter": { "term": { "case_properties.key.exact": "dob" } }, "query": { "range": { "case_properties.value.date": { "lte": "2017-02-11" } } } } } } }) } built_filter = build_filter_from_ast("domain", parsed) self.assertEqual(expected_filter, built_filter)
def _check(query, expected): node = parse_xpath(query) result = _parse_normalize_subcase_query(node) eq(result.as_tuple(), expected)
def test_today_no_domain(self): node = parse_xpath("today()") result = today(node, SearchFilterContext("domain")) eq(result, '2021-08-02')