Merge "Angular metadata update modal"

This commit is contained in:
Jenkins 2015-08-26 07:48:05 +00:00 committed by Gerrit Code Review
commit 8c1601bd45
23 changed files with 1098 additions and 11 deletions

View File

@ -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.

View File

@ -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)

View File

@ -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);
}
})();

View File

@ -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/');
}])
);
});
})();

View File

@ -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'
]);
})();

View File

@ -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();
});
});
})();

View File

@ -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);
}
}
})();

View File

@ -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);
});
});
})();

View File

@ -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();
}
}
}
}
})();

View File

@ -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();
});
});
})();

View File

@ -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;
}
}
})();

View File

@ -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'}
});
}
});
})();

View File

@ -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>

View File

@ -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'
});
})();

View File

@ -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();
});
});
})();

View File

@ -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));
}
}
})();

View File

@ -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'});
});
});
});
})();

View File

@ -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

View File

@ -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",

View File

@ -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.'));
});
}
}
}());

View File

@ -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.

View File

@ -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 = {

View File

@ -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}
)