Add user tab in project details view.

Add an extra tab "users" in the project details view which
displays the users which have a role on the project (the
members of the project).

The users are displayed in a table which is an extension
(inheritance) of the user table used in the Users panel.

An extra column is added to this table, displaying the
roles of each user on project

Only users which have directly a role on the project
are displayed, the users which have a role through a
group are not displayed.

Change-Id: I88b40fcda300ee4640155347d479a972abb2df02
Partial-Bug: #1785263
This commit is contained in:
David Gutman 2018-08-03 17:01:45 +02:00
parent c42aacb27b
commit 5cb96dd6d1
4 changed files with 212 additions and 1 deletions

View File

@ -13,10 +13,14 @@
from django.conf import settings from django.conf import settings
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import tabs from horizon import tabs
from openstack_dashboard import api from openstack_dashboard import api
from openstack_dashboard.dashboards.identity.projects.users \
import tables as users_tables
class OverviewTab(tabs.Tab): class OverviewTab(tabs.Tab):
"""Overview of the project. """ """Overview of the project. """
@ -37,6 +41,85 @@ class OverviewTab(tabs.Tab):
return context return context
class UsersTab(tabs.TableTab):
"""Display users member of the project. (directly or through a group)."""
table_classes = (users_tables.UsersTable,)
name = _("Users")
slug = "users"
template_name = "horizon/common/_detail_table.html"
preload = False
def _update_user_roles_names_from_roles_id(self, user, users_roles,
roles_list):
"""Add roles names to user.roles, based on users_roles.
:param user: user to update
:param users_roles: list of roles ID
:param roles_list: list of roles obtained with keystone
"""
user_roles_names = [role.name for role in roles_list
if role.id in users_roles]
current_user_roles_names = set(getattr(user, "roles", []))
user.roles = list(current_user_roles_names.union(user_roles_names))
def _get_users_from_project(self, project_id, roles, project_users):
"""Update with users which have role on project NOT through a group.
:param project_id: ID of the project
:param roles: list of roles from keystone
:param project_users: list to be updated with the users found
"""
# For keystone.user_list project_id is not passed as argument because
# it is ignored when using admin credentials
# Get all users (to be able to find user name)
users = api.keystone.user_list(self.request)
users = {user.id: user for user in users}
# Get project_users_roles ({user_id: [role_id_1, role_id_2]})
project_users_roles = api.keystone.get_project_users_roles(
self.request,
project=project_id)
for user_id in project_users_roles:
if user_id not in project_users:
# Add user to the project_users
project_users[user_id] = users[user_id]
project_users[user_id].roles = []
project_users[user_id].roles_from_groups = []
# Update the project_user role in order to get:
# project_users[user_id].roles = [role_name1, role_name2]
self._update_user_roles_names_from_roles_id(
user=project_users[user_id],
users_roles=project_users_roles[user_id],
roles_list=roles
)
def get_userstable_data(self):
"""Get users with roles on the project."""
project_users = {}
project = self.tab_group.kwargs['project']
try:
# Get all global roles once to avoid multiple requests.
roles = api.keystone.role_list(self.request)
# Update project_users with users which have role directly on
# the project, (NOT through a group)
self._get_users_from_project(project_id=project.id,
roles=roles,
project_users=project_users)
except Exception:
exceptions.handle(self.request,
_("Unable to display the users of this project.")
)
return project_users.values()
class ProjectDetailTabs(tabs.DetailTabsGroup): class ProjectDetailTabs(tabs.DetailTabsGroup):
slug = "project_details" slug = "project_details"
tabs = (OverviewTab,) tabs = (OverviewTab, UsersTab,)

View File

