Angular metadata update modal
This patch adds metadata update modal dialog widet written in js and some required REST API methods. To see it in action checkout following patch https://review.openstack.org/#/c/184275/ which replaces old metadata modals with new ones written in angular. Co-Authored-By: Shaoquan Chen <sean.chen2@hp.com> Co-Authored-By: Rajat Vig <rajatv@thoughtworks.com> Partially-Implements: blueprint angularize-metadata-update-modals Change-Id: I36bfb91f8b6bbba49fed6bb01cd1dd266261cfdb
This commit is contained in:
parent
4156af87b9
commit
f030262521
@ -30,7 +30,7 @@ CLIENT_KEYWORDS = {'resource_type', 'marker', 'sort_dir', 'sort_key', 'paginate'
|
||||
class Image(generic.View):
|
||||
"""API for retrieving a single image
|
||||
"""
|
||||
url_regex = r'glance/images/(?P<image_id>.+|default)$'
|
||||
url_regex = r'glance/images/(?P<image_id>[^/]+|default)/$'
|
||||
|
||||
@rest_utils.ajax()
|
||||
def get(self, request, image_id):
|
||||
@ -41,6 +41,30 @@ class Image(generic.View):
|
||||
return api.glance.image_get(request, image_id).to_dict()
|
||||
|
||||
|
||||
@urls.register
|
||||
class ImageProperties(generic.View):
|
||||
"""API for retrieving only a custom properties of single image.
|
||||
"""
|
||||
url_regex = r'glance/images/(?P<image_id>[^/]+)/properties/'
|
||||
|
||||
@rest_utils.ajax()
|
||||
def get(self, request, image_id):
|
||||
"""Get custom properties of specific image.
|
||||
"""
|
||||
return api.glance.image_get(request, image_id).properties
|
||||
|
||||
@rest_utils.ajax(data_required=True)
|
||||
def patch(self, request, image_id):
|
||||
"""Update custom properties of specific image.
|
||||
|
||||
This method returns HTTP 204 (no content) on success.
|
||||
"""
|
||||
api.glance.image_update_properties(
|
||||
request, image_id, request.DATA.get('removed'),
|
||||
**request.DATA['updated']
|
||||
)
|
||||
|
||||
|
||||
@urls.register
|
||||
class Images(generic.View):
|
||||
"""API for Glance images.
|
||||
|
@ -250,7 +250,7 @@ class Flavors(generic.View):
|
||||
class Flavor(generic.View):
|
||||
"""API for retrieving a single flavor
|
||||
"""
|
||||
url_regex = r'nova/flavors/(?P<flavor_id>.+)/$'
|
||||
url_regex = r'nova/flavors/(?P<flavor_id>[^/]+)/$'
|
||||
|
||||
@rest_utils.ajax()
|
||||
def get(self, request, flavor_id):
|
||||
@ -274,7 +274,7 @@ class Flavor(generic.View):
|
||||
class FlavorExtraSpecs(generic.View):
|
||||
"""API for managing flavor extra specs
|
||||
"""
|
||||
url_regex = r'nova/flavors/(?P<flavor_id>.+)/extra-specs$'
|
||||
url_regex = r'nova/flavors/(?P<flavor_id>[^/]+)/extra-specs/$'
|
||||
|
||||
@rest_utils.ajax()
|
||||
def get(self, request, flavor_id):
|
||||
@ -284,3 +284,45 @@ class FlavorExtraSpecs(generic.View):
|
||||
http://localhost/api/nova/flavors/1/extra-specs
|
||||
"""
|
||||
return api.nova.flavor_get_extras(request, flavor_id, raw=True)
|
||||
|
||||
@rest_utils.ajax(data_required=True)
|
||||
def patch(self, request, flavor_id):
|
||||
"""Update a specific flavor's extra specs.
|
||||
|
||||
This method returns HTTP 204 (no content) on success.
|
||||
"""
|
||||
if request.DATA.get('removed'):
|
||||
api.nova.flavor_extra_delete(
|
||||
request, flavor_id, request.DATA.get('removed')
|
||||
)
|
||||
api.nova.flavor_extra_set(
|
||||
request, flavor_id, request.DATA['updated']
|
||||
)
|
||||
|
||||
|
||||
@urls.register
|
||||
class AggregateExtraSpecs(generic.View):
|
||||
"""API for managing aggregate extra specs
|
||||
"""
|
||||
url_regex = r'nova/aggregates/(?P<aggregate_id>[^/]+)/extra-specs/$'
|
||||
|
||||
@rest_utils.ajax()
|
||||
def get(self, request, aggregate_id):
|
||||
"""Get a specific aggregate's extra specs
|
||||
|
||||
Example GET:
|
||||
http://localhost/api/nova/flavors/1/extra-specs
|
||||
"""
|
||||
return api.nova.aggregate_get(request, aggregate_id).metadata
|
||||
|
||||
@rest_utils.ajax(data_required=True)
|
||||
def patch(self, request, aggregate_id):
|
||||
"""Update a specific aggregate's extra specs.
|
||||
|
||||
This method returns HTTP 204 (no content) on success.
|
||||
"""
|
||||
updated = request.DATA['updated']
|
||||
if request.DATA.get('removed'):
|
||||
for name in request.DATA.get('removed'):
|
||||
updated[name] = None
|
||||
api.nova.aggregate_set_metadata(request, aggregate_id, updated)
|
||||
|
@ -28,10 +28,18 @@
|
||||
*/
|
||||
angular
|
||||
.module('horizon.app.core', [
|
||||
'horizon.app.core.cloud-services',
|
||||
'horizon.app.core.images',
|
||||
'horizon.app.core.workflow',
|
||||
'horizon.app.core.metadata',
|
||||
'horizon.app.core.openstack-service-api',
|
||||
'horizon.app.core.cloud-services'
|
||||
]);
|
||||
'horizon.app.core.workflow'
|
||||
], config);
|
||||
|
||||
config.$inject = ['$provide', '$windowProvider'];
|
||||
|
||||
function config($provide, $windowProvider) {
|
||||
var path = $windowProvider.$get().STATIC_URL + 'app/core/';
|
||||
$provide.constant('horizon.app.core.basePath', path);
|
||||
}
|
||||
|
||||
})();
|
||||
|
@ -22,4 +22,16 @@
|
||||
});
|
||||
});
|
||||
|
||||
describe('horizon.app.core.basePath', function () {
|
||||
beforeEach(module('horizon.app.core'));
|
||||
|
||||
it('should be defined and set correctly', inject([
|
||||
'horizon.app.core.basePath', '$window',
|
||||
function (basePath, $window) {
|
||||
expect(basePath).toBeDefined();
|
||||
expect(basePath).toBe($window.STATIC_URL + 'app/core/');
|
||||
}])
|
||||
);
|
||||
});
|
||||
|
||||
})();
|
||||
|
@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2015, Intel Corp.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* @ngdoc overview
|
||||
* @name horizon.app.core.metadata
|
||||
* @description
|
||||
*
|
||||
* # horizon.app.core.metadata
|
||||
*
|
||||
* The `horizon.app.core.metadata` provides provides metadata service.
|
||||
*
|
||||
* | Components |
|
||||
* |------------------------------------------------------------------------------|
|
||||
* | {@link horizon.app.core.metadata.service:metadataService `metadataService`} |
|
||||
*
|
||||
*/
|
||||
angular
|
||||
.module('horizon.app.core.metadata', [
|
||||
'horizon.app.core.metadata.modal'
|
||||
]);
|
||||
|
||||
})();
|
@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2015, Intel Corp.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
describe('horizon.app.core.metadata', function () {
|
||||
it('should be defined', function () {
|
||||
expect(angular.module('horizon.app.core.metadata')).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
})();
|
@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2015, Intel Corp.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('horizon.app.core.metadata')
|
||||
.factory('horizon.app.core.metadata.service', metadataService);
|
||||
|
||||
metadataService.$inject = [
|
||||
'horizon.app.core.openstack-service-api.nova',
|
||||
'horizon.app.core.openstack-service-api.glance'
|
||||
];
|
||||
|
||||
/**
|
||||
* @ngdoc service
|
||||
* @name metadataService
|
||||
* @description
|
||||
*
|
||||
* Unified acquisition and modification of metadata.
|
||||
*/
|
||||
function metadataService(nova, glance) {
|
||||
var service = {
|
||||
getMetadata: getMetadata,
|
||||
editMetadata: editMetadata,
|
||||
getNamespaces: getNamespaces
|
||||
};
|
||||
|
||||
return service;
|
||||
|
||||
/**
|
||||
* Get metadata from specified resource.
|
||||
*
|
||||
* @param {string} resource Resource type.
|
||||
* @param {string} id Resource identifier.
|
||||
*/
|
||||
function getMetadata(resource, id) {
|
||||
return {
|
||||
aggregate: nova.getAggregateExtraSpecs,
|
||||
flavor: nova.getFlavorExtraSpecs,
|
||||
image: glance.getImageProps
|
||||
}[resource](id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit metadata of specified resource.
|
||||
*
|
||||
* @param {string} resource Resource type.
|
||||
* @param {string} id Resource identifier.
|
||||
* @param {object} updated New metadata.
|
||||
* @param {[]} removed Names of removed metadata.
|
||||
*/
|
||||
function editMetadata(resource, id, updated, removed) {
|
||||
return {
|
||||
aggregate: nova.editAggregateExtraSpecs,
|
||||
flavor: nova.editFlavorExtraSpecs,
|
||||
image: glance.editImageProps
|
||||
}[resource](id, updated, removed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available metadata namespaces for specified resource.
|
||||
*
|
||||
* @param {string} resource Resource type.
|
||||
*/
|
||||
function getNamespaces(resource) {
|
||||
return glance.getNamespaces({
|
||||
resource_type: {
|
||||
aggregate: 'OS::Nova::Aggregate',
|
||||
flavor: 'OS::Nova::Flavor',
|
||||
image: 'OS::Glance::Image'
|
||||
}[resource]
|
||||
}, false);
|
||||
}
|
||||
}
|
||||
})();
|
@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright 2015, ThoughtWorks Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
describe('metadata.service', function () {
|
||||
|
||||
beforeEach(module('horizon.app.core.metadata'));
|
||||
|
||||
var nova = {getAggregateExtraSpecs: function() {},
|
||||
getFlavorExtraSpecs: function() {},
|
||||
editAggregateExtraSpecs: function() {},
|
||||
editFlavorExtraSpecs: function() {} };
|
||||
|
||||
var glance = {getImageProps: function() {},
|
||||
editImageProps: function() {},
|
||||
getNamespaces: function() {}};
|
||||
|
||||
beforeEach(function() {
|
||||
module(function($provide) {
|
||||
$provide.value('horizon.app.core.openstack-service-api.nova', nova);
|
||||
$provide.value('horizon.app.core.openstack-service-api.glance', glance);
|
||||
});
|
||||
});
|
||||
|
||||
var metadataService;
|
||||
|
||||
beforeEach(inject(function($injector) {
|
||||
metadataService = $injector.get('horizon.app.core.metadata.service');
|
||||
}));
|
||||
|
||||
it('should get aggregate metadata', function() {
|
||||
var expected = 'aggregate metadata';
|
||||
spyOn(nova, 'getAggregateExtraSpecs').and.returnValue(expected);
|
||||
var actual = metadataService.getMetadata('aggregate', '1');
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
|
||||
it('should edit aggregate metadata', function() {
|
||||
spyOn(nova, 'editAggregateExtraSpecs');
|
||||
metadataService.editMetadata('aggregate', '1', 'updated', ['removed']);
|
||||
expect(nova.editAggregateExtraSpecs).toHaveBeenCalledWith('1', 'updated', ['removed']);
|
||||
});
|
||||
|
||||
it('should get aggregate namespace', function() {
|
||||
spyOn(glance, 'getNamespaces');
|
||||
var actual = metadataService.getNamespaces('aggregate');
|
||||
expect(glance.getNamespaces)
|
||||
.toHaveBeenCalledWith({ resource_type: 'OS::Nova::Aggregate' }, false);
|
||||
});
|
||||
|
||||
it('should get flavor metadata', function() {
|
||||
var expected = 'flavor metadata';
|
||||
spyOn(nova, 'getFlavorExtraSpecs').and.returnValue(expected);
|
||||
var actual = metadataService.getMetadata('flavor', '1');
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
|
||||
it('should edit flavor metadata', function() {
|
||||
spyOn(nova, 'editFlavorExtraSpecs');
|
||||
metadataService.editMetadata('flavor', '1', 'updated', ['removed']);
|
||||
expect(nova.editFlavorExtraSpecs).toHaveBeenCalledWith('1', 'updated', ['removed']);
|
||||
});
|
||||
|
||||
it('should get flavor namespace', function() {
|
||||
spyOn(glance, 'getNamespaces');
|
||||
var actual = metadataService.getNamespaces('flavor');
|
||||
expect(glance.getNamespaces)
|
||||
.toHaveBeenCalledWith({ resource_type: 'OS::Nova::Flavor' }, false);
|
||||
});
|
||||
|
||||
it('should get image metadata', function() {
|
||||
var expected = 'image metadata';
|
||||
spyOn(glance, 'getImageProps').and.returnValue(expected);
|
||||
var actual = metadataService.getMetadata('image', '1');
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
|
||||
it('should edit image metadata', function() {
|
||||
spyOn(glance, 'editImageProps');
|
||||
metadataService.editMetadata('image', '1', 'updated', ['removed']);
|
||||
expect(glance.editImageProps).toHaveBeenCalledWith('1', 'updated', ['removed']);
|
||||
});
|
||||
|
||||
it('should get image namespace', function() {
|
||||
spyOn(glance, 'getNamespaces');
|
||||
var actual = metadataService.getNamespaces('image');
|
||||
expect(glance.getNamespaces)
|
||||
.toHaveBeenCalledWith({ resource_type: 'OS::Glance::Image' }, false);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
})();
|
@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2015, Intel Corp.
|
||||
* (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('horizon.app.core.metadata.modal')
|
||||
.controller('MetadataModalHelperController', MetadataModalHelperController);
|
||||
|
||||
MetadataModalHelperController.$inject = [
|
||||
'$window',
|
||||
'horizon.app.core.metadata.modal.service'
|
||||
];
|
||||
|
||||
/**
|
||||
* @ngdoc controller
|
||||
* @name horizon.app.core.metadata.modal.controller:MetadataModalHelperController
|
||||
* @description
|
||||
* Helper controller used by Horizon part written in Django.
|
||||
*/
|
||||
function MetadataModalHelperController($window, metadataModalService) {
|
||||
//NOTE(bluex): controller should be removed when reload is no longer needed
|
||||
var ctrl = this;
|
||||
|
||||
ctrl.openMetadataModal = openMetadataModal;
|
||||
|
||||
/**
|
||||
* Open modal allowing to edit metadata
|
||||
*
|
||||
* @param {string} resource Metadata resource type
|
||||
* @param {string} id Object identifier to retrieve metadata from
|
||||
* @param {boolean=} requireReload Whether to reload page when metadata successfully updated
|
||||
*/
|
||||
function openMetadataModal(resource, id, requireReload) {
|
||||
metadataModalService.open(resource, id)
|
||||
.result
|
||||
.then(onOpened);
|
||||
|
||||
function onOpened() {
|
||||
if (requireReload) {
|
||||
$window.location.reload();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})();
|
@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Copyright 2015 ThoughtWorks Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License. You may obtain
|
||||
* a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
describe('MetadataModalHelperController', function () {
|
||||
var $controller, $window;
|
||||
|
||||
var metadataModalService = {
|
||||
open: function () {
|
||||
return {
|
||||
result: {
|
||||
then: function (callback) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
$window = {
|
||||
location: {
|
||||
reload: jasmine.createSpy()
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(module('horizon.app.core.metadata.modal'));
|
||||
beforeEach(inject(function (_$controller_) {
|
||||
$controller = _$controller_;
|
||||
}));
|
||||
|
||||
it('should reload window if required', function () {
|
||||
var params = {
|
||||
$window: $window,
|
||||
'horizon.app.core.metadata.modal.service': metadataModalService
|
||||
};
|
||||
var controller = $controller('MetadataModalHelperController', params);
|
||||
controller.openMetadataModal('aggregate', '123', true);
|
||||
expect($window.location.reload).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not reload window if not required', function () {
|
||||
var params = {
|
||||
$window: $window,
|
||||
'horizon.app.core.metadata.modal.service': metadataModalService
|
||||
};
|
||||
var controller = $controller('MetadataModalHelperController', params);
|
||||
controller.openMetadataModal('aggregate', '123', false);
|
||||
expect($window.location.reload).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
||||
})();
|
@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright 2015, Intel Corp.
|
||||
* (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('horizon.app.core.metadata.modal')
|
||||
.controller('MetadataModalController', MetadataModalController);
|
||||
|
||||
MetadataModalController.$inject = [
|
||||
'$modalInstance',
|
||||
'horizon.framework.widgets.metadata.tree.service',
|
||||
'horizon.app.core.metadata.service',
|
||||
// Dependencies injected with resolve by $modal.open
|
||||
'available',
|
||||
'existing',
|
||||
'params'
|
||||
];
|
||||
|
||||
/**
|
||||
* @ngdoc controller
|
||||
* @name MetadataModalController
|
||||
* @description
|
||||
* Controller used by `ModalService`
|
||||
*/
|
||||
function MetadataModalController(
|
||||
$modalInstance, metadataTreeService, metadataService,
|
||||
available, existing, params
|
||||
) {
|
||||
var ctrl = this;
|
||||
|
||||
ctrl.cancel = cancel;
|
||||
ctrl.resourceType = params.resource;
|
||||
ctrl.save = save;
|
||||
ctrl.saving = false;
|
||||
ctrl.tree = new metadataTreeService.Tree(available.data.items, existing.data);
|
||||
|
||||
function save() {
|
||||
ctrl.saving = true;
|
||||
var updated = ctrl.tree.getExisting();
|
||||
var removed = angular.copy(existing.data);
|
||||
angular.forEach(updated, function(value, key) {
|
||||
delete removed[key];
|
||||
});
|
||||
|
||||
metadataService
|
||||
.editMetadata(params.resource, params.id, updated, Object.keys(removed))
|
||||
.then(onEditSuccess, onEditFailure);
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
$modalInstance.dismiss('cancel');
|
||||
}
|
||||
|
||||
function onEditSuccess() {
|
||||
$modalInstance.close();
|
||||
}
|
||||
|
||||
function onEditFailure() {
|
||||
ctrl.saving = false;
|
||||
}
|
||||
}
|
||||
})();
|
@ -0,0 +1,98 @@
|
||||
/**
|
||||
* Copyright 2015 ThoughtWorks Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License. You may obtain
|
||||
* a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
describe('MetadataModalController', function () {
|
||||
var $controller, treeService, modalInstance;
|
||||
|
||||
var metadataService = {
|
||||
editMetadata: function() {}
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
modalInstance = {
|
||||
dismiss: jasmine.createSpy(),
|
||||
close: jasmine.createSpy()
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(module('horizon.app.core.metadata.modal',
|
||||
'horizon.framework.widgets.metadata.tree'));
|
||||
beforeEach(inject(function (_$controller_, $injector) {
|
||||
$controller = _$controller_;
|
||||
treeService = $injector.get('horizon.framework.widgets.metadata.tree.service');
|
||||
}));
|
||||
|
||||
it('should dismiss modal on cancel', function () {
|
||||
var controller = createController(modalInstance);
|
||||
|
||||
controller.cancel();
|
||||
|
||||
expect(modalInstance.dismiss).toHaveBeenCalledWith('cancel');
|
||||
});
|
||||
|
||||
it('should close modal on successful save', function () {
|
||||
var controller = createController(modalInstance);
|
||||
metadataService.editMetadata = function() {
|
||||
return {
|
||||
then: function(success, fail) {
|
||||
success();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
spyOn(metadataService, 'editMetadata').and.callThrough();
|
||||
|
||||
controller.save();
|
||||
|
||||
expect(modalInstance.close).toHaveBeenCalled();
|
||||
expect(metadataService.editMetadata)
|
||||
.toHaveBeenCalledWith('aggregate', '123', {someProperty: 'someValue'}, []);
|
||||
});
|
||||
|
||||
it('should clear saving flag on failed save', function() {
|
||||
var controller = createController(modalInstance);
|
||||
metadataService.editMetadata = function() {
|
||||
return {
|
||||
then: function(success, fail) {
|
||||
fail();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
spyOn(metadataService, 'editMetadata').and.callThrough();
|
||||
|
||||
controller.save();
|
||||
|
||||
expect(modalInstance.close).not.toHaveBeenCalled();
|
||||
expect(metadataService.editMetadata)
|
||||
.toHaveBeenCalledWith('aggregate', '123', {someProperty: 'someValue'}, []);
|
||||
});
|
||||
|
||||
function createController() {
|
||||
return $controller('MetadataModalController', {
|
||||
'$modalInstance': modalInstance,
|
||||
'horizon.framework.widgets.metadata.tree.service': treeService,
|
||||
'horizon.app.core.metadata.service': metadataService,
|
||||
'available': {data: {}},
|
||||
'existing': {data: {someProperty: 'someValue'}},
|
||||
'params': {resource: 'aggregate', id: '123'}
|
||||
});
|
||||
}
|
||||
});
|
||||
})();
|
@ -0,0 +1,22 @@
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">
|
||||
<span translate ng-if="modal.resourceType==='aggregate'">Update Aggregate Metadata</span>
|
||||
<span translate ng-if="modal.resourceType==='flavor'">Update Flavor Metadata</span>
|
||||
<span translate ng-if="modal.resourceType==='image'">Update Image Metadata</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<metadata-tree model="modal.tree" form="form"></metadata-tree>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-default" ng-click="modal.cancel()">
|
||||
<i class="fa fa-close"></i>
|
||||
<span translate>Cancel</span>
|
||||
</button>
|
||||
<button class="btn btn-primary"
|
||||
ng-click="modal.save()"
|
||||
ng-disabled="form.$invalid">
|
||||
<i class="fa" ng-class="modal.saving ? 'fa-spinner fa-spin' : 'fa-save'"></i>
|
||||
<span translate>Save</span>
|
||||
</button>
|
||||
</div>
|
@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2015, Intel Corp.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
/*eslint-disable max-len */
|
||||
/**
|
||||
* @ngdoc overview
|
||||
* @name horizon.app.core.metadata.modal
|
||||
* @description
|
||||
*
|
||||
* # horizon.app.core.metadata.modal
|
||||
*
|
||||
* The `horizon.app.core.metadata.modal` provides provides metadata modal service.
|
||||
*
|
||||
* Requires {@link http://angular-ui.github.io/bootstrap/ `Angular-bootstrap`}
|
||||
*
|
||||
* | Components |
|
||||
* |---------------------------------------------------------------------------------------------|
|
||||
* | {@link horizon.app.core.metadata.modal.service:modalService `modalService`} |
|
||||
* | {@link horizon.app.core.metadata.modal.controller:MetadataModalController `MetadataModalController`} |
|
||||
* | {@link horizon.app.core.metadata.modal.controller:MetadataModalHelperController `MetadataModalHelperController`} |
|
||||
*
|
||||
*/
|
||||
/*eslint-enable max-len */
|
||||
angular
|
||||
.module('horizon.app.core.metadata.modal', [])
|
||||
.constant('horizon.app.core.metadata.modal.constants', {
|
||||
backdrop: 'static',
|
||||
controller: 'MetadataModalController as modal',
|
||||
windowClass: 'modal-dialog-metadata'
|
||||
});
|
||||
|
||||
})();
|
@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2015, Intel Corp.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
describe('horizon.app.core.metadata.modal', function () {
|
||||
it('should be defined', function () {
|
||||
expect(angular.module('horizon.app.core.metadata.modal')).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
})();
|
@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2015, Intel Corp.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('horizon.app.core.metadata.modal')
|
||||
.factory('horizon.app.core.metadata.modal.service', modalService);
|
||||
|
||||
modalService.$inject = [
|
||||
'$modal',
|
||||
'horizon.app.core.basePath',
|
||||
'horizon.app.core.metadata.service',
|
||||
'horizon.app.core.metadata.modal.constants'
|
||||
];
|
||||
|
||||
/**
|
||||
* @ngdoc service
|
||||
* @name modalService
|
||||
*/
|
||||
function modalService($modal, path, metadataService, modalConstants) {
|
||||
var service = {
|
||||
open: open
|
||||
};
|
||||
|
||||
return service;
|
||||
|
||||
/**
|
||||
* Open modal allowing to edit metadata
|
||||
*
|
||||
* @param {string} resource Metadata resource type
|
||||
* @param {string} id Object identifier to retrieve metadata from
|
||||
*/
|
||||
function open(resource, id) {
|
||||
function resolveAvailable() {
|
||||
return metadataService.getNamespaces(resource);
|
||||
}
|
||||
function resolveExisting() {
|
||||
return metadataService.getMetadata(resource, id);
|
||||
}
|
||||
function resolveParams() {
|
||||
return {resource: resource, id: id};
|
||||
}
|
||||
|
||||
var resolve = {
|
||||
available: resolveAvailable,
|
||||
existing: resolveExisting,
|
||||
params: resolveParams
|
||||
};
|
||||
var modalParams = {
|
||||
resolve: resolve,
|
||||
templateUrl: path + 'metadata/modal/modal.html'
|
||||
};
|
||||
return $modal.open(angular.extend(modalParams, modalConstants));
|
||||
}
|
||||
|
||||
}
|
||||
})();
|
@ -0,0 +1,62 @@
|
||||
/*
|
||||
* (c) Copyright 2015 ThoughtWorks, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
describe('horizon.app.core.metadata.modal', function () {
|
||||
|
||||
describe('service.modalservice', function(){
|
||||
var modalService, metadataService, $modal;
|
||||
|
||||
beforeEach(module('ui.bootstrap', function($provide){
|
||||
$modal = jasmine.createSpyObj('$modal', ['open']);
|
||||
|
||||
$provide.value('$modal', $modal);
|
||||
}));
|
||||
|
||||
beforeEach(module('horizon.app.core', function($provide) {
|
||||
$provide.constant('horizon.app.core.basePath', '/a/sample/path/');
|
||||
}));
|
||||
|
||||
beforeEach(module('horizon.app.core.metadata', function($provide){
|
||||
metadataService = jasmine.createSpyObj('metadataService', ['getMetadata', 'getNamespaces']);
|
||||
$provide.value('horizon.app.core.metadata.service', metadataService);
|
||||
}));
|
||||
|
||||
beforeEach(module('horizon.app.core.metadata.modal'));
|
||||
|
||||
beforeEach(inject(function($controller, $injector) {
|
||||
modalService = $injector.get('horizon.app.core.metadata.modal.service');
|
||||
}));
|
||||
|
||||
it('should define service.open()', function() {
|
||||
expect(modalService.open).toBeDefined();
|
||||
});
|
||||
|
||||
it('should invoke $modal.open with correct params', function() {
|
||||
modalService.open('resource', 'id');
|
||||
|
||||
expect($modal.open).toHaveBeenCalled();
|
||||
|
||||
var args = $modal.open.calls.argsFor(0)[0];
|
||||
expect(args.templateUrl).toEqual('/a/sample/path/metadata/modal/modal.html');
|
||||
expect(args.resolve.params()).toEqual({resource: 'resource', id: 'id'});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
})();
|
@ -33,6 +33,8 @@
|
||||
function GlanceAPI(apiService, toastService) {
|
||||
var service = {
|
||||
getImage: getImage,
|
||||
getImageProps: getImageProps,
|
||||
editImageProps: editImageProps,
|
||||
getImages: getImages,
|
||||
getNamespaces: getNamespaces
|
||||
};
|
||||
@ -57,6 +59,40 @@
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @name horizon.app.core.openstack-service-api.glance.getImageProps
|
||||
* @description
|
||||
* Get an image custom properties by image ID
|
||||
* @param {string} id Specifies the id of the image to request.
|
||||
*/
|
||||
function getImageProps(id) {
|
||||
return apiService.get('/api/glance/images/' + id + '/properties/')
|
||||
.error(function () {
|
||||
toastService.add('error', gettext('Unable to retrieve the image custom properties.'));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @name horizon.app.core.openstack-service-api.glance.editImageProps
|
||||
* @description
|
||||
* Update an image custom properties by image ID
|
||||
* @param {string} id Specifies the id of the image to request.
|
||||
* @param {object} updated New metadata definitions.
|
||||
* @param {[]} removed Names of removed metadata definitions.
|
||||
*/
|
||||
function editImageProps(id, updated, removed) {
|
||||
return apiService.patch(
|
||||
'/api/glance/images/' + id + '/properties/',
|
||||
{
|
||||
updated: updated,
|
||||
removed: removed
|
||||
}
|
||||
)
|
||||
.error(function () {
|
||||
toastService.add('error', gettext('Unable to edit the image custom properties.'));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @name horizon.app.core.openstack-service-api.glance.getImages
|
||||
* @description
|
||||
|
@ -49,6 +49,28 @@
|
||||
42
|
||||
]
|
||||
},
|
||||
{
|
||||
"func": "getImageProps",
|
||||
"method": "get",
|
||||
"path": "/api/glance/images/42/properties/",
|
||||
"error": "Unable to retrieve the image custom properties.",
|
||||
"testInput": [
|
||||
42
|
||||
]
|
||||
},
|
||||
{
|
||||
"func": "editImageProps",
|
||||
"method": "patch",
|
||||
"path": "/api/glance/images/42/properties/",
|
||||
"data": {
|
||||
"updated": {a: '1', b: '2'},
|
||||
"removed": ['c', 'd']
|
||||
},
|
||||
"error": "Unable to edit the image custom properties.",
|
||||
"testInput": [
|
||||
42, {a: '1', b: '2'}, ['c', 'd']
|
||||
]
|
||||
},
|
||||
{
|
||||
"func": "getImages",
|
||||
"method": "get",
|
||||
|
@ -42,7 +42,10 @@
|
||||
getExtensions: getExtensions,
|
||||
getFlavors: getFlavors,
|
||||
getFlavor: getFlavor,
|
||||
getFlavorExtraSpecs: getFlavorExtraSpecs
|
||||
getFlavorExtraSpecs: getFlavorExtraSpecs,
|
||||
editFlavorExtraSpecs: editFlavorExtraSpecs,
|
||||
getAggregateExtraSpecs: getAggregateExtraSpecs,
|
||||
editAggregateExtraSpecs: editAggregateExtraSpecs
|
||||
};
|
||||
|
||||
return service;
|
||||
@ -289,11 +292,65 @@
|
||||
* Specifies the id of the flavor to request the extra specs.
|
||||
*/
|
||||
function getFlavorExtraSpecs(id) {
|
||||
return apiService.get('/api/nova/flavors/' + id + '/extra-specs')
|
||||
return apiService.get('/api/nova/flavors/' + id + '/extra-specs/')
|
||||
.error(function () {
|
||||
toastService.add('error', gettext('Unable to retrieve the flavor extra specs.'));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @name horizon.openstack-service-api.nova.editFlavorExtraSpecs
|
||||
* @description
|
||||
* Update a single flavor's extra specs by ID.
|
||||
* @param {string} id
|
||||
* @param {object} updated New extra specs.
|
||||
* @param {[]} removed Names of removed extra specs.
|
||||
*/
|
||||
function editFlavorExtraSpecs(id, updated, removed) {
|
||||
return apiService.patch(
|
||||
'/api/nova/flavors/' + id + '/extra-specs/',
|
||||
{
|
||||
updated: updated,
|
||||
removed: removed
|
||||
}
|
||||
).error(function () {
|
||||
toastService.add('error', gettext('Unable to edit the flavor extra specs.'));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @name horizon.openstack-service-api.nova.getAggregateExtraSpecs
|
||||
* @description
|
||||
* Get a single aggregate's extra specs by ID.
|
||||
* @param {string} id
|
||||
* Specifies the id of the flavor to request the extra specs.
|
||||
*/
|
||||
function getAggregateExtraSpecs(id) {
|
||||
return apiService.get('/api/nova/aggregates/' + id + '/extra-specs/')
|
||||
.error(function () {
|
||||
toastService.add('error', gettext('Unable to retrieve the aggregate extra specs.'));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @name horizon.openstack-service-api.nova.editAggregateExtraSpecs
|
||||
* @description
|
||||
* Update a single aggregate's extra specs by ID.
|
||||
* @param {string} id
|
||||
* @param {object} updated New extra specs.
|
||||
* @param {[]} removed Names of removed extra specs.
|
||||
*/
|
||||
function editAggregateExtraSpecs(id, updated, removed) {
|
||||
return apiService.patch(
|
||||
'/api/nova/aggregates/' + id + '/extra-specs/',
|
||||
{
|
||||
updated: updated,
|
||||
removed: removed
|
||||
}
|
||||
).error(function () {
|
||||
toastService.add('error', gettext('Unable to edit the aggregate extra specs.'));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}());
|
||||
|
@ -201,13 +201,47 @@
|
||||
{
|
||||
"func": "getFlavorExtraSpecs",
|
||||
"method": "get",
|
||||
"path": "/api/nova/flavors/42/extra-specs",
|
||||
"path": "/api/nova/flavors/42/extra-specs/",
|
||||
"error": "Unable to retrieve the flavor extra specs.",
|
||||
"testInput": [
|
||||
42
|
||||
]
|
||||
},
|
||||
{
|
||||
"func": "editFlavorExtraSpecs",
|
||||
"method": "patch",
|
||||
"path": "/api/nova/flavors/42/extra-specs/",
|
||||
"data": {
|
||||
"updated": {a: '1', b: '2'},
|
||||
"removed": ['c', 'd']
|
||||
},
|
||||
"error": "Unable to edit the flavor extra specs.",
|
||||
"testInput": [
|
||||
42, {a: '1', b: '2'}, ['c', 'd']
|
||||
]
|
||||
},
|
||||
{
|
||||
"func": "getAggregateExtraSpecs",
|
||||
"method": "get",
|
||||
"path": "/api/nova/aggregates/42/extra-specs/",
|
||||
"error": "Unable to retrieve the aggregate extra specs.",
|
||||
"testInput": [
|
||||
42
|
||||
]
|
||||
},
|
||||
{
|
||||
"func": "editAggregateExtraSpecs",
|
||||
"method": "patch",
|
||||
"path": "/api/nova/aggregates/42/extra-specs/",
|
||||
"data": {
|
||||
"updated": {a: '1', b: '2'},
|
||||
"removed": ['c', 'd']
|
||||
},
|
||||
"error": "Unable to edit the aggregate extra specs.",
|
||||
"testInput": [
|
||||
42, {a: '1', b: '2'}, ['c', 'd']
|
||||
]
|
||||
}
|
||||
|
||||
];
|
||||
|
||||
// Iterate through the defined tests and apply as Jasmine specs.
|
||||
|
@ -28,6 +28,29 @@ class ImagesRestTestCase(test.TestCase):
|
||||
self.assertStatusCode(response, 200)
|
||||
gc.image_get.assert_called_once_with(request, "1")
|
||||
|
||||
@mock.patch.object(glance.api, 'glance')
|
||||
def test_image_get_metadata(self, gc):
|
||||
request = self.mock_rest_request()
|
||||
gc.image_get.return_value.properties = {'a': '1', 'b': '2'}
|
||||
|
||||
response = glance.ImageProperties().get(request, "1")
|
||||
self.assertStatusCode(response, 200)
|
||||
self.assertEqual(response.content, '{"a": "1", "b": "2"}')
|
||||
gc.image_get.assert_called_once_with(request, "1")
|
||||
|
||||
@mock.patch.object(glance.api, 'glance')
|
||||
def test_image_edit_metadata(self, gc):
|
||||
request = self.mock_rest_request(
|
||||
body='{"updated": {"a": "1", "b": "2"}, "removed": ["c", "d"]}'
|
||||
)
|
||||
|
||||
response = glance.ImageProperties().patch(request, '1')
|
||||
self.assertStatusCode(response, 204)
|
||||
self.assertEqual(response.content, '')
|
||||
gc.image_update_properties.assert_called_once_with(
|
||||
request, '1', ['c', 'd'], a='1', b='2'
|
||||
)
|
||||
|
||||
@mock.patch.object(glance.api, 'glance')
|
||||
def test_image_get_list_detailed(self, gc):
|
||||
kwargs = {
|
||||
|
@ -266,10 +266,49 @@ class NovaRestTestCase(test.TestCase):
|
||||
self._test_flavor_list_extras(get_extras=None)
|
||||
|
||||
@mock.patch.object(nova.api, 'nova')
|
||||
def test_flavor_extra_specs(self, nc):
|
||||
def test_flavor_get_extra_specs(self, nc):
|
||||
request = self.mock_rest_request()
|
||||
nc.flavor_get_extras.return_value.to_dict.return_value = {'foo': '1'}
|
||||
|
||||
response = nova.FlavorExtraSpecs().get(request, "1")
|
||||
self.assertStatusCode(response, 200)
|
||||
nc.flavor_get_extras.assert_called_once_with(request, "1", raw=True)
|
||||
|
||||
@mock.patch.object(nova.api, 'nova')
|
||||
def test_flavor_edit_extra_specs(self, nc):
|
||||
request = self.mock_rest_request(
|
||||
body='{"updated": {"a": "1", "b": "2"}, "removed": ["c", "d"]}'
|
||||
)
|
||||
|
||||
response = nova.FlavorExtraSpecs().patch(request, '1')
|
||||
self.assertStatusCode(response, 204)
|
||||
self.assertEqual(response.content, '')
|
||||
nc.flavor_extra_set.assert_called_once_with(
|
||||
request, '1', {'a': '1', 'b': '2'}
|
||||
)
|
||||
nc.flavor_extra_delete.assert_called_once_with(
|
||||
request, '1', ['c', 'd']
|
||||
)
|
||||
|
||||
@mock.patch.object(nova.api, 'nova')
|
||||
def test_aggregate_get_extra_specs(self, nc):
|
||||
request = self.mock_rest_request()
|
||||
nc.aggregate_get.return_value.metadata = {'a': '1', 'b': '2'}
|
||||
|
||||
response = nova.AggregateExtraSpecs().get(request, "1")
|
||||
self.assertStatusCode(response, 200)
|
||||
self.assertEqual(response.content, '{"a": "1", "b": "2"}')
|
||||
nc.aggregate_get.assert_called_once_with(request, "1")
|
||||
|
||||
@mock.patch.object(nova.api, 'nova')
|
||||
def test_aggregate_edit_extra_specs(self, nc):
|
||||
request = self.mock_rest_request(
|
||||
body='{"updated": {"a": "1", "b": "2"}, "removed": ["c", "d"]}'
|
||||
)
|
||||
|
||||
response = nova.AggregateExtraSpecs().patch(request, '1')
|
||||
self.assertStatusCode(response, 204)
|
||||
self.assertEqual(response.content, '')
|
||||
nc.aggregate_set_metadata.assert_called_once_with(
|
||||
request, '1', {'a': '1', 'b': '2', 'c': None, 'd': None}
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user