Added Server-side filtering for swift UI

Added server-side filtering for containers in swift UI using the
magic search directive.

Change-Id: Id0eaa818fab8b2c7d7a0ab40c1a943c2e7342f10
Partial-Implements: blueprint server-side-filtering
This commit is contained in:
Luis Daniel Castellanos 2016-06-08 11:19:37 -05:00
parent ce7dc70c7f
commit 277d15abff
13 changed files with 181 additions and 13 deletions

View File

@ -72,7 +72,8 @@
getName: getName,
getTextFacet: getTextFacet,
getUnusedFacetChoices: getUnusedFacetChoices,
getQueryPattern: getQueryPattern
getQueryPattern: getQueryPattern,
getQueryObject: getQueryObject
};
return service;
@ -324,6 +325,17 @@
}
}
}
// Gets a query object from a query string
function getQueryObject(data) {
return getSearchTermsFromQueryString(data)
.map(getSearchTermObject)
.reduce(addToObject, {});
function addToObject(orig, curr) {
orig[curr.type] = curr.value;
return orig;
}
}
}
})();

View File

@ -325,5 +325,15 @@
{name: "other"}]);
});
});
describe("getQueryObject", function() {
it("should get a query object from a query string", function() {
var queryString = "name=test&type=test";
var queryObject = service.getQueryObject(queryString);
expect(queryObject).toEqual({name: "test", type: "test"});
});
});
});
})();

View File

@ -53,9 +53,18 @@ class Containers(generic.View):
def get(self, request):
"""Get the list of containers for this account
:param prefix: container name prefix value. Named items in the
response begin with this value
TODO(neillc): Add pagination
"""
containers, has_more = api.swift.swift_get_containers(request)
prefix = request.GET.get('prefix', None)
if prefix:
containers, has_more = api.swift.\
swift_get_containers(request, prefix=prefix)
else:
containers, has_more = api.swift.swift_get_containers(request)
containers = [container.to_dict() for container in containers]
return {'items': containers, 'has_more': has_more}

View File

@ -135,10 +135,11 @@ def swift_object_exists(request, container_name, object_name):
@profiler.trace
def swift_get_containers(request, marker=None):
def swift_get_containers(request, marker=None, prefix=None):
limit = getattr(settings, 'API_RESULT_LIMIT', 1000)
headers, containers = swift_api(request).get_account(limit=limit + 1,
marker=marker,
prefix=prefix,
full_listing=True)
container_objs = [Container(c) for c in containers]
if(len(container_objs) > limit):

View File

@ -63,6 +63,7 @@
updateContainer: updateContainer,
recursiveCollect: recursiveCollect,
recursiveDelete: recursiveDelete,
getContainers: getContainers,
_recursiveDeleteFiles: recursiveDeleteFiles,
_recursiveDeleteFolders: recursiveDeleteFolders
@ -72,6 +73,8 @@
// initialisation completing (i.e. containers listing loaded)
model.intialiseDeferred = $q.defer();
model.getContainersDeferred = $q.defer();
return model;
/**
@ -370,5 +373,25 @@
return deleteFolder(node.folder);
});
}
/**
* @ngdoc method
* @name ContainersModel.getContainers
*
* @param {Object} params Search parameters for filtering
* @description
* Gets the model containers filtered by the given query. If query is empty
* then it returns all of the containers
*
*/
function getContainers(params) {
swiftAPI.getContainers(params).then(function onContainers(data) {
model.containers.length = 0;
push.apply(model.containers, data.data.items);
}).then(function resolve() {
model.getContainersDeferred.resolve();
});
}
}
})();

View File

@ -192,6 +192,22 @@
expect(service.objects).toEqual([{name: 'two'}]);
});
it('should fetch containers on demand', function test() {
var deferred = $q.defer();
spyOn(swiftAPI, 'getContainers').and.returnValue(deferred.promise);
var containers = ['two', 'items'];
var params = {};
service.getContainers(params);
expect(swiftAPI.getContainers).toHaveBeenCalledWith({});
deferred.resolve({data: {items: ['two', 'items']}});
$rootScope.$apply();
expect(containers).toEqual(['two', 'items']);
});
describe('recursive deletion', function describe() {
// fake up a basic set of object listings to return from our fake getObjects
// below
@ -359,6 +375,27 @@
expect(state.deleted.folders).toEqual(4);
expect(state.deleted.failures).toEqual(0);
});
it('should increase state failures on deletion failure', function test() {
var state = {deleted: {folders: 0, failures: 0}};
var failures = [];
spyOn(apiService, 'delete').and.callFake(function fake(url) {
failures.push(url);
var deferred = $q.defer();
deferred.reject({status: 403});
return deferred.promise;
});
service._recursiveDeleteFolders(state, {tree: fakeTree});
$rootScope.$apply();
expect(failures).toEqual([
'/api/swift/containers/spam/object/folder/subfolder3/',
'/api/swift/containers/spam/object/folder/subfolder/',
'/api/swift/containers/spam/object/folder/subfolder2/',
'/api/swift/containers/spam/object/folder/'
]);
expect(state.deleted.failures).toEqual(4);
expect(state.deleted.folders).toEqual(0);
});
});
});
})();