@ -1350,6 +1350,101 @@ class DetailProjectViewTests(test.BaseAdminViewTests):
self.tenant.id) self.tenant.id)
self.mock_enabled_quotas.assert_called_once_with(test.IsHttpRequest()) self.mock_enabled_quotas.assert_called_once_with(test.IsHttpRequest())
def _project_user_roles(self, role_assignments):
roles = {}
for role_assignment in role_assignments:
if hasattr(role_assignment, 'user'):
roles[role_assignment.user['id']] = [
role_assignment.role["id"]]
return roles
@test.create_mocks({api.keystone: ('tenant_get',
'user_list',
'get_project_users_roles',
'role_list',),
quotas: ('enabled_quotas',)})
def test_detail_view_users_tab(self):
project = self.tenants.first()
users = self.users.filter(domain_id=project.domain_id)
role_assignments = self.role_assignments.filter(
scope={'project': {'id': project.id}})
project_users_roles = self._project_user_roles(role_assignments)
# Prepare mocks
self.mock_tenant_get.return_value = project
self.mock_enabled_quotas.return_value = ('instances',)
self.mock_role_list.return_value = self.roles.list()
self.mock_user_list.return_value = users
self.mock_get_project_users_roles.return_value = project_users_roles
# Get project details view on user tab
url = PROJECT_DETAIL_URL % [project.id]
detail_view = tabs.ProjectDetailTabs(self.request, group=project)
users_tab_link = "?%s=%s" % (
detail_view.param_name,
detail_view.get_tab("users").get_id()
)
url += users_tab_link
res = self.client.get(url)
self.assertTemplateUsed(res, "horizon/common/_detail_table.html")
# Check the content of the table
users_expected = {
'1': {'roles': ['admin'], },
'2': {'roles': ['_member_'], },
'3': {'roles': ['_member_'], },
}
users_id_observed = [user.id for user in
res.context["userstable_table"].data]
self.assertItemsEqual(users_expected.keys(), users_id_observed)
# Check the users roles
for user in res.context["userstable_table"].data:
self.assertItemsEqual(users_expected[user.id]["roles"],
user.roles)
self.mock_tenant_get.assert_called_once_with(test.IsHttpRequest(),
self.tenant.id)
self.mock_enabled_quotas.assert_called_once_with(test.IsHttpRequest())
self.mock_role_list.assert_called_once_with(test.IsHttpRequest())
self.mock_get_project_users_roles.assert_called_once_with(
test.IsHttpRequest(), project=project.id)
self.mock_user_list.assert_called_once_with(test.IsHttpRequest())
@test.create_mocks({api.keystone: ("tenant_get",
"role_list",),
quotas: ('enabled_quotas',)})
def test_detail_view_users_tab_exception(self):
project = self.tenants.first()
# Prepare mocks
self.mock_tenant_get.return_value = project
self.mock_enabled_quotas.return_value = ('instances',)
self.mock_role_list.side_effect = self.exceptions.keystone
# Get project details view on user tab
url = reverse('horizon:identity:projects:detail', args=[project.id])
detail_view = tabs.ProjectDetailTabs(self.request, group=project)
users_tab_link = "?%s=%s" % (
detail_view.param_name,
detail_view.get_tab("users").get_id()
)
url += users_tab_link
res = self.client.get(url)
# Check the projects table is empty
self.assertFalse(res.context["userstable_table"].data)
# Check one error message is displayed
self.assertMessageCount(res, error=1)
self.mock_tenant_get.assert_called_once_with(test.IsHttpRequest(),
self.tenant.id)
self.mock_enabled_quotas.assert_called_once_with(test.IsHttpRequest())
self.mock_role_list.assert_called_once_with(test.IsHttpRequest())
@tag('selenium') @tag('selenium')
class SeleniumTests(test.SeleniumAdminTestCase, test.TestCase): class SeleniumTests(test.SeleniumAdminTestCase, test.TestCase):

View File

@ -0,0 +1,33 @@
# 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 _
from horizon import forms
from horizon import tables
from openstack_dashboard.dashboards.identity.users \
import tables as users_tables
class UsersTable(users_tables.UsersTable):
"""Display Users of the project with roles."""
roles = tables.Column(
lambda obj: ", ".join(getattr(obj, 'roles', [])),
verbose_name=_('Roles'),
form_field=forms.CharField(
widget=forms.Textarea(attrs={'rows': 4}),
required=False))
class Meta(object):
name = "userstable"
verbose_name = _("Users")