mirror of
https://github.com/ansible/awx.git
synced 2026-05-20 15:27:47 -02:30
Merge pull request #2722 from YunfanZhang42/search-filter
Add AND filter to related search
This commit is contained in:
@@ -4,6 +4,7 @@
|
|||||||
# Python
|
# Python
|
||||||
import re
|
import re
|
||||||
import json
|
import json
|
||||||
|
from functools import reduce
|
||||||
|
|
||||||
# Django
|
# Django
|
||||||
from django.core.exceptions import FieldError, ValidationError
|
from django.core.exceptions import FieldError, ValidationError
|
||||||
@@ -238,7 +239,11 @@ class FieldLookupBackend(BaseFilterBackend):
|
|||||||
or_filters = []
|
or_filters = []
|
||||||
chain_filters = []
|
chain_filters = []
|
||||||
role_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():
|
for key, values in request.query_params.lists():
|
||||||
if key in self.RESERVED_NAMES:
|
if key in self.RESERVED_NAMES:
|
||||||
continue
|
continue
|
||||||
@@ -262,11 +267,13 @@ class FieldLookupBackend(BaseFilterBackend):
|
|||||||
|
|
||||||
# Search across related objects.
|
# Search across related objects.
|
||||||
if key.endswith('__search'):
|
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:
|
for value in values:
|
||||||
search_value, new_keys = self.value_to_python(queryset.model, key, force_text(value))
|
search_value, new_keys = self.value_to_python(queryset.model, key, force_text(value))
|
||||||
assert isinstance(new_keys, list)
|
assert isinstance(new_keys, list)
|
||||||
for new_key in new_keys:
|
search_filters[search_value] = new_keys
|
||||||
search_filters.append((new_key, search_value))
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Custom chain__ and or__ filters, mutually exclusive (both can
|
# Custom chain__ and or__ filters, mutually exclusive (both can
|
||||||
@@ -355,11 +362,18 @@ class FieldLookupBackend(BaseFilterBackend):
|
|||||||
else:
|
else:
|
||||||
q |= Q(**{k:v})
|
q |= Q(**{k:v})
|
||||||
args.append(q)
|
args.append(q)
|
||||||
if search_filters:
|
if search_filters and search_filter_relation == 'OR':
|
||||||
q = Q()
|
q = Q()
|
||||||
for k,v in search_filters:
|
for term, constrains in search_filters.iteritems():
|
||||||
q |= Q(**{k:v})
|
for constrain in constrains:
|
||||||
|
q |= Q(**{constrain: term})
|
||||||
args.append(q)
|
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:
|
for n,k,v in chain_filters:
|
||||||
if n:
|
if n:
|
||||||
q = ~Q(**{k:v})
|
q = ~Q(**{k:v})
|
||||||
|
|||||||
54
awx/main/tests/functional/api/test_search_filter.py
Normal file
54
awx/main/tests/functional/api/test_search_filter.py
Normal file
@@ -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'
|
||||||
@@ -92,6 +92,7 @@ function resolveResource (
|
|||||||
|
|
||||||
if (job_event_search) { // eslint-disable-line camelcase
|
if (job_event_search) { // eslint-disable-line camelcase
|
||||||
const query = qs.encodeQuerysetObject(qs.decodeArr(job_event_search));
|
const query = qs.encodeQuerysetObject(qs.decodeArr(job_event_search));
|
||||||
|
|
||||||
Object.assign(config.params, query);
|
Object.assign(config.params, query);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,49 +44,50 @@ function QuerysetService ($q, Rest, ProcessErrors, $rootScope, Wait, DjangoSearc
|
|||||||
replaceEncodedTokens(value) {
|
replaceEncodedTokens(value) {
|
||||||
return decodeURIComponent(value).replace(/"|'/g, "");
|
return decodeURIComponent(value).replace(/"|'/g, "");
|
||||||
},
|
},
|
||||||
encodeTerms (values, key) {
|
encodeTerms(value, key){
|
||||||
key = this.replaceDefaultFlags(key);
|
key = this.replaceDefaultFlags(key);
|
||||||
|
value = this.replaceDefaultFlags(value);
|
||||||
if (!Array.isArray(values)) {
|
var that = this;
|
||||||
values = [values];
|
if (Array.isArray(value)){
|
||||||
}
|
value = _.uniq(_.flattenDeep(value));
|
||||||
|
let concated = '';
|
||||||
return values
|
angular.forEach(value, function(item){
|
||||||
.map(value => {
|
if(item && typeof item === 'string') {
|
||||||
value = this.replaceDefaultFlags(value);
|
item = that.replaceEncodedTokens(item);
|
||||||
value = this.replaceEncodedTokens(value);
|
}
|
||||||
return [key, value];
|
concated += `${key}=${item}&`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return concated;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if(value && typeof value === 'string') {
|
||||||
|
value = this.replaceEncodedTokens(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${key}=${value}&`;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
// encodes ui-router params from {operand__key__comparator: value} pairs to API-consumable URL
|
// encodes ui-router params from {operand__key__comparator: value} pairs to API-consumable URL
|
||||||
encodeQueryset(params) {
|
encodeQueryset(params) {
|
||||||
if (typeof params !== 'object') {
|
let queryset;
|
||||||
return '';
|
queryset = _.reduce(params, (result, value, key) => {
|
||||||
}
|
return result + this.encodeTerms(value, key);
|
||||||
|
}, '');
|
||||||
|
queryset = queryset.substring(0, queryset.length - 1);
|
||||||
|
return angular.isObject(params) ? `?${queryset}` : '';
|
||||||
|
|
||||||
return _.reduce(params, (result, value, key) => {
|
|
||||||
if (result !== '?') {
|
|
||||||
result += '&';
|
|
||||||
}
|
|
||||||
|
|
||||||
const encodedTermString = this.encodeTerms(value, key)
|
|
||||||
.map(([key, value]) => `${key}=${value}`)
|
|
||||||
.join('&');
|
|
||||||
|
|
||||||
return result += encodedTermString;
|
|
||||||
}, '?');
|
|
||||||
},
|
},
|
||||||
// like encodeQueryset, but return an actual unstringified API-consumable http param object
|
// like encodeQueryset, but return an actual unstringified API-consumable http param object
|
||||||
encodeQuerysetObject(params) {
|
encodeQuerysetObject(params) {
|
||||||
return _.reduce(params, (obj, value, key) => {
|
return _.reduce(params, (obj, value, key) => {
|
||||||
const encodedTerms = this.encodeTerms(value, key);
|
const encodedKey = this.replaceDefaultFlags(key);
|
||||||
|
const values = Array.isArray(value) ? value : [value];
|
||||||
|
|
||||||
for (let encodedIndex in encodedTerms) {
|
obj[encodedKey] = values
|
||||||
const [encodedKey, encodedValue] = encodedTerms[encodedIndex];
|
.map(value => this.replaceDefaultFlags(value))
|
||||||
obj[encodedKey] = obj[encodedKey] || [];
|
.map(value => this.replaceEncodedTokens(value))
|
||||||
obj[encodedKey].push(encodedValue);
|
.join(',');
|
||||||
}
|
|
||||||
|
|
||||||
return obj;
|
return obj;
|
||||||
}, {});
|
}, {});
|
||||||
|
|||||||
Reference in New Issue
Block a user