From bac1e8b4fec5e4ab6ec2e3057b3c250e6940aba5 Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Fri, 19 May 2017 16:01:11 -0400 Subject: [PATCH] Extend SmartFilter to expand search and related search fields --- awx/api/generics.py | 7 +--- awx/main/tests/unit/utils/test_filters.py | 22 +++++++++++- awx/main/utils/common.py | 11 +++++- awx/main/utils/filters.py | 42 +++++++++++++++++++---- 4 files changed, 67 insertions(+), 15 deletions(-) diff --git a/awx/api/generics.py b/awx/api/generics.py index 4045e5780a..823af3cf56 100644 --- a/awx/api/generics.py +++ b/awx/api/generics.py @@ -293,12 +293,7 @@ class ListAPIView(generics.ListAPIView, GenericAPIView): @property def search_fields(self): - fields = [] - for field in self.model._meta.fields: - if field.name in ('username', 'first_name', 'last_name', 'email', - 'name', 'description'): - fields.append(field.name) - return fields + return get_search_fields(self.model) @property def related_search_fields(self): diff --git a/awx/main/tests/unit/utils/test_filters.py b/awx/main/tests/unit/utils/test_filters.py index 910217deab..cfac7e9e96 100644 --- a/awx/main/tests/unit/utils/test_filters.py +++ b/awx/main/tests/unit/utils/test_filters.py @@ -2,6 +2,7 @@ # Python import pytest import mock +from collections import namedtuple # AWX from awx.main.utils.filters import SmartFilter @@ -10,6 +11,10 @@ from awx.main.utils.filters import SmartFilter from django.db.models import Q +Field = namedtuple('Field', 'name') +Meta = namedtuple('Meta', 'fields') + + class mockObjects: def filter(self, *args, **kwargs): return Q(*args, **kwargs) @@ -19,9 +24,10 @@ class mockHost: def __init__(self): print("Host mock created") self.objects = mockObjects() + self._meta = Meta(fields=(Field(name='name'), Field(name='description'))) -@mock.patch('awx.main.utils.filters.get_host_model', return_value=mockHost()) +@mock.patch('awx.main.utils.filters.get_model', return_value=mockHost()) class TestSmartFilterQueryFromString(): @pytest.mark.parametrize("filter_string,q_expected", [ ('facts__facts__blank=""', Q(**{u"facts__facts__blank": u""})), @@ -109,6 +115,20 @@ class TestSmartFilterQueryFromString(): assert unicode(q) == unicode(q_expected) + @pytest.mark.parametrize("filter_string,q_expected", [ + ('search=foo', Q(**{u"name": u"foo"}) | Q(**{ u"description": u"foo"})), + ('group__search=foo', Q(**{u"group__name": u"foo"}) | Q(**{u"group__description": u"foo"})), + ('search=foo and group__search=foo', Q( + Q(**{u"name": u"foo"}) | Q(**{ u"description": u"foo"}), + Q(**{u"group__name": u"foo"}) | Q(**{u"group__description": u"foo"}))), + ('search=foo or ansible_facts__a=null', + (Q(**{u"name": u"foo"}) | Q(**{u"description": u"foo"})) | + Q(**{u"ansible_facts__contains": {u"a": u"null"}})), + ]) + def test_search_related_fields(self, mock_get_host_model, filter_string, q_expected): + q = SmartFilter.query_from_string(filter_string) + assert unicode(q) == unicode(q_expected) + ''' #('"facts__quoted_val"="f\"oo"', 1), #('facts__facts__arr[]="foo"', 1), diff --git a/awx/main/utils/common.py b/awx/main/utils/common.py index b56c954f9c..58068a2e2c 100644 --- a/awx/main/utils/common.py +++ b/awx/main/utils/common.py @@ -45,7 +45,7 @@ __all__ = ['get_object_or_400', 'get_object_or_403', 'camelcase_to_underscore', 'ignore_inventory_computed_fields', 'ignore_inventory_group_removal', '_inventory_updates', 'get_pk_from_dict', 'getattrd', 'NoDefaultProvided', 'get_current_apps', 'set_current_apps', 'OutputEventFilter', - 'callback_filter_out_ansible_extra_vars',] + 'callback_filter_out_ansible_extra_vars', 'get_search_fields',] def get_object_or_400(klass, *args, **kwargs): @@ -862,3 +862,12 @@ def callback_filter_out_ansible_extra_vars(extra_vars): if not key.startswith('ansible_'): extra_vars_redacted[key] = value return extra_vars_redacted + + +def get_search_fields(model): + fields = [] + for field in model._meta.fields: + if field.name in ('username', 'first_name', 'last_name', 'email', + 'name', 'description'): + fields.append(field.name) + return fields diff --git a/awx/main/utils/filters.py b/awx/main/utils/filters.py index 845d75a852..1b9b5b40dc 100644 --- a/awx/main/utils/filters.py +++ b/awx/main/utils/filters.py @@ -10,6 +10,8 @@ from pyparsing import ( import django +from awx.main.utils.common import get_search_fields + __all__ = ['SmartFilter'] unicode_spaces = [unichr(c) for c in xrange(sys.maxunicode) if unichr(c).isspace()] @@ -31,8 +33,8 @@ def string_to_type(t): return t -def get_host_model(): - return django.apps.apps.get_model('main', 'host') +def get_model(name): + return django.apps.apps.get_model('main', name) class SmartFilter(object): @@ -43,11 +45,16 @@ class SmartFilter(object): kwargs = dict() k, v = self._extract_key_value(t) k, v = self._json_path_to_contains(k, v) - kwargs[k] = v - # Avoid import circular dependency - Host = get_host_model() - self.result = Host.objects.filter(**kwargs) + Host = get_model('host') + search_kwargs = self._expand_search(k, v) + if search_kwargs: + kwargs.update(search_kwargs) + q = reduce(lambda x, y: x | y, [django.db.models.Q(**{u'%s' % _k:_v}) for _k, _v in kwargs.items()]) + self.result = q + else: + kwargs[k] = v + self.result = Host.objects.filter(**kwargs) def strip_quotes_traditional_logic(self, v): if type(v) is unicode and v.startswith('"') and v.endswith('"'): @@ -145,6 +152,28 @@ class SmartFilter(object): return (k, v) + def _expand_search(self, k, v): + if 'search' not in k: + return None + + model, relation = None, None + if k == 'search': + model = get_model('host') + elif k.endswith('__search'): + relation = k.split('__')[0] + model = get_model(relation) + + search_kwargs = {} + if model is not None: + search_fields = get_search_fields(model) + for field in search_fields: + if relation is not None: + k = '{0}__{1}'.format(relation, field) + else: + k = field + search_kwargs[k] = v + return search_kwargs + class BoolBinOp(object): def __init__(self, t): @@ -206,7 +235,6 @@ class SmartFilter(object): try: res = boolExpr.parseString('(' + filter_string + ')') - #except ParseException as e: except Exception: raise RuntimeError(u"Invalid query %s" % filter_string_raw)