diff --git a/horizon/templates/horizon/common/_modal_form.html b/horizon/templates/horizon/common/_modal_form.html index 9f9b117995..8b56631d71 100644 --- a/horizon/templates/horizon/common/_modal_form.html +++ b/horizon/templates/horizon/common/_modal_form.html @@ -20,3 +20,5 @@ +{% block modal-js %} +{% endblock %} \ No newline at end of file diff --git a/horizon/templates/horizon/common/_modal_form_add_members.html b/horizon/templates/horizon/common/_modal_form_add_members.html new file mode 100644 index 0000000000..fa7bf6d0fa --- /dev/null +++ b/horizon/templates/horizon/common/_modal_form_add_members.html @@ -0,0 +1,41 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal-body %} + +{% endblock %} + +{% block modal-js %} + +{% endblock %} \ No newline at end of file diff --git a/openstack_dashboard/api/keystone.py b/openstack_dashboard/api/keystone.py index 8b704e52d2..1dbf03d59b 100644 --- a/openstack_dashboard/api/keystone.py +++ b/openstack_dashboard/api/keystone.py @@ -360,6 +360,45 @@ def user_update_tenant(request, user, project, admin=True): return manager.update(user, project=project) +def group_create(request, domain_id, name, description=None): + manager = keystoneclient(request, admin=True).groups + return manager.create(domain=domain_id, + name=name, + description=description) + + +def group_get(request, group_id, admin=True): + manager = keystoneclient(request, admin=admin).groups + return manager.get(group_id) + + +def group_delete(request, group_id): + manager = keystoneclient(request, admin=True).groups + return manager.delete(group_id) + + +def group_list(request): + manager = keystoneclient(request, admin=True).groups + return manager.list() + + +def group_update(request, group_id, name=None, description=None): + manager = keystoneclient(request, admin=True).groups + return manager.update(group=group_id, + name=name, + description=description) + + +def add_group_user(request, group_id, user_id): + manager = keystoneclient(request, admin=True).users + return manager.add_to_group(group=group_id, user=user_id) + + +def remove_group_user(request, group_id, user_id): + manager = keystoneclient(request, admin=True).users + return manager.remove_from_group(group=group_id, user=user_id) + + def role_create(request, name): manager = keystoneclient(request, admin=True).roles return manager.create(name) @@ -472,6 +511,11 @@ def keystone_can_edit_project(): return backend_settings.get('can_edit_project', True) +def keystone_can_edit_group(): + backend_settings = getattr(settings, "OPENSTACK_KEYSTONE_BACKEND", {}) + return backend_settings.get('can_edit_group', True) + + def keystone_can_edit_role(): backend_settings = getattr(settings, "OPENSTACK_KEYSTONE_BACKEND", {}) return backend_settings.get('can_edit_role', True) diff --git a/openstack_dashboard/dashboards/admin/dashboard.py b/openstack_dashboard/dashboards/admin/dashboard.py index e3b2b3de02..d63da76645 100644 --- a/openstack_dashboard/dashboards/admin/dashboard.py +++ b/openstack_dashboard/dashboards/admin/dashboard.py @@ -23,8 +23,8 @@ class SystemPanels(horizon.PanelGroup): slug = "admin" name = _("System Panel") panels = ('overview', 'instances', 'volumes', 'flavors', - 'images', 'domains', 'projects', 'users', 'roles', - 'networks', 'routers', 'info') + 'images', 'domains', 'projects', 'users', 'groups', + 'roles', 'networks', 'routers', 'info') class Admin(horizon.Dashboard): diff --git a/openstack_dashboard/dashboards/admin/groups/__init__.py b/openstack_dashboard/dashboards/admin/groups/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openstack_dashboard/dashboards/admin/groups/constants.py b/openstack_dashboard/dashboards/admin/groups/constants.py new file mode 100644 index 0000000000..db4adc507c --- /dev/null +++ b/openstack_dashboard/dashboards/admin/groups/constants.py @@ -0,0 +1,11 @@ +GROUPS_INDEX_URL = 'horizon:admin:groups:index' +GROUPS_INDEX_VIEW_TEMPLATE = 'admin/groups/index.html' +GROUPS_CREATE_URL = 'horizon:admin:groups:create' +GROUPS_CREATE_VIEW_TEMPLATE = 'admin/groups/create.html' +GROUPS_UPDATE_URL = 'horizon:admin:groups:update' +GROUPS_UPDATE_VIEW_TEMPLATE = 'admin/groups/update.html' +GROUPS_MANAGE_URL = 'horizon:admin:groups:manage_members' +GROUPS_MANAGE_VIEW_TEMPLATE = 'admin/groups/manage.html' +GROUPS_ADD_MEMBER_URL = 'horizon:admin:groups:add_members' +GROUPS_ADD_MEMBER_VIEW_TEMPLATE = 'admin/groups/add_non_member.html' +GROUPS_ADD_MEMBER_AJAX_VIEW_TEMPLATE = 'admin/groups/_add_non_member.html' diff --git a/openstack_dashboard/dashboards/admin/groups/forms.py b/openstack_dashboard/dashboards/admin/groups/forms.py new file mode 100644 index 0000000000..edb7d578d8 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/groups/forms.py @@ -0,0 +1,77 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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. + +import logging + +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import forms +from horizon import messages + +from openstack_dashboard import api + + +LOG = logging.getLogger(__name__) + + +class CreateGroupForm(forms.SelfHandlingForm): + name = forms.CharField(label=_("Name"), + required=True) + description = forms.CharField(widget=forms.widgets.Textarea(), + label=_("Description"), + required=False) + + def handle(self, request, data): + try: + LOG.info('Creating group with name "%s"' % data['name']) + # TODO: Set the domain_id with the value from the Domain scope. + new_group = api.keystone.group_create( + request, + domain_id=None, + name=data['name'], + description=data['description']) + messages.success(request, + _('Group "%s" was successfully created.') + % data['name']) + except: + exceptions.handle(request, _('Unable to create group.')) + return False + return True + + +class UpdateGroupForm(forms.SelfHandlingForm): + group_id = forms.CharField(widget=forms.HiddenInput()) + name = forms.CharField(label=_("Name"), + required=True) + description = forms.CharField(widget=forms.widgets.Textarea(), + label=_("Description"), + required=False) + + def handle(self, request, data): + group_id = data.pop('group_id') + + try: + api.keystone.group_update(request, + group_id=group_id, + name=data['name'], + description=data['description']) + messages.success(request, + _('Group has been updated successfully.')) + except: + exceptions.handle(request, _('Unable to update the group.')) + return False + return True diff --git a/openstack_dashboard/dashboards/admin/groups/panel.py b/openstack_dashboard/dashboards/admin/groups/panel.py new file mode 100644 index 0000000000..92452a6de1 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/groups/panel.py @@ -0,0 +1,31 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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. + +from django.utils.translation import ugettext_lazy as _ + +import horizon + +from openstack_dashboard.api.keystone import VERSIONS as IDENTITY_VERSIONS +from openstack_dashboard.dashboards.admin import dashboard + + +class Groups(horizon.Panel): + name = _("Groups") + slug = 'groups' + + +if IDENTITY_VERSIONS.active >= 3: + dashboard.Admin.register(Groups) diff --git a/openstack_dashboard/dashboards/admin/groups/tables.py b/openstack_dashboard/dashboards/admin/groups/tables.py new file mode 100644 index 0000000000..87b8e2480a --- /dev/null +++ b/openstack_dashboard/dashboards/admin/groups/tables.py @@ -0,0 +1,206 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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. + +import logging + +from django.core.urlresolvers import reverse +from django.template import defaultfilters +from django.utils.translation import ugettext_lazy as _ + +from horizon import tables + +from openstack_dashboard import api + +from .constants import GROUPS_CREATE_URL, GROUPS_UPDATE_URL, \ + GROUPS_MANAGE_URL, GROUPS_ADD_MEMBER_URL + + +LOG = logging.getLogger(__name__) +LOGOUT_URL = 'logout' +STATUS_CHOICES = ( + ("true", True), + ("false", False) +) + + +class CreateGroupLink(tables.LinkAction): + name = "create" + verbose_name = _("Create Group") + url = GROUPS_CREATE_URL + classes = ("ajax-modal", "btn-create") + + def allowed(self, request, group): + return api.keystone.keystone_can_edit_group() + + +class EditGroupLink(tables.LinkAction): + name = "edit" + verbose_name = _("Edit Group") + url = GROUPS_UPDATE_URL + classes = ("ajax-modal", "btn-edit") + + def allowed(self, request, group): + return api.keystone.keystone_can_edit_group() + + +class DeleteGroupsAction(tables.DeleteAction): + name = "delete" + data_type_singular = _("Group") + data_type_plural = _("Groups") + + def allowed(self, request, datum): + return api.keystone.keystone_can_edit_group() + + def delete(self, request, obj_id): + LOG.info('Deleting group "%s".' % obj_id) + api.keystone.group_delete(request, obj_id) + + +class ManageUsersLink(tables.LinkAction): + name = "users" + verbose_name = _("Modify Users") + url = GROUPS_MANAGE_URL + classes = ("btn-edit") + + def allowed(self, request, datum): + return api.keystone.keystone_can_edit_group() + + +class GroupFilterAction(tables.FilterAction): + def filter(self, table, groups, filter_string): + """ Naive case-insensitive search """ + q = filter_string.lower() + + def comp(group): + if q in group.name.lower(): + return True + return False + + return filter(comp, groups) + + +class GroupsTable(tables.DataTable): + name = tables.Column('name', verbose_name=_('Name')) + description = tables.Column(lambda obj: getattr(obj, 'description', None), + verbose_name=_('Description')) + id = tables.Column('id', verbose_name=_('Group ID')) + + class Meta: + name = "groups" + verbose_name = _("Groups") + row_actions = (ManageUsersLink, EditGroupLink, DeleteGroupsAction) + table_actions = (GroupFilterAction, CreateGroupLink, + DeleteGroupsAction) + + +class UserFilterAction(tables.FilterAction): + def filter(self, table, users, filter_string): + """ Naive case-insensitive search """ + q = filter_string.lower() + return [user for user in users + if q in user.name.lower() + or q in user.email.lower()] + + +class RemoveMembers(tables.DeleteAction): + name = "removeGroupMember" + action_present = _("Remove") + action_past = _("Removed") + data_type_singular = _("User") + data_type_plural = _("Users") + + def allowed(self, request, user=None): + return api.keystone.keystone_can_edit_group() + + def action(self, request, obj_id): + user_obj = self.table.get_object_by_id(obj_id) + group_id = self.table.kwargs['group_id'] + LOG.info('Removing user %s from group %s.' % (user_obj.id, + group_id)) + api.keystone.remove_group_user(request, + group_id=group_id, + user_id=user_obj.id) + # TODO: Fix the bug when removing current user + # Keystone revokes the token of the user removed from the group. + # If the logon user was removed, redirect the user to logout. + + +class AddMembersLink(tables.LinkAction): + name = "add_user_link" + verbose_name = _("Add...") + classes = ("ajax-modal", "btn-create") + url = GROUPS_ADD_MEMBER_URL + + def allowed(self, request, user=None): + return api.keystone.keystone_can_edit_group() + + def get_link_url(self, datum=None): + return reverse(self.url, kwargs=self.table.kwargs) + + +class UsersTable(tables.DataTable): + name = tables.Column('name', verbose_name=_('User Name')) + email = tables.Column('email', verbose_name=_('Email'), + filters=[defaultfilters.urlize]) + id = tables.Column('id', verbose_name=_('User ID')) + enabled = tables.Column('enabled', verbose_name=_('Enabled'), + status=True, + status_choices=STATUS_CHOICES, + empty_value="False") + + +class GroupMembersTable(UsersTable): + class Meta: + name = "group_members" + verbose_name = _("Group Members") + table_actions = (UserFilterAction, AddMembersLink, RemoveMembers) + + +class AddMembers(tables.BatchAction): + name = "addMember" + action_present = _("Add") + action_past = _("Added") + data_type_singular = _("User") + data_type_plural = _("Users") + classes = ("btn-create", ) + requires_input = True + success_url = GROUPS_MANAGE_URL + + def allowed(self, request, user=None): + return api.keystone.keystone_can_edit_group() + + def action(self, request, obj_id): + user_obj = self.table.get_object_by_id(obj_id) + group_id = self.table.kwargs['group_id'] + LOG.info('Adding user %s to group %s.' % (user_obj.id, + group_id)) + api.keystone.add_group_user(request, + group_id=group_id, + user_id=user_obj.id) + # TODO: Fix the bug when adding current user + # Keystone revokes the token of the user added to the group. + # If the logon user was added, redirect the user to logout. + + def get_success_url(self, request=None): + group_id = self.table.kwargs.get('group_id', None) + return reverse(self.success_url, args=[group_id]) + + +class GroupNonMembersTable(UsersTable): + class Meta: + name = "group_non_members" + verbose_name = _("Non-Members") + table_actions = (UserFilterAction, AddMembers) diff --git a/openstack_dashboard/dashboards/admin/groups/templates/groups/_add_non_member.html b/openstack_dashboard/dashboards/admin/groups/templates/groups/_add_non_member.html new file mode 100644 index 0000000000..360045ad51 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/groups/templates/groups/_add_non_member.html @@ -0,0 +1,9 @@ +{% extends "horizon/common/_modal_form_add_members.html" %} +{% load i18n %} +{% load url from future %} + +{% block modal-header %}{% trans "Add Group Assignment" %}{% endblock %} + +{% block modal-footer %} + {% trans "Cancel" %} +{% endblock %} diff --git a/openstack_dashboard/dashboards/admin/groups/templates/groups/_create.html b/openstack_dashboard/dashboards/admin/groups/templates/groups/_create.html new file mode 100644 index 0000000000..5b4c3df6ec --- /dev/null +++ b/openstack_dashboard/dashboards/admin/groups/templates/groups/_create.html @@ -0,0 +1,25 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} +{% load url from future %} + +{% block form_id %}create_group_form{% endblock %} +{% block form_action %}{% url 'horizon:admin:groups:create' %}{% endblock %} + +{% block modal-header %}{% trans "Create Group" %}{% endblock %} + +{% block modal-body %} +
+
+ {% include "horizon/common/_form_fields.html" %} +
+
+
+

