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:
parent
ce7dc70c7f
commit
277d15abff
@ -72,7 +72,8 @@
|
|||||||
getName: getName,
|
getName: getName,
|
||||||
getTextFacet: getTextFacet,
|
getTextFacet: getTextFacet,
|
||||||
getUnusedFacetChoices: getUnusedFacetChoices,
|
getUnusedFacetChoices: getUnusedFacetChoices,
|
||||||
getQueryPattern: getQueryPattern
|
getQueryPattern: getQueryPattern,
|
||||||
|
getQueryObject: getQueryObject
|
||||||
};
|
};
|
||||||
|
|
||||||
return service;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
@ -325,5 +325,15 @@
|
|||||||
{name: "other"}]);
|
{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"});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
@ -53,9 +53,18 @@ class Containers(generic.View):
|
|||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""Get the list of containers for this account
|
"""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
|
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]
|
containers = [container.to_dict() for container in containers]
|
||||||
return {'items': containers, 'has_more': has_more}
|
return {'items': containers, 'has_more': has_more}
|
||||||
|
|
||||||
|
@ -135,10 +135,11 @@ def swift_object_exists(request, container_name, object_name):
|
|||||||
|
|
||||||
|
|
||||||
@profiler.trace
|
@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)
|
limit = getattr(settings, 'API_RESULT_LIMIT', 1000)
|
||||||
headers, containers = swift_api(request).get_account(limit=limit + 1,
|
headers, containers = swift_api(request).get_account(limit=limit + 1,
|
||||||
marker=marker,
|
marker=marker,
|
||||||
|
prefix=prefix,
|
||||||
full_listing=True)
|
full_listing=True)
|
||||||
container_objs = [Container(c) for c in containers]
|
container_objs = [Container(c) for c in containers]
|
||||||
if(len(container_objs) > limit):
|
if(len(container_objs) > limit):
|
||||||
|
@ -63,6 +63,7 @@
|
|||||||
updateContainer: updateContainer,
|
updateContainer: updateContainer,
|
||||||
recursiveCollect: recursiveCollect,
|
recursiveCollect: recursiveCollect,
|
||||||
recursiveDelete: recursiveDelete,
|
recursiveDelete: recursiveDelete,
|
||||||
|
getContainers: getContainers,
|
||||||
|
|
||||||
_recursiveDeleteFiles: recursiveDeleteFiles,
|
_recursiveDeleteFiles: recursiveDeleteFiles,
|
||||||
_recursiveDeleteFolders: recursiveDeleteFolders
|
_recursiveDeleteFolders: recursiveDeleteFolders
|
||||||
@ -72,6 +73,8 @@
|
|||||||
// initialisation completing (i.e. containers listing loaded)
|
// initialisation completing (i.e. containers listing loaded)
|
||||||
model.intialiseDeferred = $q.defer();
|
model.intialiseDeferred = $q.defer();
|
||||||
|
|
||||||
|
model.getContainersDeferred = $q.defer();
|
||||||
|
|
||||||
return model;
|
return model;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -370,5 +373,25 @@
|
|||||||
return deleteFolder(node.folder);
|
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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
@ -192,6 +192,22 @@
|
|||||||
expect(service.objects).toEqual([{name: 'two'}]);
|
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() {
|
describe('recursive deletion', function describe() {
|
||||||
// fake up a basic set of object listings to return from our fake getObjects
|
// fake up a basic set of object listings to return from our fake getObjects
|
||||||
// below
|
// below
|
||||||
@ -359,6 +375,27 @@
|
|||||||
expect(state.deleted.folders).toEqual(4);
|
expect(state.deleted.folders).toEqual(4);
|
||||||
expect(state.deleted.failures).toEqual(0);
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
@ -38,6 +38,9 @@
|
|||||||
'horizon.framework.widgets.form.ModalFormService',
|
'horizon.framework.widgets.form.ModalFormService',
|
||||||
'horizon.framework.widgets.modal.simple-modal.service',
|
'horizon.framework.widgets.modal.simple-modal.service',
|
||||||
'horizon.framework.widgets.toast.service',
|
'horizon.framework.widgets.toast.service',
|
||||||
|
'horizon.framework.widgets.magic-search.events',
|
||||||
|
'horizon.framework.widgets.magic-search.service',
|
||||||
|
'$scope',
|
||||||
'$location',
|
'$location',
|
||||||
'$q'
|
'$q'
|
||||||
];
|
];
|
||||||
@ -50,6 +53,9 @@
|
|||||||
modalFormService,
|
modalFormService,
|
||||||
simpleModalService,
|
simpleModalService,
|
||||||
toastService,
|
toastService,
|
||||||
|
magicSearchEvents,
|
||||||
|
magicSearchService,
|
||||||
|
$scope,
|
||||||
$location,
|
$location,
|
||||||
$q) {
|
$q) {
|
||||||
var ctrl = this;
|
var ctrl = this;
|
||||||
@ -57,6 +63,20 @@
|
|||||||
ctrl.model.initialize();
|
ctrl.model.initialize();
|
||||||
ctrl.baseRoute = baseRoute;
|
ctrl.baseRoute = baseRoute;
|
||||||
ctrl.containerRoute = containerRoute;
|
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.checkContainerNameConflict = checkContainerNameConflict;
|
||||||
ctrl.toggleAccess = toggleAccess;
|
ctrl.toggleAccess = toggleAccess;
|
||||||
@ -81,8 +101,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function selectContainer(container) {
|
function selectContainer(container) {
|
||||||
|
if (!ctrl.model.container || container.name !== ctrl.model.container.name) {
|
||||||
|
ctrl.filterEventTrigeredBySearchBar = false;
|
||||||
|
}
|
||||||
ctrl.model.container = container;
|
ctrl.model.container = container;
|
||||||
$location.path(ctrl.containerRoute + container.name);
|
$location.path(ctrl.containerRoute + container.name);
|
||||||
|
|
||||||
return ctrl.model.fetchContainerDetail(container);
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
@ -25,6 +25,13 @@
|
|||||||
var fakeModel = {
|
var fakeModel = {
|
||||||
loadContainerContents: angular.noop,
|
loadContainerContents: angular.noop,
|
||||||
initialize: angular.noop,
|
initialize: angular.noop,
|
||||||
|
getContainers: function fake(query) {
|
||||||
|
return {
|
||||||
|
then: function then(callback) {
|
||||||
|
callback(query);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
fetchContainerDetail: function fake() {
|
fetchContainerDetail: function fake() {
|
||||||
return {
|
return {
|
||||||
then: function then(callback) {
|
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) {
|
beforeEach(module('horizon.dashboard.project.containers', function($provide) {
|
||||||
$provide.value('horizon.dashboard.project.containers.containers-model', fakeModel);
|
$provide.value('horizon.dashboard.project.containers.containers-model', fakeModel);
|
||||||
@ -48,11 +56,12 @@
|
|||||||
$q = _$q_;
|
$q = _$q_;
|
||||||
$location = $injector.get('$location');
|
$location = $injector.get('$location');
|
||||||
$rootScope = _$rootScope_;
|
$rootScope = _$rootScope_;
|
||||||
|
scope = $rootScope.$new();
|
||||||
modalFormService = $injector.get('horizon.framework.widgets.form.ModalFormService');
|
modalFormService = $injector.get('horizon.framework.widgets.form.ModalFormService');
|
||||||
simpleModal = $injector.get('horizon.framework.widgets.modal.simple-modal.service');
|
simpleModal = $injector.get('horizon.framework.widgets.modal.simple-modal.service');
|
||||||
swiftAPI = $injector.get('horizon.app.core.openstack-service-api.swift');
|
swiftAPI = $injector.get('horizon.app.core.openstack-service-api.swift');
|
||||||
toast = $injector.get('horizon.framework.widgets.toast.service');
|
toast = $injector.get('horizon.framework.widgets.toast.service');
|
||||||
|
fakeModel.getContainersDeferred = $q.defer();
|
||||||
spyOn(fakeModel, 'initialize');
|
spyOn(fakeModel, 'initialize');
|
||||||
spyOn(fakeModel, 'loadContainerContents');
|
spyOn(fakeModel, 'loadContainerContents');
|
||||||
spyOn(fakeModel, 'fetchContainerDetail').and.callThrough();
|
spyOn(fakeModel, 'fetchContainerDetail').and.callThrough();
|
||||||
@ -62,6 +71,7 @@
|
|||||||
function createController() {
|
function createController() {
|
||||||
return controller(
|
return controller(
|
||||||
'horizon.dashboard.project.containers.ContainersController', {
|
'horizon.dashboard.project.containers.ContainersController', {
|
||||||
|
$scope:scope,
|
||||||
'horizon.dashboard.project.containers.baseRoute': 'base ham',
|
'horizon.dashboard.project.containers.baseRoute': 'base ham',
|
||||||
'horizon.dashboard.project.containers.containerRoute': 'eggs '
|
'horizon.dashboard.project.containers.containerRoute': 'eggs '
|
||||||
});
|
});
|
||||||
@ -258,5 +268,28 @@
|
|||||||
expect(fakeModel.containers[0].name).toEqual('spam');
|
expect(fakeModel.containers[0].name).toEqual('spam');
|
||||||
expect(fakeModel.containers.length).toEqual(1);
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
@ -11,7 +11,10 @@
|
|||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-12">
|
<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"
|
<div uib-accordion-group ng-repeat="container in cc.model.containers"
|
||||||
ng-class="{'panel-primary': container.name === cc.model.container.name}"
|
ng-class="{'panel-primary': container.name === cc.model.container.name}"
|
||||||
class="panel-default"
|
class="panel-default"
|
||||||
@ -42,15 +45,15 @@
|
|||||||
<ul ng-if="container.is_fetched" class="hz-object-detail list-unstyled">
|
<ul ng-if="container.is_fetched" class="hz-object-detail list-unstyled">
|
||||||
<li class="hz-object-count row">
|
<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-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>
|
||||||
<li class="hz-object-size row">
|
<li class="hz-object-size row">
|
||||||
<span class="hz-object-label col-lg-7 col-md-12" translate>Size</span>
|
<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>
|
||||||
<li class="hz-object-timestamp row">
|
<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-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>
|
||||||
<li class="hz-object-link row">
|
<li class="hz-object-link row">
|
||||||
<div class="themable-checkbox col-lg-7 col-md-12">
|
<div class="themable-checkbox col-lg-7 col-md-12">
|
||||||
@ -68,6 +71,9 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div uib-accordion-group>
|
</div uib-accordion-group>
|
||||||
</uib-accordion>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -97,8 +97,9 @@
|
|||||||
* @returns {Object} An object with 'items' and 'has_more' flag.
|
* @returns {Object} An object with 'items' and 'has_more' flag.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
function getContainers() {
|
function getContainers(params) {
|
||||||
return apiService.get('/api/swift/containers/')
|
var config = params ? {'params': params} : {};
|
||||||
|
return apiService.get('/api/swift/containers/', config)
|
||||||
.error(function() {
|
.error(function() {
|
||||||
toastService.add('error', gettext('Unable to get the Swift container listing.'));
|
toastService.add('error', gettext('Unable to get the Swift container listing.'));
|
||||||
});
|
});
|
||||||
|
@ -50,7 +50,8 @@
|
|||||||
func: 'getContainers',
|
func: 'getContainers',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
path: '/api/swift/containers/',
|
path: '/api/swift/containers/',
|
||||||
error: 'Unable to get the Swift container listing.'
|
error: 'Unable to get the Swift container listing.',
|
||||||
|
data: {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
func: 'getContainer',
|
func: 'getContainer',
|
||||||
|
@ -49,7 +49,7 @@ class SwiftRestTestCase(test.TestCase):
|
|||||||
#
|
#
|
||||||
@mock.patch.object(swift.api, 'swift')
|
@mock.patch.object(swift.api, 'swift')
|
||||||
def test_containers_get(self, nc):
|
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)
|
nc.swift_get_containers.return_value = (self._containers, False)
|
||||||
response = swift.Containers().get(request)
|
response = swift.Containers().get(request)
|
||||||
self.assertStatusCode(response, 200)
|
self.assertStatusCode(response, 200)
|
||||||
|
@ -33,6 +33,7 @@ class SwiftApiTests(test.APITestCase):
|
|||||||
swift_api = self.stub_swiftclient()
|
swift_api = self.stub_swiftclient()
|
||||||
swift_api.get_account(limit=1001,
|
swift_api.get_account(limit=1001,
|
||||||
marker=None,
|
marker=None,
|
||||||
|
prefix=None,
|
||||||
full_listing=True).AndReturn([{}, cont_data])
|
full_listing=True).AndReturn([{}, cont_data])
|
||||||
self.mox.ReplayAll()
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user