View File

@ -38,6 +38,9 @@
'horizon.framework.widgets.form.ModalFormService',
'horizon.framework.widgets.modal.simple-modal.service',
'horizon.framework.widgets.toast.service',
'horizon.framework.widgets.magic-search.events',
'horizon.framework.widgets.magic-search.service',
'$scope',
'$location',
'$q'
];
@ -50,6 +53,9 @@
modalFormService,
simpleModalService,
toastService,
magicSearchEvents,
magicSearchService,
$scope,
$location,
$q) {
var ctrl = this;
@ -57,6 +63,20 @@
ctrl.model.initialize();
ctrl.baseRoute = baseRoute;
ctrl.containerRoute = containerRoute;
ctrl.filterFacets = [
{
name: 'prefix',
label: gettext('Prefix'),
singleton: true,
isServer: true
}];
// TODO(lcastell): remove this flag when the magic search bar can be used
// as an standalone component.
// This flag is used to tell what trigger the search updated event. Currently,
// when selecting a container the magic-search controller gets executed and emits
// the searchUpdated event.
ctrl.filterEventTrigeredBySearchBar = true;
ctrl.checkContainerNameConflict = checkContainerNameConflict;
ctrl.toggleAccess = toggleAccess;
@ -81,8 +101,12 @@
}
function selectContainer(container) {
if (!ctrl.model.container || container.name !== ctrl.model.container.name) {
ctrl.filterEventTrigeredBySearchBar = false;
}
ctrl.model.container = container;
$location.path(ctrl.containerRoute + container.name);
return ctrl.model.fetchContainerDetail(container);
}
@ -225,5 +249,15 @@
}
);
}
$scope.$on(magicSearchEvents.SEARCH_UPDATED, function(event, data) {
// At this moment there's only server side filtering supported, therefore
// there's no need to check if it is client side or server side filtering
if (ctrl.filterEventTrigeredBySearchBar) {
ctrl.model.getContainers(magicSearchService.getQueryObject(data));
}
else {
ctrl.filterEventTrigeredBySearchBar = true;
}
});
}
})();

View File

@ -25,6 +25,13 @@
var fakeModel = {
loadContainerContents: angular.noop,
initialize: angular.noop,
getContainers: function fake(query) {
return {
then: function then(callback) {
callback(query);
}
};
},
fetchContainerDetail: function fake() {
return {
then: function then(callback) {
@ -37,7 +44,8 @@
}
};
var $q, $location, $rootScope, controller, modalFormService, simpleModal, swiftAPI, toast;
var $q, scope, $location, $rootScope, controller,
modalFormService, simpleModal, swiftAPI, toast;
beforeEach(module('horizon.dashboard.project.containers', function($provide) {
$provide.value('horizon.dashboard.project.containers.containers-model', fakeModel);
@ -48,11 +56,12 @@
$q = _$q_;
$location = $injector.get('$location');
$rootScope = _$rootScope_;
scope = $rootScope.$new();
modalFormService = $injector.get('horizon.framework.widgets.form.ModalFormService');
simpleModal = $injector.get('horizon.framework.widgets.modal.simple-modal.service');
swiftAPI = $injector.get('horizon.app.core.openstack-service-api.swift');
toast = $injector.get('horizon.framework.widgets.toast.service');
fakeModel.getContainersDeferred = $q.defer();
spyOn(fakeModel, 'initialize');
spyOn(fakeModel, 'loadContainerContents');
spyOn(fakeModel, 'fetchContainerDetail').and.callThrough();
@ -62,6 +71,7 @@
function createController() {
return controller(
'horizon.dashboard.project.containers.ContainersController', {
$scope:scope,
'horizon.dashboard.project.containers.baseRoute': 'base ham',
'horizon.dashboard.project.containers.containerRoute': 'eggs '
});
@ -258,5 +268,28 @@
expect(fakeModel.containers[0].name).toEqual('spam');
expect(fakeModel.containers.length).toEqual(1);
});
it('should call getContainers when filters change', function test() {
spyOn(fakeModel, 'getContainers').and.callThrough();
var ctrl = createController();
ctrl.filterEventTrigeredBySearchBar = true;
scope.cc = ctrl;
scope.$digest();
scope.cc.currentSearchFacets = 'prefix=test';
scope.$digest();
scope.$emit('searchUpdated', scope.cc.currentSearchFacets);
expect(fakeModel.getContainers).toHaveBeenCalledWith({prefix: 'test'});
});
it('should not call getContainers when "searchUpdated" event was not ' +
'triggered by filters update', function test() {
spyOn(fakeModel, 'getContainers');
var ctrl = createController();
ctrl.filterEventTrigeredBySearchBar = false;
scope.cc = ctrl;
scope.$emit('searchUpdated', scope.cc.currentSearchFacets);
expect(fakeModel.getContainers).not.toHaveBeenCalled();
expect(ctrl.filterEventTrigeredBySearchBar).toEqual(true);
});
});
})();

