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 %}
+
+
+
+
+
{% 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 %}
+
+
+
+
+
{% 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.",