From b191f6cfc30225709fdfe2b39640248b69c38590 Mon Sep 17 00:00:00 2001 From: Yunfan Zhang Date: Wed, 1 Aug 2018 12:20:27 -0400 Subject: [PATCH] Add AND filter to related search. Signed-off-by: Yunfan Zhang --- awx/api/filters.py | 26 ++++++--- .../functional/api/test_search_filter.py | 54 +++++++++++++++++++ .../shared/smart-search/queryset.service.js | 5 +- 3 files changed, 77 insertions(+), 8 deletions(-) create mode 100644 awx/main/tests/functional/api/test_search_filter.py diff --git a/awx/api/filters.py b/awx/api/filters.py index 81290c377b..8f883191f3 100644 --- a/awx/api/filters.py +++ b/awx/api/filters.py @@ -4,6 +4,7 @@ # Python import re import json +from functools import reduce # Django from django.core.exceptions import FieldError, ValidationError @@ -238,7 +239,11 @@ class FieldLookupBackend(BaseFilterBackend): or_filters = [] chain_filters = [] role_filters = [] - search_filters = [] + search_filters = {} + # Can only have two values: 'AND', 'OR' + # If 'AND' is used, an iterm must satisfy all condition to show up in the results. + # If 'OR' is used, an item just need to satisfy one condition to appear in results. + search_filter_relation = 'OR' for key, values in request.query_params.lists(): if key in self.RESERVED_NAMES: continue @@ -262,11 +267,13 @@ class FieldLookupBackend(BaseFilterBackend): # Search across related objects. if key.endswith('__search'): + if values and ',' in values[0]: + search_filter_relation = 'AND' + values = reduce(lambda list1, list2: list1 + list2, [i.split(',') for i in values]) for value in values: search_value, new_keys = self.value_to_python(queryset.model, key, force_text(value)) assert isinstance(new_keys, list) - for new_key in new_keys: - search_filters.append((new_key, search_value)) + search_filters[search_value] = new_keys continue # Custom chain__ and or__ filters, mutually exclusive (both can @@ -355,11 +362,18 @@ class FieldLookupBackend(BaseFilterBackend): else: q |= Q(**{k:v}) args.append(q) - if search_filters: + if search_filters and search_filter_relation == 'OR': q = Q() - for k,v in search_filters: - q |= Q(**{k:v}) + for term, constrains in search_filters.iteritems(): + for constrain in constrains: + q |= Q(**{constrain: term}) args.append(q) + elif search_filters and search_filter_relation == 'AND': + for term, constrains in search_filters.iteritems(): + q_chain = Q() + for constrain in constrains: + q_chain |= Q(**{constrain: term}) + queryset = queryset.filter(q_chain) for n,k,v in chain_filters: if n: q = ~Q(**{k:v}) diff --git a/awx/main/tests/functional/api/test_search_filter.py b/awx/main/tests/functional/api/test_search_filter.py new file mode 100644 index 0000000000..4e67ed834a --- /dev/null +++ b/awx/main/tests/functional/api/test_search_filter.py @@ -0,0 +1,54 @@ +# Python +import pytest +import json + +# Django Rest Framework +from rest_framework.test import APIRequestFactory + +# AWX +from awx.api.views import HostList +from awx.main.models import Host, Group, Inventory +from awx.api.versioning import reverse + + +@pytest.mark.django_db +class TestSearchFilter: + def test_related_research_filter_relation(self, admin): + inv = Inventory.objects.create(name="inv") + group1 = Group.objects.create(name="g1", inventory=inv) + group2 = Group.objects.create(name="g2", inventory=inv) + host1 = Host.objects.create(name="host1", inventory=inv) + host2 = Host.objects.create(name="host2", inventory=inv) + host3 = Host.objects.create(name="host3", inventory=inv) + host1.groups.add(group1) + host2.groups.add(group1) + host2.groups.add(group2) + host3.groups.add(group2) + host1.save() + host2.save() + host3.save() + # Login the client + factory = APIRequestFactory() + # Actually test the endpoint. + host_list_url = reverse('api:host_list') + + # Test if the OR releation works. + request = factory.get(host_list_url, data={'groups__search': ['g1', 'g2']}) + request.user = admin + response = HostList.as_view()(request) + response.render() + result = json.loads(response.content) + assert result['count'] == 3 + expected_hosts = ['host1', 'host2', 'host3'] + for i in result['results']: + expected_hosts.remove(i['name']) + assert not expected_hosts + + # Test if the AND relation works. + request = factory.get(host_list_url, data={'groups__search': ['g1,g2']}) + request.user = admin + response = HostList.as_view()(request) + response.render() + result = json.loads(response.content) + assert result['count'] == 1 + assert result['results'][0]['name'] == 'host2' diff --git a/awx/ui/client/src/shared/smart-search/queryset.service.js b/awx/ui/client/src/shared/smart-search/queryset.service.js index d55f4e27f0..392ead279b 100644 --- a/awx/ui/client/src/shared/smart-search/queryset.service.js +++ b/awx/ui/client/src/shared/smart-search/queryset.service.js @@ -80,15 +80,16 @@ function QuerysetService ($q, Rest, ProcessErrors, $rootScope, Wait, DjangoSearc }, // like encodeQueryset, but return an actual unstringified API-consumable http param object encodeQuerysetObject(params) { + console.log(params); return _.reduce(params, (obj, value, key) => { const encodedTerms = this.encodeTerms(value, key); - + console.log(encodedTerms); for (let encodedIndex in encodedTerms) { const [encodedKey, encodedValue] = encodedTerms[encodedIndex]; obj[encodedKey] = obj[encodedKey] || []; obj[encodedKey].push(encodedValue); } - + console.log(obj); return obj; }, {}); },