Group CRUD and Management in Admin Dashboard.

Add support for CRUD on Group for admin users. It also includes
the user management capability for the group.

Keystone revokes the token when a user is added or removed from the
group. If the logon user is added/removed from the group, the user
will be redirected to the login page.

This feature is only exposed if the user explicitly set horizon
for keystone V3.

Implements blueprint admin-group-crud

Change-Id: I1b5456af80bcc35e9d16ac6ba6792e82704e76fd
This commit is contained in:
Lin Hua Cheng 2013-05-23 23:09:34 -07:00
parent 65525a15f5
commit fa32d84b0e
23 changed files with 915 additions and 3 deletions

View File

@ -20,3 +20,5 @@
<div class="modal-footer">{% block modal-footer %}{% endblock %}</div>
</form>
</div>
{% block modal-js %}
{% endblock %}

View File

@ -0,0 +1,41 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block modal-body %}
<input type="hidden" id="hidden_redirect_back_to_home" name="redirect" value="{{redirect}}"/>
{% endblock %}
{% block modal-js %}
<script type="text/javascript">
<!--
/* ensure the value of the action buttons also gets submitted */
$(':button[name="action"]').click(function () {
value = $(this).attr("value");
$('<input>').attr({
type: 'hidden',
name: 'action',
value: value,
}).appendTo('form');
var redirect = $('#hidden_redirect_back_to_home').val();
$('<input>').attr({
type: 'hidden',
name: 'redirect',
value: redirect,
}).appendTo('form');
return true;
});
/* add the correct ajax class for table footer links */
$("tfoot a").addClass("ajax-modal");
/* as you navigate around the more pages, they stack on top of each other
make sure this stack gets cleaned up so there is only on*/
hidden_modals = $("#modal_wrapper .modal.hide:not(:last-child)");
hidden_modals.detach();
//-->
</script>
{% endblock %}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 %}
<a href="{% url 'horizon:admin:groups:manage_members' group.id %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}

View File

@ -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 %}
<div class="left">
<fieldset>
{% include "horizon/common/_form_fields.html" %}
</fieldset>
</div>
<div class="right">
<h3>{% trans "Description" %}:</h3>
<p>{% trans "From here you can create a new group to organize users and roles." %}</p>
</div>
{% endblock %}
{% block modal-footer %}
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Create Group" %}" />
<a href="{% url 'horizon:admin:groups:index' %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}

View File

@ -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 %}
<div class="left">
<fieldset>
{% include "horizon/common/_form_fields.html" %}
</fieldset>
</div>
<div class="right">
<h3>{% trans "Description" %}:</h3>
<p>{% trans "From here you can edit the group's details." %}</p>
</div>
{% endblock %}
{% block modal-footer %}
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Update Group" %}" />
<a href="{% url 'horizon:admin:groups:index' %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<group_id>[^/]+)/update/$',
UpdateView.as_view(), name='update'),
url(r'^(?P<group_id>[^/]+)/manage_members/$',
ManageMembersView.as_view(), name='manage_members'),
url(r'^(?P<group_id>[^/]+)/add_members/$',
NonMembersView.as_view(), name='add_members'),
)

View File

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

View File

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

View File

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

View File

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