{% trans "Description" %}:

+

{% trans "From here you can create a new group to organize users and roles." %}

+
+{% endblock %} + +{% block modal-footer %} + + {% trans "Cancel" %} +{% endblock %} diff --git a/openstack_dashboard/dashboards/admin/groups/templates/groups/_update.html b/openstack_dashboard/dashboards/admin/groups/templates/groups/_update.html new file mode 100644 index 0000000000..6ad94e3255 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/groups/templates/groups/_update.html @@ -0,0 +1,25 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} +{% load url from future %} + +{% block form_id %}update_group_form{% endblock %} +{% block form_action %}{% url 'horizon:admin:groups:update' group.id %}{% endblock %} + +{% block modal-header %}{% trans "Update Group" %}{% endblock %} + +{% block modal-body %} +
+
+ {% include "horizon/common/_form_fields.html" %} +
+
+
+

{% trans "Description" %}:

+

{% trans "From here you can edit the group's details." %}

+
+{% endblock %} + +{% block modal-footer %} + + {% trans "Cancel" %} +{% endblock %} diff --git a/openstack_dashboard/dashboards/admin/groups/templates/groups/add_non_member.html b/openstack_dashboard/dashboards/admin/groups/templates/groups/add_non_member.html new file mode 100644 index 0000000000..fe380dbee8 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/groups/templates/groups/add_non_member.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans 'Add User to Group' %}{% endblock %} + +{% block main %} + {% include 'admin/groups/_add_non_member.html' %} +{% endblock %} diff --git a/openstack_dashboard/dashboards/admin/groups/templates/groups/create.html b/openstack_dashboard/dashboards/admin/groups/templates/groups/create.html new file mode 100644 index 0000000000..126c8aa6c2 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/groups/templates/groups/create.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Create Group" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Create Group") %} +{% endblock page_header %} + +{% block main %} + {% include 'admin/groups/_create.html' %} +{% endblock %} diff --git a/openstack_dashboard/dashboards/admin/groups/templates/groups/index.html b/openstack_dashboard/dashboards/admin/groups/templates/groups/index.html new file mode 100644 index 0000000000..395fe3e45c --- /dev/null +++ b/openstack_dashboard/dashboards/admin/groups/templates/groups/index.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Groups" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Groups") %} +{% endblock page_header %} + +{% block main %} + {{ table.render }} +{% endblock %} diff --git a/openstack_dashboard/dashboards/admin/groups/templates/groups/manage.html b/openstack_dashboard/dashboards/admin/groups/templates/groups/manage.html new file mode 100644 index 0000000000..5e8b778ee7 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/groups/templates/groups/manage.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans 'Group Management' %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Group Management: ")|add:group.name %} +{% endblock page_header %} + +{% block main %} + {{ group_members_table.render }} +{% endblock %} diff --git a/openstack_dashboard/dashboards/admin/groups/templates/groups/update.html b/openstack_dashboard/dashboards/admin/groups/templates/groups/update.html new file mode 100644 index 0000000000..abb88a4345 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/groups/templates/groups/update.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Update Group" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Update Group") %} +{% endblock page_header %} + +{% block main %} + {% include 'admin/groups/_update.html' %} +{% endblock %} diff --git a/openstack_dashboard/dashboards/admin/groups/tests.py b/openstack_dashboard/dashboards/admin/groups/tests.py new file mode 100644 index 0000000000..e259c39228 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/groups/tests.py @@ -0,0 +1,196 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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. + +from django import http +from django.core.urlresolvers import reverse + +from mox import IgnoreArg, IsA + +from openstack_dashboard import api +from openstack_dashboard.test import helpers as test + +from .constants import GROUPS_INDEX_VIEW_TEMPLATE, \ + GROUPS_MANAGE_VIEW_TEMPLATE, \ + GROUPS_INDEX_URL as index_url, \ + GROUPS_CREATE_URL as create_url, \ + GROUPS_UPDATE_URL as update_url, \ + GROUPS_MANAGE_URL as manage_url, \ + GROUPS_ADD_MEMBER_URL as add_member_url + + +GROUPS_INDEX_URL = reverse(index_url) +GROUP_CREATE_URL = reverse(create_url) +GROUP_UPDATE_URL = reverse(update_url, args=[1]) +GROUP_MANAGE_URL = reverse(manage_url, args=[1]) +GROUP_ADD_MEMBER_URL = reverse(add_member_url, args=[1]) + + +class GroupsViewTests(test.BaseAdminViewTests): + @test.create_stubs({api.keystone: ('group_list',)}) + def test_index(self): + api.keystone.group_list(IgnoreArg()).AndReturn(self.groups.list()) + + self.mox.ReplayAll() + + res = self.client.get(GROUPS_INDEX_URL) + + self.assertTemplateUsed(res, GROUPS_INDEX_VIEW_TEMPLATE) + self.assertItemsEqual(res.context['table'].data, self.groups.list()) + + self.assertContains(res, 'Create Group') + self.assertContains(res, 'Edit') + self.assertContains(res, 'Delete Group') + + @test.create_stubs({api.keystone: ('group_list', + 'keystone_can_edit_group')}) + def test_index_with_keystone_can_edit_group_false(self): + api.keystone.group_list(IgnoreArg()).AndReturn(self.groups.list()) + api.keystone.keystone_can_edit_group() \ + .MultipleTimes().AndReturn(False) + + self.mox.ReplayAll() + + res = self.client.get(GROUPS_INDEX_URL) + + self.assertTemplateUsed(res, GROUPS_INDEX_VIEW_TEMPLATE) + self.assertItemsEqual(res.context['table'].data, self.groups.list()) + + self.assertNotContains(res, 'Create Group') + self.assertNotContains(res, 'Edit') + self.assertNotContains(res, 'Delete Group') + + @test.create_stubs({api.keystone: ('group_create', )}) + def test_create(self): + group = self.groups.get(id="1") + + api.keystone.group_create(IsA(http.HttpRequest), + description=group.description, + domain_id=None, + name=group.name).AndReturn(group) + + self.mox.ReplayAll() + + formData = {'method': 'CreateGroupForm', + 'name': group.name, + 'description': group.description} + res = self.client.post(GROUP_CREATE_URL, formData) + + self.assertNoFormErrors(res) + self.assertMessageCount(success=1) + + @test.create_stubs({api.keystone: ('group_get', + 'group_update')}) + def test_update(self): + group = self.groups.get(id="1") + test_description = 'updated description' + + api.keystone.group_get(IsA(http.HttpRequest), '1').AndReturn(group) + api.keystone.group_update(IsA(http.HttpRequest), + description=test_description, + group_id=group.id, + name=group.name).AndReturn(None) + + self.mox.ReplayAll() + + formData = {'method': 'UpdateGroupForm', + 'group_id': group.id, + 'name': group.name, + 'description': test_description} + + res = self.client.post(GROUP_UPDATE_URL, formData) + + self.assertNoFormErrors(res) + + @test.create_stubs({api.keystone: ('group_list', + 'group_delete')}) + def test_delete_group(self): + group = self.groups.get(id="2") + + api.keystone.group_list(IgnoreArg()).AndReturn(self.groups.list()) + api.keystone.group_delete(IgnoreArg(), group.id) + + self.mox.ReplayAll() + + formData = {'action': 'groups__delete__%s' % group.id} + res = self.client.post(GROUPS_INDEX_URL, formData) + + self.assertRedirectsNoFollow(res, GROUPS_INDEX_URL) + + @test.create_stubs({api.keystone: ('group_get', + 'user_list',)}) + def test_manage(self): + group = self.groups.get(id="1") + group_members = self.users.list() + + api.keystone.group_get(IsA(http.HttpRequest), group.id).\ + AndReturn(group) + api.keystone.user_list(IgnoreArg(), + group=group.id).\ + AndReturn(group_members) + self.mox.ReplayAll() + + res = self.client.get(GROUP_MANAGE_URL) + + self.assertTemplateUsed(res, GROUPS_MANAGE_VIEW_TEMPLATE) + self.assertItemsEqual(res.context['table'].data, group_members) + + @test.create_stubs({api.keystone: ('user_list', + 'remove_group_user')}) + def test_remove_user(self): + group = self.groups.get(id="1") + user = self.users.get(id="2") + + api.keystone.user_list(IgnoreArg(), + group=group.id).\ + AndReturn(self.users.list()) + api.keystone.remove_group_user(IgnoreArg(), + group_id=group.id, + user_id=user.id) + self.mox.ReplayAll() + + formData = {'action': 'group_members__removeGroupMember__%s' % user.id} + res = self.client.post(GROUP_MANAGE_URL, formData) + + self.assertRedirectsNoFollow(res, GROUP_MANAGE_URL) + self.assertMessageCount(success=1) + + @test.create_stubs({api.keystone: ('group_get', + 'user_list', + 'add_group_user')}) + def test_add_user(self): + group = self.groups.get(id="1") + user = self.users.get(id="2") + + api.keystone.group_get(IsA(http.HttpRequest), group.id).\ + AndReturn(group) + api.keystone.user_list(IgnoreArg(), + domain=group.domain_id).\ + AndReturn(self.users.list()) + api.keystone.user_list(IgnoreArg(), + group=group.id).\ + AndReturn(self.users.list()[2:]) + + api.keystone.add_group_user(IgnoreArg(), + group_id=group.id, + user_id=user.id) + + self.mox.ReplayAll() + + formData = {'action': 'group_non_members__addMember__%s' % user.id} + res = self.client.post(GROUP_ADD_MEMBER_URL, formData) + + self.assertRedirectsNoFollow(res, GROUP_MANAGE_URL) + self.assertMessageCount(success=1) diff --git a/openstack_dashboard/dashboards/admin/groups/urls.py b/openstack_dashboard/dashboards/admin/groups/urls.py new file mode 100644 index 0000000000..dc1db02f96 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/groups/urls.py @@ -0,0 +1,32 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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. + +from django.conf.urls.defaults import patterns, url + +from .views import IndexView, CreateView, UpdateView, \ + ManageMembersView, NonMembersView + + +urlpatterns = patterns('', + url(r'^$', IndexView.as_view(), name='index'), + url(r'^create$', CreateView.as_view(), name='create'), + url(r'^(?P[^/]+)/update/$', + UpdateView.as_view(), name='update'), + url(r'^(?P[^/]+)/manage_members/$', + ManageMembersView.as_view(), name='manage_members'), + url(r'^(?P[^/]+)/add_members/$', + NonMembersView.as_view(), name='add_members'), +) diff --git a/openstack_dashboard/dashboards/admin/groups/views.py b/openstack_dashboard/dashboards/admin/groups/views.py new file mode 100644 index 0000000000..86b8084e16 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/groups/views.py @@ -0,0 +1,146 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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. + +from django.core.urlresolvers import reverse, reverse_lazy +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import forms +from horizon import tables + +from openstack_dashboard import api +from .constants import GROUPS_INDEX_URL, GROUPS_INDEX_VIEW_TEMPLATE, \ + GROUPS_CREATE_VIEW_TEMPLATE, GROUPS_UPDATE_VIEW_TEMPLATE, \ + GROUPS_MANAGE_VIEW_TEMPLATE, GROUPS_ADD_MEMBER_VIEW_TEMPLATE, \ + GROUPS_ADD_MEMBER_AJAX_VIEW_TEMPLATE +from .forms import CreateGroupForm, UpdateGroupForm +from .tables import GroupsTable, GroupMembersTable, \ + GroupNonMembersTable + + +class IndexView(tables.DataTableView): + table_class = GroupsTable + template_name = GROUPS_INDEX_VIEW_TEMPLATE + + def get_data(self): + groups = [] + try: + groups = api.keystone.group_list(self.request) + except: + exceptions.handle(self.request, + _('Unable to retrieve group list.')) + return groups + + +class CreateView(forms.ModalFormView): + form_class = CreateGroupForm + template_name = GROUPS_CREATE_VIEW_TEMPLATE + success_url = reverse_lazy(GROUPS_INDEX_URL) + + +class UpdateView(forms.ModalFormView): + form_class = UpdateGroupForm + template_name = GROUPS_UPDATE_VIEW_TEMPLATE + success_url = reverse_lazy(GROUPS_INDEX_URL) + + def get_object(self): + if not hasattr(self, "_object"): + try: + self._object = api.keystone.group_get(self.request, + self.kwargs['group_id']) + except: + redirect = reverse(GROUPS_INDEX_URL) + exceptions.handle(self.request, + _('Unable to update group.'), + redirect=redirect) + return self._object + + def get_context_data(self, **kwargs): + context = super(UpdateView, self).get_context_data(**kwargs) + context['group'] = self.get_object() + return context + + def get_initial(self): + group = self.get_object() + return {'group_id': group.id, + 'name': group.name, + 'description': group.description} + + +class GroupManageMixin(object): + def _get_group(self): + if not hasattr(self, "_group"): + group_id = self.kwargs['group_id'] + self._group = api.keystone.group_get(self.request, group_id) + return self._group + + def _get_group_members(self): + if not hasattr(self, "_group_members"): + group_id = self.kwargs['group_id'] + self._group_members = api.keystone.user_list(self.request, + group=group_id) + return self._group_members + + def _get_group_non_members(self): + if not hasattr(self, "_group_non_members"): + domain_id = self._get_group().domain_id + all_users = api.keystone.user_list(self.request, + domain=domain_id) + group_members = self._get_group_members() + group_member_ids = [user.id for user in group_members] + self._group_non_members = filter( + lambda u: u.id not in group_member_ids, all_users) + return self._group_non_members + + +class ManageMembersView(GroupManageMixin, tables.DataTableView): + table_class = GroupMembersTable + template_name = GROUPS_MANAGE_VIEW_TEMPLATE + + def get_context_data(self, **kwargs): + context = super(ManageMembersView, self).get_context_data(**kwargs) + context['group'] = self._get_group() + return context + + def get_data(self): + group_members = [] + try: + group_members = self._get_group_members() + except: + exceptions.handle(self.request, + _('Unable to retrieve group users.')) + return group_members + + +class NonMembersView(GroupManageMixin, forms.ModalFormMixin, + tables.DataTableView): + template_name = GROUPS_ADD_MEMBER_VIEW_TEMPLATE + ajax_template_name = GROUPS_ADD_MEMBER_AJAX_VIEW_TEMPLATE + table_class = GroupNonMembersTable + + def get_context_data(self, **kwargs): + context = super(NonMembersView, self).get_context_data(**kwargs) + context['group'] = self._get_group() + return context + + def get_data(self): + group_non_members = [] + try: + group_non_members = self._get_group_non_members() + except: + exceptions.handle(self.request, + _('Unable to retrieve users.')) + return group_non_members diff --git a/openstack_dashboard/local/local_settings.py.example b/openstack_dashboard/local/local_settings.py.example index 990b3fd7ed..8ee94955d7 100644 --- a/openstack_dashboard/local/local_settings.py.example +++ b/openstack_dashboard/local/local_settings.py.example @@ -136,6 +136,7 @@ OPENSTACK_KEYSTONE_DEFAULT_ROLE = "Member" OPENSTACK_KEYSTONE_BACKEND = { 'name': 'native', 'can_edit_user': True, + 'can_edit_group': True, 'can_edit_project': True, 'can_edit_domain': True, 'can_edit_role': True diff --git a/openstack_dashboard/test/settings.py b/openstack_dashboard/test/settings.py index 78afb1f343..3ecedcddd9 100644 --- a/openstack_dashboard/test/settings.py +++ b/openstack_dashboard/test/settings.py @@ -76,6 +76,7 @@ OPENSTACK_KEYSTONE_DEFAULT_DOMAIN = 'test_domain' OPENSTACK_KEYSTONE_BACKEND = { 'name': 'native', 'can_edit_user': True, + 'can_edit_group': True, 'can_edit_project': True, 'can_edit_domain': True, 'can_edit_role': True diff --git a/openstack_dashboard/test/test_data/keystone_data.py b/openstack_dashboard/test/test_data/keystone_data.py index ad343bc020..d5e114bbfe 100644 --- a/openstack_dashboard/test/test_data/keystone_data.py +++ b/openstack_dashboard/test/test_data/keystone_data.py @@ -18,7 +18,8 @@ from django.conf import settings from django.utils import datetime_safe from keystoneclient.v2_0 import users, tenants, tokens, roles, ec2 -from keystoneclient.v3 import domains +from keystoneclient.v3 import domains, groups + from .utils import TestDataContainer @@ -99,6 +100,7 @@ def data(TEST): TEST.tokens = TestDataContainer() TEST.domains = TestDataContainer() TEST.users = TestDataContainer() + TEST.groups = TestDataContainer() TEST.tenants = TestDataContainer() TEST.roles = TestDataContainer() TEST.ec2 = TestDataContainer() @@ -154,6 +156,18 @@ def data(TEST): TEST.user = user # Your "current" user TEST.user.service_catalog = SERVICE_CATALOG + group_dict = {'id': "1", + 'name': 'group_one', + 'description': 'group one description', + 'domain_id': '1'} + group = groups.Group(groups.GroupManager(None), group_dict) + group_dict = {'id': "2", + 'name': 'group_two', + 'description': 'group two description', + 'domain_id': '1'} + group2 = groups.Group(groups.GroupManager(None), group_dict) + TEST.groups.add(group, group2) + tenant_dict = {'id': "1", 'name': 'test_tenant', 'description': "a test tenant.",