View File

@ -11,7 +11,10 @@
<div class="row">
<div class="col-xs-12">
<uib-accordion class="hz-container-accordion">
<hz-magic-search-context filter-facets="cc.filterFacets">
<hz-magic-search-bar></hz-magic-search-bar>
</hz-magic-search-context>
<uib-accordion class="hz-container-accordion" ng-if="cc.model.containers.length > 0">
<div uib-accordion-group ng-repeat="container in cc.model.containers"
ng-class="{'panel-primary': container.name === cc.model.container.name}"
class="panel-default"
@ -42,15 +45,15 @@
<ul ng-if="container.is_fetched" class="hz-object-detail list-unstyled">
<li class="hz-object-count row">
<span class="hz-object-label col-lg-7 col-md-12" translate>Object Count</span>
<span class="hz-object-val col-lg-5 col-md-12">{$container.count$}</span>
<span class="hz-object-val col-lg-5 col-md-12">{$ container.count $}</span>
</li>
<li class="hz-object-size row">
<span class="hz-object-label col-lg-7 col-md-12" translate>Size</span>
<span class="hz-object-val col-lg-5 col-md-12">{$container.bytes | bytes$}</span>
<span class="hz-object-val col-lg-5 col-md-12">{$ container.bytes | bytes $}</span>
</li>
<li class="hz-object-timestamp row">
<span class="hz-object-label col-lg-7 col-md-12" translate>Date Created</span>
<span class="hz-object-val col-lg-5 col-md-12">{$container.timestamp | date$}</span>
<span class="hz-object-val col-lg-5 col-md-12">{$ container.timestamp | date $}</span>
</li>
<li class="hz-object-link row">
<div class="themable-checkbox col-lg-7 col-md-12">
@ -68,6 +71,9 @@
</ul>
</div uib-accordion-group>
</uib-accordion>
<div class="col-xs-12" ng-if="cc.model.containers.length == 0">
<p><translate>No items to display.</translate></p>
</div>
</div>
</div>
</div>

View File

@ -97,8 +97,9 @@
* @returns {Object} An object with 'items' and 'has_more' flag.
*
*/
function getContainers() {
return apiService.get('/api/swift/containers/')
function getContainers(params) {
var config = params ? {'params': params} : {};
return apiService.get('/api/swift/containers/', config)
.error(function() {
toastService.add('error', gettext('Unable to get the Swift container listing.'));
});

View File

@ -50,7 +50,8 @@
func: 'getContainers',
method: 'get',
path: '/api/swift/containers/',
error: 'Unable to get the Swift container listing.'
error: 'Unable to get the Swift container listing.',
data: {}
},
{
func: 'getContainer',

View File

@ -49,7 +49,7 @@ class SwiftRestTestCase(test.TestCase):
#
@mock.patch.object(swift.api, 'swift')
def test_containers_get(self, nc):
request = self.mock_rest_request()
request = self.mock_rest_request(GET={})
nc.swift_get_containers.return_value = (self._containers, False)
response = swift.Containers().get(request)
self.assertStatusCode(response, 200)

View File

@ -33,6 +33,7 @@ class SwiftApiTests(test.APITestCase):
swift_api = self.stub_swiftclient()
swift_api.get_account(limit=1001,
marker=None,
prefix=None,
full_listing=True).AndReturn([{}, cont_data])
self.mox.ReplayAll()