diff --git a/horizon/static/framework/util/i18n/i18n.js b/horizon/static/framework/util/i18n/i18n.js index cadaab0c43..e3e2b74c96 100644 --- a/horizon/static/framework/util/i18n/i18n.js +++ b/horizon/static/framework/util/i18n/i18n.js @@ -18,7 +18,8 @@ angular .module('horizon.framework.util.i18n', []) - .factory('horizon.framework.util.i18n.gettext', getText); + .factory('horizon.framework.util.i18n.gettext', getText) + .factory('horizon.framework.util.i18n.ngettext', nGetText); getText.$inject = ['$window']; @@ -52,4 +53,27 @@ return gettextFunc.apply(this, arguments); }; } + + nGetText.$inject = ['$window']; + + /** + * @name horizon.framework.util.i18n.ngettext + * @description + * Provides a wrapper for translation, using the global 'ngettext' + * function if it is present. Provides a method that + * simply returns the input if the expected global 'ngettext' is + * not provided. + */ + function nGetText($window) { + // If no global function, revert to just returning given text. + var nGettextFunc = $window.ngettext || function (x) { + return x; + }; + + // Eventually, could delete the window ngettext references here, + // or provide an appropriate method. + return function () { + return nGettextFunc.apply(this, arguments); + }; + } })(); diff --git a/openstack_dashboard/dashboards/identity/static/dashboard/identity/roles/actions/actions.module.js b/openstack_dashboard/dashboards/identity/static/dashboard/identity/roles/actions/actions.module.js index ced90e6937..63cbca82fe 100644 --- a/openstack_dashboard/dashboards/identity/static/dashboard/identity/roles/actions/actions.module.js +++ b/openstack_dashboard/dashboards/identity/static/dashboard/identity/roles/actions/actions.module.js @@ -33,15 +33,38 @@ registerRoleActions.$inject = [ 'horizon.framework.conf.resource-type-registry.service', 'horizon.dashboard.identity.roles.actions.create.service', + 'horizon.dashboard.identity.roles.actions.delete.service', 'horizon.dashboard.identity.roles.resourceType' ]; function registerRoleActions( registry, createService, + deleteService, roleResourceTypeCode ) { var roleResourceType = registry.getResourceType(roleResourceTypeCode); + + roleResourceType.itemActions + .append({ + id: 'deleteAction', + service: deleteService, + template: { + text: gettext('Delete Role'), + type: 'delete' + } + }); + + roleResourceType.batchActions + .append({ + id: 'batchDeleteAction', + service: deleteService, + template: { + type: 'delete-selected', + text: gettext('Delete Roles') + } + }); + roleResourceType.globalActions .append({ id: 'createRoleAction', diff --git a/openstack_dashboard/dashboards/identity/static/dashboard/identity/roles/actions/delete.action.service.js b/openstack_dashboard/dashboards/identity/static/dashboard/identity/roles/actions/delete.action.service.js new file mode 100644 index 0000000000..6f88b5ccc3 --- /dev/null +++ b/openstack_dashboard/dashboards/identity/static/dashboard/identity/roles/actions/delete.action.service.js @@ -0,0 +1,107 @@ +/** + * Copyright 2016 99Cloud + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use self 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.dashboard.identity.roles') + .factory('horizon.dashboard.identity.roles.actions.delete.service', deleteRoleService); + + deleteRoleService.$inject = [ + 'horizon.app.core.openstack-service-api.keystone', + 'horizon.app.core.openstack-service-api.policy', + 'horizon.framework.util.actions.action-result.service', + 'horizon.framework.util.i18n.ngettext', + 'horizon.framework.widgets.modal.deleteModalService', + 'horizon.dashboard.identity.roles.resourceType' + ]; + + /* + * @ngdoc factory + * @name horizon.dashboard.identity.roles.actions.delete.service + * + * @Description + * Brings up the delete roles confirmation modal dialog. + + * On submit, delete given roles. + * On cancel, do nothing. + */ + function deleteRoleService( + keystone, + policy, + actionResultService, + ngettext, + deleteModal, + roleResourceType + ) { + return { + allowed: allowed, + perform: perform, + deleteResult: deleteResult // exposed just for testing + }; + + ////////////// + + function allowed() { + return policy.ifAllowed({rules: [[ 'identity', 'identity:delete_role' ]]}); + } + + function perform(items, scope) { + var roles = angular.isArray(items) ? items : [items]; + var context = { + labels: labelize(roles.length), + deleteEntity: function deleteRole(role) { + return keystone.deleteRole(role); + } + }; + return deleteModal.open(scope, roles, context).then(deleteResult); + } + + function deleteResult(deleteModalResult) { + // To make the result of this action generically useful, reformat the return + // from the deleteModal into a standard form + var actionResult = actionResultService.getActionResult(); + deleteModalResult.pass.forEach(function markDeleted(item) { + actionResult.deleted(roleResourceType, item.context.id); + }); + deleteModalResult.fail.forEach(function markFailed(item) { + actionResult.failed(roleResourceType, item.context.id); + }); + return actionResult.result; + } + + function labelize(count) { + return { + title: ngettext( + 'Confirm Delete Role', + 'Confirm Delete Roles', count), + message: ngettext( + 'You have selected "%s". Deleted role is not recoverable.', + 'You have selected "%s". Deleted roles are not recoverable.', count), + submit: ngettext( + 'Delete Role', + 'Delete Roles', count), + success: ngettext( + 'Deleted Role: %s.', + 'Deleted Roles: %s.', count), + error: ngettext( + 'Unable to delete Role: %s.', + 'Unable to delete Roles: %s.', count) + }; + } + } +})(); diff --git a/openstack_dashboard/dashboards/identity/static/dashboard/identity/roles/actions/delete.action.service.spec.js b/openstack_dashboard/dashboards/identity/static/dashboard/identity/roles/actions/delete.action.service.spec.js new file mode 100644 index 0000000000..e6aef1f1a9 --- /dev/null +++ b/openstack_dashboard/dashboards/identity/static/dashboard/identity/roles/actions/delete.action.service.spec.js @@ -0,0 +1,83 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use self 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.dashboard.identity.roles.actions.delete.service', function() { + beforeEach(module('horizon.app.core')); + beforeEach(module('horizon.dashboard.identity.roles')); + beforeEach(module('horizon.framework')); + + var deleteModalService, service, keystoneAPI, policyAPI; + + beforeEach(inject(function($injector) { + service = $injector.get('horizon.dashboard.identity.roles.actions.delete.service'); + keystoneAPI = $injector.get('horizon.app.core.openstack-service-api.keystone'); + deleteModalService = $injector.get('horizon.framework.widgets.modal.deleteModalService'); + policyAPI = $injector.get('horizon.app.core.openstack-service-api.policy'); + })); + + describe('perform method', function() { + beforeEach(function () { + // just need for this to return something that looks like a promise but does nothing + spyOn(deleteModalService, 'open').and.returnValue({then: angular.noop}); + }); + + it('should open the modal with correct label for single entity', function test() { + service.perform({name: 'spam'}); + var labels = deleteModalService.open.calls.argsFor(0)[2].labels; + expect(deleteModalService.open).toHaveBeenCalled(); + angular.forEach(labels, function eachLabel(label) { + expect(label.toLowerCase()).toContain('role'); + }); + }); + + it('should open the modal with correct label for multiple entities', function test() { + service.perform([{name: 'one'}, {name: 'two'}]); + var labels = deleteModalService.open.calls.argsFor(0)[2].labels; + expect(deleteModalService.open).toHaveBeenCalled(); + angular.forEach(labels, function eachLabel(label) { + expect(label.toLowerCase()).toContain('roles'); + }); + }); + + it('should open the delete modal with correct entities', function test() { + service.perform([{name: 'one'}, {name: 'two'}]); + var entities = deleteModalService.open.calls.argsFor(0)[1]; + expect(deleteModalService.open).toHaveBeenCalled(); + expect(entities.length).toEqual(2); + }); + + it('should pass in a function that deletes an role', function test() { + spyOn(keystoneAPI, 'deleteRole').and.callFake(angular.noop); + service.perform({id: 1, name: 'one'}); + var contextArg = deleteModalService.open.calls.argsFor(0)[2]; + var deleteFunction = contextArg.deleteEntity; + deleteFunction(1); + expect(keystoneAPI.deleteRole).toHaveBeenCalledWith(1); + }); + }); + + describe('allow method', function() { + it('should use default policy if batch action', function test() { + spyOn(policyAPI, 'ifAllowed'); + service.allowed(); + expect(policyAPI.ifAllowed).toHaveBeenCalled(); + }); + }); // end of allowed + + }); // end of delete + +})();