From 2b0598f0929ed83df9da7d4dabb4946577a58577 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Tue, 10 Jan 2017 14:49:58 -0500 Subject: [PATCH 1/8] Started to refactor some smart search functionality --- awx/ui/client/src/shared/smart-search/main.js | 3 +- .../shared/smart-search/queryset.service.js | 102 ++++++++++++++++-- .../smart-search/smart-search.controller.js | 76 +++++++++---- .../smart-search/smart-search.service.js | 19 ++++ .../smart-search/smart-search.service-test.js | 42 ++++++++ 5 files changed, 215 insertions(+), 27 deletions(-) create mode 100644 awx/ui/client/src/shared/smart-search/smart-search.service.js create mode 100644 awx/ui/tests/spec/smart-search/smart-search.service-test.js diff --git a/awx/ui/client/src/shared/smart-search/main.js b/awx/ui/client/src/shared/smart-search/main.js index 7653df7bd7..e7aaf825a7 100644 --- a/awx/ui/client/src/shared/smart-search/main.js +++ b/awx/ui/client/src/shared/smart-search/main.js @@ -2,11 +2,12 @@ import directive from './smart-search.directive'; import controller from './smart-search.controller'; import service from './queryset.service'; import DjangoSearchModel from './django-search-model.class'; - +import smartSearchService from './smart-search.service'; export default angular.module('SmartSearchModule', []) .directive('smartSearch', directive) .controller('SmartSearchController', controller) .service('QuerySet', service) + .service('SmartSearchService', smartSearchService) .constant('DjangoSearchModel', DjangoSearchModel); 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 359245bed4..ec18f1b0d1 100644 --- a/awx/ui/client/src/shared/smart-search/queryset.service.js +++ b/awx/ui/client/src/shared/smart-search/queryset.service.js @@ -1,5 +1,5 @@ -export default ['$q', 'Rest', 'ProcessErrors', '$rootScope', 'Wait', 'DjangoSearchModel', '$cacheFactory', - function($q, Rest, ProcessErrors, $rootScope, Wait, DjangoSearchModel, $cacheFactory) { +export default ['$q', 'Rest', 'ProcessErrors', '$rootScope', 'Wait', 'DjangoSearchModel', '$cacheFactory', 'SmartSearchService', + function($q, Rest, ProcessErrors, $rootScope, Wait, DjangoSearchModel, $cacheFactory, SmartSearchService) { return { // kick off building a model for a specific endpoint // this is usually a list's basePath @@ -67,29 +67,114 @@ export default ['$q', 'Rest', 'ProcessErrors', '$rootScope', 'Wait', 'DjangoSear return angular.isObject(params) ? `?${queryset}` : ''; function encodeTerm(value, key){ + if (Array.isArray(value)){ - return _.map(value, (item) => `${key}=${item}`).join('&') + '&'; + return _.map(value, function(item){ + item = item.replace(/"|'/g, ""); + return `${key}=${item}`.join('&') + '&'; + }); } else { + value = value.replace(/"|'/g, ""); return `${key}=${value}&`; } } }, // encodes a ui smart-search param to a django-friendly param // operand:key:comparator:value => {operand__key__comparator: value} - encodeParam(param){ - let split = param.split(':'); - return {[split.slice(0,split.length -1).join('__')] : split[split.length-1]}; + encodeParam(params){ + // Assumption here is that we have a key and a value so the length + // of the paramParts array will be 2. [0] is the key and [1] the value + let paramParts = SmartSearchService.splitTermIntoParts(params.term); + let keySplit = paramParts[0].split('.'); + let exclude = false; + let lessThanGreaterThan = paramParts[1].match(/^(>|<).*$/) ? true : false; + if(keySplit[0].startsWith("-")) { + exclude = true; + keySplit[0] = keySplit[0].replace(/^-/, ''); + } + let paramString = exclude ? "not__" : ""; + let valueString = paramParts[1]; + if(keySplit.length === 1) { + if(params.searchTerm && !lessThanGreaterThan) { + paramString += keySplit[0] + '__icontains'; + } + else if(params.relatedSearchTerm) { + paramString += keySplit[0] + '__search'; + } + else { + paramString += keySplit[0]; + } + } + else { + paramString += keySplit.join('__'); + } + + if(lessThanGreaterThan) { + if(paramParts[1].match(/^>=.*$/)) { + paramString += '__gte'; + valueString = valueString.replace(/^(>=)/,""); + } + else if(paramParts[1].match(/^<=.*$/)) { + paramString += '__lte'; + valueString = valueString.replace(/^(<=)/,""); + } + else if(paramParts[1].match(/^<.*$/)) { + paramString += '__lt'; + valueString = valueString.replace(/^(<)/,""); + } + else if(paramParts[1].match(/^>.*$/)) { + paramString += '__gt'; + valueString = valueString.replace(/^(>)/,""); + } + } + + return {[paramString] : valueString}; }, // decodes a django queryset param into a ui smart-search tag or set of tags decodeParam(value, key){ + + let decodeParamString = function(searchString) { + if(key === 'search') { + // Don't include 'search:' in the search tag + return decodeURIComponent(`${searchString}`); + } + else { + key = key.replace(/__icontains/g, ""); + let split = key.split('__'); + let decodedParam = searchString; + let exclude = false; + if(key.startsWith('not__')) { + exclude = true; + split = split.splice(1, split.length); + } + if(key.endsWith('__gt')) { + decodedParam = '>' + decodedParam; + split = split.splice(0, split.length-1); + } + else if(key.endsWith('__lt')) { + decodedParam = '<' + decodedParam; + split = split.splice(0, split.length-1); + } + else if(key.endsWith('__gte')) { + decodedParam = '>=' + decodedParam; + split = split.splice(0, split.length-1); + } + else if(key.endsWith('__lte')) { + decodedParam = '<=' + decodedParam; + split = split.splice(0, split.length-1); + } + return exclude ? `-${split.join('.')}:${decodedParam}` : `${split.join('.')}:${decodedParam}`; + } + }; + if (Array.isArray(value)){ return _.map(value, (item) => { - return `${key.split('__').join(':')}:${item}`; + return decodeParamString(item); }); } else { - return `${key.split('__').join(':')}:${value}`; + return decodeParamString(value); } }, @@ -161,6 +246,7 @@ export default ['$q', 'Rest', 'ProcessErrors', '$rootScope', 'Wait', 'DjangoSear success(data) { return data; }, + }; } ]; diff --git a/awx/ui/client/src/shared/smart-search/smart-search.controller.js b/awx/ui/client/src/shared/smart-search/smart-search.controller.js index 95f3be3adb..7b09def616 100644 --- a/awx/ui/client/src/shared/smart-search/smart-search.controller.js +++ b/awx/ui/client/src/shared/smart-search/smart-search.controller.js @@ -1,5 +1,5 @@ -export default ['$stateParams', '$scope', '$state', 'QuerySet', 'GetBasePath', 'QuerySet', - function($stateParams, $scope, $state, QuerySet, GetBasePath, qs) { +export default ['$stateParams', '$scope', '$state', 'QuerySet', 'GetBasePath', 'QuerySet', 'SmartSearchService', + function($stateParams, $scope, $state, QuerySet, GetBasePath, qs, SmartSearchService) { let path, relations, // steps through the current tree of $state configurations, grabs default search params @@ -17,6 +17,7 @@ export default ['$stateParams', '$scope', '$state', 'QuerySet', 'GetBasePath', ' $scope.searchTags = stripDefaultParams($state.params[`${$scope.iterator}_search`]); qs.initFieldset(path, $scope.djangoModel, relations).then((data) => { $scope.models = data.models; + $scope.options = data.options.data; $scope.$emit(`${$scope.list.iterator}_options`, data.options); }); } @@ -38,6 +39,16 @@ export default ['$stateParams', '$scope', '$state', 'QuerySet', 'GetBasePath', ' return flat; } + function setDefaults(term) { + if ($scope.list.defaultSearchParams) { + return $scope.list.defaultSearchParams(term); + } else { + return { + search: encodeURIComponent(term) + }; + } + } + $scope.toggleKeyPane = function() { $scope.showKeyPane = !$scope.showKeyPane; }; @@ -56,7 +67,27 @@ export default ['$stateParams', '$scope', '$state', 'QuerySet', 'GetBasePath', ' // remove tag, merge new queryset, $state.go $scope.remove = function(index) { - let removed = qs.encodeParam($scope.searchTags.splice(index, 1)[0]); + let tagToRemove = $scope.searchTags.splice(index, 1)[0]; + let termParts = SmartSearchService.splitTermIntoParts(tagToRemove); + let removed; + if (termParts.length === 1) { + removed = setDefaults(tagToRemove); + } + else { + let root = termParts[0].split(".")[0].replace(/^-/, ''); + let encodeParams = { + term: tagToRemove + }; + if(_.has($scope.options.actions.GET, root)) { + if($scope.options.actions.GET[root].type && $scope.options.actions.GET[root].type === 'field') { + encodeParams.relatedSearchTerm = true; + } + else { + encodeParams.searchTerm = true; + } + } + removed = qs.encodeParam(encodeParams); + } _.each(removed, (value, key) => { if (Array.isArray(queryset[key])){ _.remove(queryset[key], (item) => item === value); @@ -79,26 +110,35 @@ export default ['$stateParams', '$scope', '$state', 'QuerySet', 'GetBasePath', ' let params = {}, origQueryset = _.clone(queryset); - function setDefaults(term) { - // "name" and "description" are sane defaults for MOST models, but not ALL! - // defaults may be configured in ListDefinition.defaultSearchParams - if ($scope.list.defaultSearchParams) { - return $scope.list.defaultSearchParams(term); - } else { - return { - or__name__icontains: term, - or__description__icontains: term - }; - } - } + // Remove leading/trailing whitespace if there is any + terms = terms.trim(); if(terms && terms !== '') { - _.forEach(terms.split(' '), (term) => { + // Split the terms up + let splitTerms = SmartSearchService.splitSearchIntoTerms(terms); + _.forEach(splitTerms, (term) => { + + let termParts = SmartSearchService.splitTermIntoParts(term); + // if only a value is provided, search using default keys - if (term.split(':').length === 1) { + if (termParts.length === 1) { params = _.merge(params, setDefaults(term)); } else { - params = _.merge(params, qs.encodeParam(term)); + // Figure out if this is a search term + let root = termParts[0].split(".")[0].replace(/^-/, ''); + if(_.has($scope.options.actions.GET, root)) { + if($scope.options.actions.GET[root].type && $scope.options.actions.GET[root].type === 'field') { + params = _.merge(params, qs.encodeParam({term: term, relatedSearchTerm: true})); + } + else { + params = _.merge(params, qs.encodeParam({term: term, searchTerm: true})); + } + } + // Its not a search term or a related search term + else { + params = _.merge(params, qs.encodeParam({term: term})); + } + } }); diff --git a/awx/ui/client/src/shared/smart-search/smart-search.service.js b/awx/ui/client/src/shared/smart-search/smart-search.service.js new file mode 100644 index 0000000000..fe683c08d7 --- /dev/null +++ b/awx/ui/client/src/shared/smart-search/smart-search.service.js @@ -0,0 +1,19 @@ +export default [function() { + return { + splitSearchIntoTerms(searchString) { + return searchString.match(/(?:[^\s("')]+|"[^"]*"|'[^']*')+/g); + }, + splitTermIntoParts(searchTerm) { + let breakOnColon = searchTerm.match(/(?:[^:"]+|"[^"]*")+/g); + + if(breakOnColon.length > 2) { + // concat all the strings after the first one together + let stringsToJoin = breakOnColon.slice(1,breakOnColon.length); + return [breakOnColon[0], stringsToJoin.join(':')]; + } + else { + return breakOnColon; + } + } + }; +}]; diff --git a/awx/ui/tests/spec/smart-search/smart-search.service-test.js b/awx/ui/tests/spec/smart-search/smart-search.service-test.js new file mode 100644 index 0000000000..679a5656b4 --- /dev/null +++ b/awx/ui/tests/spec/smart-search/smart-search.service-test.js @@ -0,0 +1,42 @@ +'use strict'; + +describe('Service: SmartSearch', () => { + let SmartSearchService; + + beforeEach(angular.mock.module('Tower')); + + beforeEach(angular.mock.module('SmartSearchModule')); + + beforeEach(angular.mock.inject((_SmartSearchService_) => { + SmartSearchService = _SmartSearchService_; + })); + + describe('fn splitSearchIntoTerms', () => { + it('should convert the search string to an array tag strings', () =>{ + expect(SmartSearchService.splitSearchIntoTerms('foo')).toEqual(["foo"]); + expect(SmartSearchService.splitSearchIntoTerms('foo bar')).toEqual(["foo", "bar"]); + expect(SmartSearchService.splitSearchIntoTerms('name:foo bar')).toEqual(["name:foo", "bar"]); + expect(SmartSearchService.splitSearchIntoTerms('name:foo description:bar')).toEqual(["name:foo", "description:bar"]); + expect(SmartSearchService.splitSearchIntoTerms('name:"foo bar"')).toEqual(["name:\"foo bar\""]); + expect(SmartSearchService.splitSearchIntoTerms('name:"foo bar" description:"bar foo"')).toEqual(["name:\"foo bar\"", "description:\"bar foo\""]); + expect(SmartSearchService.splitSearchIntoTerms('name:"foo bar" description:"bar foo"')).toEqual(["name:\"foo bar\"", "description:\"bar foo\""]); + expect(SmartSearchService.splitSearchIntoTerms('name:\'foo bar\'')).toEqual(["name:\'foo bar\'"]); + expect(SmartSearchService.splitSearchIntoTerms('name:\'foo bar\' description:\'bar foo\'')).toEqual(["name:\'foo bar\'", "description:\'bar foo\'"]); + expect(SmartSearchService.splitSearchIntoTerms('name:\'foo bar\' description:\'bar foo\'')).toEqual(["name:\'foo bar\'", "description:\'bar foo\'"]); + expect(SmartSearchService.splitSearchIntoTerms('name:\"foo bar\" description:\'bar foo\'')).toEqual(["name:\"foo bar\"", "description:\'bar foo\'"]); + }); + }); + + describe('fn splitTermIntoParts', () => { + it('should convert the search term to a key and value', () =>{ + expect(SmartSearchService.splitTermIntoParts('foo')).toEqual(["foo"]); + expect(SmartSearchService.splitTermIntoParts('foo:bar')).toEqual(["foo", "bar"]); + expect(SmartSearchService.splitTermIntoParts('foo:bar:foobar')).toEqual(["foo", "bar:foobar"]); + expect(SmartSearchService.splitTermIntoParts('name:\"foo bar\"')).toEqual(["name", "\"foo bar\""]); + expect(SmartSearchService.splitTermIntoParts('name:\"foo:bar\"')).toEqual(["name", "\"foo:bar\""]); + expect(SmartSearchService.splitTermIntoParts('name:\'foo bar\'')).toEqual(["name", "\'foo bar\'"]); + expect(SmartSearchService.splitTermIntoParts('name:\'foo:bar\'')).toEqual(["name", "\'foo:bar\'"]); + }); + }); + +}); From d589dceaa2e3509899c2ccbc9518d04ad33e4d22 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Tue, 10 Jan 2017 16:00:59 -0500 Subject: [PATCH 2/8] Fixed pagination by stringifying the page number before trying to update the query param --- awx/ui/client/src/shared/paginate/paginate.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/client/src/shared/paginate/paginate.controller.js b/awx/ui/client/src/shared/paginate/paginate.controller.js index fdd80000c7..407f9e10f6 100644 --- a/awx/ui/client/src/shared/paginate/paginate.controller.js +++ b/awx/ui/client/src/shared/paginate/paginate.controller.js @@ -18,7 +18,7 @@ export default ['$scope', '$stateParams', '$state', '$filter', 'GetBasePath', 'Q return; } path = GetBasePath($scope.basePath) || $scope.basePath; - queryset = _.merge($stateParams[`${$scope.iterator}_search`], { page: page }); + queryset = _.merge($stateParams[`${$scope.iterator}_search`], { page: page.toString() }); $state.go('.', { [$scope.iterator + '_search']: queryset }); From a36981dcb98705fc9a8db21567b5224caba0a08a Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Tue, 10 Jan 2017 16:53:35 -0500 Subject: [PATCH 3/8] Now handling multiple instances of the same query param with different values --- .../shared/smart-search/queryset.service.js | 6 ++++-- .../smart-search/smart-search.controller.js | 19 +++++++++++++++---- 2 files changed, 19 insertions(+), 6 deletions(-) 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 ec18f1b0d1..89ad2f14d1 100644 --- a/awx/ui/client/src/shared/smart-search/queryset.service.js +++ b/awx/ui/client/src/shared/smart-search/queryset.service.js @@ -69,10 +69,12 @@ export default ['$q', 'Rest', 'ProcessErrors', '$rootScope', 'Wait', 'DjangoSear function encodeTerm(value, key){ if (Array.isArray(value)){ - return _.map(value, function(item){ + let concated = ''; + angular.forEach(value, function(item){ item = item.replace(/"|'/g, ""); - return `${key}=${item}`.join('&') + '&'; + concated += `${key}=${item}&`; }); + return concated; } else { value = value.replace(/"|'/g, ""); diff --git a/awx/ui/client/src/shared/smart-search/smart-search.controller.js b/awx/ui/client/src/shared/smart-search/smart-search.controller.js index 7b09def616..2da9648ca1 100644 --- a/awx/ui/client/src/shared/smart-search/smart-search.controller.js +++ b/awx/ui/client/src/shared/smart-search/smart-search.controller.js @@ -120,23 +120,34 @@ export default ['$stateParams', '$scope', '$state', 'QuerySet', 'GetBasePath', ' let termParts = SmartSearchService.splitTermIntoParts(term); + function combineSameSearches(a,b){ + if (_.isArray(a)) { + return a.concat(b); + } + else { + if(a) { + return [a,b]; + } + } + } + // if only a value is provided, search using default keys if (termParts.length === 1) { - params = _.merge(params, setDefaults(term)); + params = _.merge(params, setDefaults(term), combineSameSearches); } else { // Figure out if this is a search term let root = termParts[0].split(".")[0].replace(/^-/, ''); if(_.has($scope.options.actions.GET, root)) { if($scope.options.actions.GET[root].type && $scope.options.actions.GET[root].type === 'field') { - params = _.merge(params, qs.encodeParam({term: term, relatedSearchTerm: true})); + params = _.merge(params, qs.encodeParam({term: term, relatedSearchTerm: true}), combineSameSearches); } else { - params = _.merge(params, qs.encodeParam({term: term, searchTerm: true})); + params = _.merge(params, qs.encodeParam({term: term, searchTerm: true}), combineSameSearches); } } // Its not a search term or a related search term else { - params = _.merge(params, qs.encodeParam({term: term})); + params = _.merge(params, qs.encodeParam({term: term}), combineSameSearches); } } From 644e4647981f3d0c7a56fa6b6fabadb296ac9ea3 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Wed, 11 Jan 2017 10:47:06 -0500 Subject: [PATCH 4/8] Handle the injected __search and __icontains so that they don't show up in the search tags. --- .../client/src/shared/smart-search/queryset.service.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) 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 89ad2f14d1..edb1301b9c 100644 --- a/awx/ui/client/src/shared/smart-search/queryset.service.js +++ b/awx/ui/client/src/shared/smart-search/queryset.service.js @@ -68,6 +68,9 @@ export default ['$q', 'Rest', 'ProcessErrors', '$rootScope', 'Wait', 'DjangoSear function encodeTerm(value, key){ + key = key.replace(/__icontains_DEFAULT/g, "__icontains"); + key = key.replace(/__search_DEFAULT/g, "__search"); + if (Array.isArray(value)){ let concated = ''; angular.forEach(value, function(item){ @@ -99,10 +102,10 @@ export default ['$q', 'Rest', 'ProcessErrors', '$rootScope', 'Wait', 'DjangoSear let valueString = paramParts[1]; if(keySplit.length === 1) { if(params.searchTerm && !lessThanGreaterThan) { - paramString += keySplit[0] + '__icontains'; + paramString += keySplit[0] + '__icontains_DEFAULT'; } else if(params.relatedSearchTerm) { - paramString += keySplit[0] + '__search'; + paramString += keySplit[0] + '__search_DEFAULT'; } else { paramString += keySplit[0]; @@ -142,7 +145,8 @@ export default ['$q', 'Rest', 'ProcessErrors', '$rootScope', 'Wait', 'DjangoSear return decodeURIComponent(`${searchString}`); } else { - key = key.replace(/__icontains/g, ""); + key = key.replace(/__icontains_DEFAULT/g, ""); + key = key.replace(/__search_DEFAULT/g, ""); let split = key.split('__'); let decodedParam = searchString; let exclude = false; From c452f5fd804db4b2105a33f3234d6c54ec7fe9a9 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Wed, 11 Jan 2017 13:21:19 -0500 Subject: [PATCH 5/8] Added more testing --- .../smart-search/queryset.service-test.js | 43 +++++++++++-------- .../smart-search/smart-search.service-test.js | 1 + 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/awx/ui/tests/spec/smart-search/queryset.service-test.js b/awx/ui/tests/spec/smart-search/queryset.service-test.js index b932925171..3d49440920 100644 --- a/awx/ui/tests/spec/smart-search/queryset.service-test.js +++ b/awx/ui/tests/spec/smart-search/queryset.service-test.js @@ -3,7 +3,8 @@ describe('Service: QuerySet', () => { let $httpBackend, QuerySet, - Authorization; + Authorization, + SmartSearchService; beforeEach(angular.mock.module('Tower', ($provide) =>{ // @todo: improve app source / write testing utilities for interim @@ -17,9 +18,10 @@ describe('Service: QuerySet', () => { })); beforeEach(angular.mock.module('RestServices')); - beforeEach(angular.mock.inject((_$httpBackend_, _QuerySet_) => { + beforeEach(angular.mock.inject((_$httpBackend_, _QuerySet_, _SmartSearchService_) => { $httpBackend = _$httpBackend_; QuerySet = _QuerySet_; + SmartSearchService = _SmartSearchService_; // @todo: improve app source // config.js / local_settings emit $http requests in the app's run block @@ -33,24 +35,27 @@ describe('Service: QuerySet', () => { .respond(200, ''); })); - describe('fn encodeQuery', () => { - xit('null/undefined params should return an empty string', () => { - expect(QuerySet.encodeQuery(null)).toEqual(''); - expect(QuerySet.encodeQuery(undefined)).toEqual(''); + describe('fn encodeParam', () => { + it('should encode parameters properly', () =>{ + expect(QuerySet.encodeParam({term: "name:foo", searchTerm: true})).toEqual({"name__icontains_DEFAULT" : "foo"}); + expect(QuerySet.encodeParam({term: "-name:foo", searchTerm: true})).toEqual({"not__name__icontains_DEFAULT" : "foo"}); + expect(QuerySet.encodeParam({term: "name:'foo bar'", searchTerm: true})).toEqual({"name__icontains_DEFAULT" : "'foo bar'"}); + expect(QuerySet.encodeParam({term: "-name:'foo bar'", searchTerm: true})).toEqual({"not__name__icontains_DEFAULT" : "'foo bar'"}); + expect(QuerySet.encodeParam({term: "organization:foo", relatedSearchTerm: true})).toEqual({"organization__search_DEFAULT" : "foo"}); + expect(QuerySet.encodeParam({term: "-organization:foo", relatedSearchTerm: true})).toEqual({"not__organization__search_DEFAULT" : "foo"}); + expect(QuerySet.encodeParam({term: "organization.name:foo", relatedSearchTerm: true})).toEqual({"organization__name" : "foo"}); + expect(QuerySet.encodeParam({term: "-organization.name:foo", relatedSearchTerm: true})).toEqual({"not__organization__name" : "foo"}); + expect(QuerySet.encodeParam({term: "id:11", searchTerm: true})).toEqual({"id__icontains_DEFAULT" : "11"}); + expect(QuerySet.encodeParam({term: "-id:11", searchTerm: true})).toEqual({"not__id__icontains_DEFAULT" : "11"}); + expect(QuerySet.encodeParam({term: "id:>11", searchTerm: true})).toEqual({"id__gt" : "11"}); + expect(QuerySet.encodeParam({term: "-id:>11", searchTerm: true})).toEqual({"not__id__gt" : "11"}); + expect(QuerySet.encodeParam({term: "id:>=11", searchTerm: true})).toEqual({"id__gte" : "11"}); + expect(QuerySet.encodeParam({term: "-id:>=11", searchTerm: true})).toEqual({"not__id__gte" : "11"}); + expect(QuerySet.encodeParam({term: "id:<11", searchTerm: true})).toEqual({"id__lt" : "11"}); + expect(QuerySet.encodeParam({term: "-id:<11", searchTerm: true})).toEqual({"not__id__lt" : "11"}); + expect(QuerySet.encodeParam({term: "id:<=11", searchTerm: true})).toEqual({"id__lte" : "11"}); + expect(QuerySet.encodeParam({term: "-id:<=11", searchTerm: true})).toEqual({"not__id__lte" : "11"}); }); - xit('should encode params to a string', () => { - let params = { - or__created_by: 'Jenkins', - or__modified_by: 'Jenkins', - and__not__status: 'success', - }, - result = '?or__created_by=Jenkins&or__modified_by=Jenkins&and__not__status=success'; - expect(QuerySet.encodeQuery(params)).toEqual(result); - }); - }); - - xdescribe('fn decodeQuery', () => { - }); diff --git a/awx/ui/tests/spec/smart-search/smart-search.service-test.js b/awx/ui/tests/spec/smart-search/smart-search.service-test.js index 679a5656b4..d5c35a08c2 100644 --- a/awx/ui/tests/spec/smart-search/smart-search.service-test.js +++ b/awx/ui/tests/spec/smart-search/smart-search.service-test.js @@ -24,6 +24,7 @@ describe('Service: SmartSearch', () => { expect(SmartSearchService.splitSearchIntoTerms('name:\'foo bar\' description:\'bar foo\'')).toEqual(["name:\'foo bar\'", "description:\'bar foo\'"]); expect(SmartSearchService.splitSearchIntoTerms('name:\'foo bar\' description:\'bar foo\'')).toEqual(["name:\'foo bar\'", "description:\'bar foo\'"]); expect(SmartSearchService.splitSearchIntoTerms('name:\"foo bar\" description:\'bar foo\'')).toEqual(["name:\"foo bar\"", "description:\'bar foo\'"]); + expect(SmartSearchService.splitSearchIntoTerms('name:\"foo bar\" foo')).toEqual(["name:\"foo bar\"", "foo"]); }); }); From 50fd3d38cb92dcf8b71de9700b3f0c1ad88e8ad8 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Wed, 11 Jan 2017 13:57:13 -0500 Subject: [PATCH 6/8] Treat any search string that doesn't start with a matching attribute as a string --- .../client/src/shared/smart-search/smart-search.controller.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/ui/client/src/shared/smart-search/smart-search.controller.js b/awx/ui/client/src/shared/smart-search/smart-search.controller.js index 2da9648ca1..1ec198c18d 100644 --- a/awx/ui/client/src/shared/smart-search/smart-search.controller.js +++ b/awx/ui/client/src/shared/smart-search/smart-search.controller.js @@ -145,9 +145,9 @@ export default ['$stateParams', '$scope', '$state', 'QuerySet', 'GetBasePath', ' params = _.merge(params, qs.encodeParam({term: term, searchTerm: true}), combineSameSearches); } } - // Its not a search term or a related search term + // Its not a search term or a related search term - treat it as a string else { - params = _.merge(params, qs.encodeParam({term: term}), combineSameSearches); + params = _.merge(params, setDefaults(term), combineSameSearches); } } From 46491a59e617c7fb9992a620da4e8b02c6d92cf8 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Wed, 11 Jan 2017 15:46:52 -0500 Subject: [PATCH 7/8] Removed the use of startsWith from encodeParam. PhantomJS didn't recognize that when the unit tests ran in jenkins. Rather than try to figure out why that might be the case I just changed the line. --- awx/ui/client/src/shared/smart-search/queryset.service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 edb1301b9c..86a2bc2b20 100644 --- a/awx/ui/client/src/shared/smart-search/queryset.service.js +++ b/awx/ui/client/src/shared/smart-search/queryset.service.js @@ -94,7 +94,7 @@ export default ['$q', 'Rest', 'ProcessErrors', '$rootScope', 'Wait', 'DjangoSear let keySplit = paramParts[0].split('.'); let exclude = false; let lessThanGreaterThan = paramParts[1].match(/^(>|<).*$/) ? true : false; - if(keySplit[0].startsWith("-")) { + if(keySplit[0].match(/^-/g)) { exclude = true; keySplit[0] = keySplit[0].replace(/^-/, ''); } From 18fcfee96de8c9fa873a6f65ca2000e61013cbaf Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Thu, 12 Jan 2017 10:38:40 -0500 Subject: [PATCH 8/8] Fixed a few search related bugs based on PR review feedback --- awx/ui/client/src/shared/form-generator.js | 6 +++--- .../src/shared/smart-search/smart-search.controller.js | 9 ++++++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/awx/ui/client/src/shared/form-generator.js b/awx/ui/client/src/shared/form-generator.js index 63146ce890..e9d6597001 100644 --- a/awx/ui/client/src/shared/form-generator.js +++ b/awx/ui/client/src/shared/form-generator.js @@ -1830,7 +1830,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat // smart-search directive html += `
+ ng-hide="${itm}.length === 0 && (searchTags | isEmpty)"> + ng-show="${itm}.length === 0 && !(searchTags | isEmpty)">
No records matched your search.
@@ -1865,7 +1865,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat // Show the "no items" box when loading is done and the user isn't actively searching and there are no results var emptyListText = (collection.emptyListText) ? collection.emptyListText : i18n._("PLEASE ADD ITEMS TO THIS LIST"); html += `
`; - html += `
${emptyListText}
`; + html += `
${emptyListText}
`; html += '
'; html += ` diff --git a/awx/ui/client/src/shared/smart-search/smart-search.controller.js b/awx/ui/client/src/shared/smart-search/smart-search.controller.js index 1ec198c18d..b601813a72 100644 --- a/awx/ui/client/src/shared/smart-search/smart-search.controller.js +++ b/awx/ui/client/src/shared/smart-search/smart-search.controller.js @@ -85,12 +85,19 @@ export default ['$stateParams', '$scope', '$state', 'QuerySet', 'GetBasePath', ' else { encodeParams.searchTerm = true; } + removed = qs.encodeParam(encodeParams); + } + else { + removed = setDefaults(tagToRemove); } - removed = qs.encodeParam(encodeParams); } _.each(removed, (value, key) => { if (Array.isArray(queryset[key])){ _.remove(queryset[key], (item) => item === value); + // If the array is now empty, remove that key + if(queryset[key].length === 0) { + delete queryset[key]; + } } else { delete queryset[key];