Supports extra properties in project and user
This change is to support extra properties in project and user. To show an extra property in project/user table, you can customize. The explanation of the method to add an extra property is added by this patch. In addition, when you do a create or update user, you can specify a value of extra property by setting in local_settings.py Change-Id: Ifee491f2a55d9207fe5da70136f749e1fc4bab82 Implements: blueprint support-extra-prop-for-project-and-user
This commit is contained in:
parent
0e0b01fd27
commit
ab11eef92a
@ -402,6 +402,8 @@ files to be added. There are some example files available within this folder, w
|
||||
customization as much as possible, and support for this is given preference over more
|
||||
exotic methods such as monkey patching and overrides files.
|
||||
|
||||
.. _horizon-customization-module:
|
||||
|
||||
Horizon customization module (overrides)
|
||||
========================================
|
||||
|
||||
@ -515,6 +517,35 @@ similar way, add the new column definition and then use the ``Meta``
|
||||
WSGIDaemonProcess [... existing options ...] python-path=/opt/python
|
||||
|
||||
|
||||
Customize the project and user table columns
|
||||
===========================================
|
||||
|
||||
|
||||
Keystone V3 has a place to store extra information regarding project and user.
|
||||
Using the override mechanism described in :ref:`horizon-customization-module`,
|
||||
Horizon is able to show these extra information as a custom column.
|
||||
For example, if a user in Keystone has an attribute ``phone_num``, you could
|
||||
define new column::
|
||||
|
||||
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 user_tables
|
||||
from openstack_dashboard.dashboards.identity.users import views
|
||||
|
||||
class MyUsersTable(user_tables.UsersTable):
|
||||
phone_num = tables.Column('phone_num',
|
||||
verbose_name=_('Phone Number'),
|
||||
form_field=forms.CharField(),)
|
||||
|
||||
class Meta(user_tables.UsersTable.Meta):
|
||||
columns = ('name', 'description', 'phone_num')
|
||||
|
||||
views.IndexView.table_class = MyUsersTable
|
||||
|
||||
|
||||
Icons
|
||||
=====
|
||||
|
||||
|
@ -1700,6 +1700,34 @@ This setting controls the behavior of the operation log.
|
||||
* %(param)s
|
||||
|
||||
|
||||
``PROJECT_TABLE_EXTRA_INFO``
|
||||
----------------------
|
||||
|
||||
.. versionadded:: 10.0.0(Newton)
|
||||
|
||||
Default: ``{}``
|
||||
|
||||
Add additional information for project as an extra attribute.
|
||||
Project and user can have any attributes by keystone mechanism.
|
||||
This setting can treat these attributes on Horizon when only
|
||||
using Keystone v3.
|
||||
For example::
|
||||
|
||||
PROJECT_TABLE_EXTRA_INFO = {
|
||||
'phone_num': _('Phone Number'),
|
||||
}
|
||||
|
||||
|
||||
``USER_TABLE_EXTRA_INFO``
|
||||
-------------------
|
||||
|
||||
.. versionadded:: 10.0.0(Newton)
|
||||
|
||||
Default: ``{}``
|
||||
|
||||
Same as ``PROJECT_TABLE_EXTRA_INFO``, add additional information for user.
|
||||
|
||||
|
||||
Django Settings (Partial)
|
||||
=========================
|
||||
|
||||
|
@ -375,7 +375,7 @@ def user_list(request, project=None, domain=None, group=None, filters=None):
|
||||
|
||||
|
||||
def user_create(request, name=None, email=None, password=None, project=None,
|
||||
enabled=None, domain=None, description=None):
|
||||
enabled=None, domain=None, description=None, **data):
|
||||
manager = keystoneclient(request, admin=True).users
|
||||
try:
|
||||
if VERSIONS.active < 3:
|
||||
@ -384,7 +384,8 @@ def user_create(request, name=None, email=None, password=None, project=None,
|
||||
else:
|
||||
return manager.create(name, password=password, email=email,
|
||||
default_project=project, enabled=enabled,
|
||||
domain=domain, description=description)
|
||||
domain=domain, description=description,
|
||||
**data)
|
||||
except keystone_exceptions.Conflict:
|
||||
raise exceptions.Conflict()
|
||||
|
||||
|
@ -10,6 +10,12 @@
|
||||
<dd>{{ project.enabled|yesno|capfirst }}</dd>
|
||||
<dt>{% trans "Description" %}</dt>
|
||||
<dd>{{ project.description|default:_("None") }}</dd>
|
||||
{% if extras %}
|
||||
{% for key, value in extras.items %}
|
||||
<dt>{{ key }}</dt>
|
||||
<dd>{{ value }}</dd>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
|
@ -299,6 +299,7 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests):
|
||||
self.assertEqual(step.action.initial['subnet'],
|
||||
neutron_quotas.get('subnet').limit)
|
||||
|
||||
@override_settings(PROJECT_TABLE_EXTRA_INFO={'phone_num': 'Phone Number'})
|
||||
@test.create_stubs({api.keystone: ('get_default_role',
|
||||
'add_tenant_user_role',
|
||||
'tenant_create',
|
||||
@ -321,6 +322,8 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests):
|
||||
users = self._get_all_users(domain_id)
|
||||
groups = self._get_all_groups(domain_id)
|
||||
roles = self.roles.list()
|
||||
# extra info
|
||||
phone_number = "+81-3-1234-5678"
|
||||
|
||||
# init
|
||||
quotas.get_disabled_quotas(IsA(http.HttpRequest)) \
|
||||
@ -338,6 +341,8 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests):
|
||||
|
||||
# handle
|
||||
project_details = self._get_project_info(project)
|
||||
# add extra info
|
||||
project_details.update({'phone_num': phone_number})
|
||||
quota_data = self._get_quota_info(quota)
|
||||
|
||||
api.keystone.tenant_create(IsA(http.HttpRequest), **project_details) \
|
||||
@ -377,6 +382,7 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests):
|
||||
self.mox.ReplayAll()
|
||||
|
||||
workflow_data.update(self._get_workflow_data(project, quota))
|
||||
workflow_data.update({'phone_num': phone_number})
|
||||
|
||||
url = reverse('horizon:identity:projects:create')
|
||||
res = self.client.post(url, workflow_data)
|
||||
|
@ -16,6 +16,7 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
@ -198,8 +199,13 @@ class UpdateProjectView(workflows.WorkflowView):
|
||||
for field in PROJECT_INFO_FIELDS:
|
||||
initial[field] = getattr(project_info, field, None)
|
||||
|
||||
# Retrieve the domain name where the project belong
|
||||
if keystone.VERSIONS.active >= 3:
|
||||
# get extra columns info
|
||||
ex_info = getattr(settings, 'PROJECT_TABLE_EXTRA_INFO', {})
|
||||
for ex_field in ex_info:
|
||||
initial[ex_field] = getattr(project_info, ex_field, None)
|
||||
|
||||
# Retrieve the domain name where the project belong
|
||||
try:
|
||||
if policy.check((("identity", "identity:get_domain"),),
|
||||
self.request):
|
||||
@ -245,6 +251,12 @@ class DetailProjectView(views.HorizonTemplateView):
|
||||
context["project"] = project
|
||||
context["url"] = reverse(INDEX_URL)
|
||||
context["actions"] = table.render_row_actions(project)
|
||||
|
||||
if keystone.VERSIONS.active >= 3:
|
||||
extra_info = getattr(settings, 'PROJECT_TABLE_EXTRA_INFO', {})
|
||||
context['extras'] = dict(
|
||||
(display_key, getattr(project, key, ''))
|
||||
for key, display_key in extra_info.items())
|
||||
return context
|
||||
|
||||
@memoized.memoized_method
|
||||
|
@ -169,6 +169,14 @@ class CreateProjectInfoAction(workflows.Action):
|
||||
readonlyInput = forms.TextInput(attrs={'readonly': 'readonly'})
|
||||
self.fields["domain_id"].widget = readonlyInput
|
||||
self.fields["domain_name"].widget = readonlyInput
|
||||
self.add_extra_fields()
|
||||
|
||||
def add_extra_fields(self):
|
||||
# add extra column defined by setting
|
||||
EXTRA_INFO = getattr(settings, 'PROJECT_TABLE_EXTRA_INFO', {})
|
||||
for key, value in EXTRA_INFO.items():
|
||||
form = forms.CharField(label=value, required=False,)
|
||||
self.fields[key] = form
|
||||
|
||||
class Meta(object):
|
||||
name = _("Project Information")
|
||||
@ -185,6 +193,12 @@ class CreateProjectInfo(workflows.Step):
|
||||
"description",
|
||||
"enabled")
|
||||
|
||||
def __init__(self, workflow):
|
||||
super(CreateProjectInfo, self).__init__(workflow)
|
||||
if keystone.VERSIONS.active >= 3:
|
||||
EXTRA_INFO = getattr(settings, 'PROJECT_TABLE_EXTRA_INFO', {})
|
||||
self.contributes += tuple(EXTRA_INFO.keys())
|
||||
|
||||
|
||||
class UpdateProjectMembersAction(workflows.MembershipAction):
|
||||
def __init__(self, request, *args, **kwargs):
|
||||
@ -445,12 +459,20 @@ class CreateProject(CommonQuotaWorkflow):
|
||||
# create the project
|
||||
domain_id = data['domain_id']
|
||||
try:
|
||||
# add extra information
|
||||
if keystone.VERSIONS.active >= 3:
|
||||
EXTRA_INFO = getattr(settings, 'PROJECT_TABLE_EXTRA_INFO', {})
|
||||
kwargs = dict((key, data.get(key)) for key in EXTRA_INFO)
|
||||
else:
|
||||
kwargs = {}
|
||||
|
||||
desc = data['description']
|
||||
self.object = api.keystone.tenant_create(request,
|
||||
name=data['name'],
|
||||
description=desc,
|
||||
enabled=data['enabled'],
|
||||
domain=domain_id)
|
||||
domain=domain_id,
|
||||
**kwargs)
|
||||
return self.object
|
||||
except exceptions.Conflict:
|
||||
msg = _('Project name "%s" is already used.') % data['name']
|
||||
@ -608,6 +630,12 @@ class UpdateProjectInfo(workflows.Step):
|
||||
"description",
|
||||
"enabled")
|
||||
|
||||
def __init__(self, workflow):
|
||||
super(UpdateProjectInfo, self).__init__(workflow)
|
||||
if keystone.VERSIONS.active >= 3:
|
||||
EXTRA_INFO = getattr(settings, 'PROJECT_TABLE_EXTRA_INFO', {})
|
||||
self.contributes += tuple(EXTRA_INFO.keys())
|
||||
|
||||
|
||||
class UpdateProject(CommonQuotaWorkflow):
|
||||
slug = "update_project"
|
||||
@ -649,13 +677,22 @@ class UpdateProject(CommonQuotaWorkflow):
|
||||
domain_id = api.keystone.get_effective_domain_id(self.request)
|
||||
try:
|
||||
project_id = data['project_id']
|
||||
|
||||
# add extra information
|
||||
if keystone.VERSIONS.active >= 3:
|
||||
EXTRA_INFO = getattr(settings, 'PROJECT_TABLE_EXTRA_INFO', {})
|
||||
kwargs = dict((key, data.get(key)) for key in EXTRA_INFO)
|
||||
else:
|
||||
kwargs = {}
|
||||
|
||||
return api.keystone.tenant_update(
|
||||
request,
|
||||
project_id,
|
||||
name=data['name'],
|
||||
description=data['description'],
|
||||
enabled=data['enabled'],
|
||||
domain=domain_id)
|
||||
domain=domain_id,
|
||||
**kwargs)
|
||||
except exceptions.Conflict:
|
||||
msg = _('Project name "%s" is already used.') % data['name']
|
||||
self.failure_message = msg
|
||||
|
@ -93,10 +93,22 @@ class BaseUserForm(forms.SelfHandlingForm):
|
||||
LOG.debug("User: %s has no projects" % user_id)
|
||||
|
||||
|
||||
class AddExtraColumnMixIn(object):
|
||||
def add_extra_fields(self, ordering=None):
|
||||
if api.keystone.VERSIONS.active >= 3:
|
||||
# add extra column defined by setting
|
||||
EXTRA_INFO = getattr(settings, 'USER_TABLE_EXTRA_INFO', {})
|
||||
for key, value in EXTRA_INFO.items():
|
||||
self.fields[key] = forms.CharField(label=value,
|
||||
required=False)
|
||||
if ordering:
|
||||
ordering.append(key)
|
||||
|
||||
|
||||
ADD_PROJECT_URL = "horizon:identity:projects:create"
|
||||
|
||||
|
||||
class CreateUserForm(PasswordMixin, BaseUserForm):
|
||||
class CreateUserForm(PasswordMixin, BaseUserForm, AddExtraColumnMixIn):
|
||||
# Hide the domain_id and domain_name by default
|
||||
domain_id = forms.CharField(label=_("Domain ID"),
|
||||
required=False,
|
||||
@ -129,6 +141,7 @@ class CreateUserForm(PasswordMixin, BaseUserForm):
|
||||
"description", "email", "password",
|
||||
"confirm_password", "project", "role_id",
|
||||
"enabled"]
|
||||
self.add_extra_fields(ordering)
|
||||
self.fields = collections.OrderedDict(
|
||||
(key, self.fields[key]) for key in ordering)
|
||||
role_choices = [(role.id, role.name) for role in roles]
|
||||
@ -153,6 +166,14 @@ class CreateUserForm(PasswordMixin, BaseUserForm):
|
||||
desc = data["description"]
|
||||
if "email" in data:
|
||||
data['email'] = data['email'] or None
|
||||
|
||||
# add extra information
|
||||
if api.keystone.VERSIONS.active >= 3:
|
||||
EXTRA_INFO = getattr(settings, 'USER_TABLE_EXTRA_INFO', {})
|
||||
kwargs = dict((key, data.get(key)) for key in EXTRA_INFO)
|
||||
else:
|
||||
kwargs = {}
|
||||
|
||||
new_user = \
|
||||
api.keystone.user_create(request,
|
||||
name=data['name'],
|
||||
@ -161,7 +182,8 @@ class CreateUserForm(PasswordMixin, BaseUserForm):
|
||||
password=data['password'],
|
||||
project=data['project'] or None,
|
||||
enabled=data['enabled'],
|
||||
domain=domain.id)
|
||||
domain=domain.id,
|
||||
**kwargs)
|
||||
messages.success(request,
|
||||
_('User "%s" was successfully created.')
|
||||
% data['name'])
|
||||
@ -189,7 +211,7 @@ class CreateUserForm(PasswordMixin, BaseUserForm):
|
||||
exceptions.handle(request, _('Unable to create user.'))
|
||||
|
||||
|
||||
class UpdateUserForm(BaseUserForm):
|
||||
class UpdateUserForm(BaseUserForm, AddExtraColumnMixIn):
|
||||
# Hide the domain_id and domain_name by default
|
||||
domain_id = forms.CharField(label=_("Domain ID"),
|
||||
required=False,
|
||||
@ -211,7 +233,7 @@ class UpdateUserForm(BaseUserForm):
|
||||
|
||||
def __init__(self, request, *args, **kwargs):
|
||||
super(UpdateUserForm, self).__init__(request, *args, **kwargs)
|
||||
|
||||
self.add_extra_fields()
|
||||
if api.keystone.keystone_can_edit_user() is False:
|
||||
for field in ('name', 'email'):
|
||||
self.fields.pop(field)
|
||||
|
@ -28,6 +28,12 @@
|
||||
<dt>{% trans "Primary Project Name" %}</dt>
|
||||
<dd>{{ tenant_name }}</dd>
|
||||
{% endif %}
|
||||
{% if extras %}
|
||||
{% for key, value in extras.items %}
|
||||
<dt>{{ key }}</dt>
|
||||
<dd>{{ value }}</dd>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
|
@ -84,6 +84,7 @@ class UsersViewTests(test.BaseAdminViewTests):
|
||||
domain_context_name=domain.name)
|
||||
self.test_index()
|
||||
|
||||
@override_settings(USER_TABLE_EXTRA_INFO={'phone_num': 'Phone Number'})
|
||||
@test.create_stubs({api.keystone: ('user_create',
|
||||
'get_default_domain',
|
||||
'tenant_list',
|
||||
@ -95,6 +96,7 @@ class UsersViewTests(test.BaseAdminViewTests):
|
||||
user = self.users.get(id="1")
|
||||
domain = self._get_default_domain()
|
||||
domain_id = domain.id
|
||||
phone_number = "+81-3-1234-5678"
|
||||
|
||||
role = self.roles.first()
|
||||
|
||||
@ -112,6 +114,7 @@ class UsersViewTests(test.BaseAdminViewTests):
|
||||
IgnoreArg(), user=None).AndReturn(
|
||||
[self.tenants.list(), False])
|
||||
|
||||
kwargs = {'phone_num': phone_number}
|
||||
api.keystone.user_create(IgnoreArg(),
|
||||
name=user.name,
|
||||
description=user.description,
|
||||
@ -119,7 +122,8 @@ class UsersViewTests(test.BaseAdminViewTests):
|
||||
password=user.password,
|
||||
project=self.tenant.id,
|
||||
enabled=True,
|
||||
domain=domain_id).AndReturn(user)
|
||||
domain=domain_id,
|
||||
**kwargs).AndReturn(user)
|
||||
api.keystone.role_list(IgnoreArg()).AndReturn(self.roles.list())
|
||||
api.keystone.get_default_role(IgnoreArg()).AndReturn(role)
|
||||
api.keystone.roles_for_user(IgnoreArg(), user.id, self.tenant.id)
|
||||
@ -137,7 +141,8 @@ class UsersViewTests(test.BaseAdminViewTests):
|
||||
'project': self.tenant.id,
|
||||
'role_id': self.roles.first().id,
|
||||
'enabled': True,
|
||||
'confirm_password': user.password}
|
||||
'confirm_password': user.password,
|
||||
'phone_num': phone_number}
|
||||
res = self.client.post(USER_CREATE_URL, formData)
|
||||
|
||||
self.assertNoFormErrors(res)
|
||||
@ -370,6 +375,7 @@ class UsersViewTests(test.BaseAdminViewTests):
|
||||
res, "form", 'password',
|
||||
['Password must be between 8 and 18 characters.'])
|
||||
|
||||
@override_settings(USER_TABLE_EXTRA_INFO={'phone_num': 'Phone Number'})
|
||||
@test.create_stubs({api.keystone: ('user_get',
|
||||
'domain_get',
|
||||
'tenant_list',
|
||||
@ -381,6 +387,7 @@ class UsersViewTests(test.BaseAdminViewTests):
|
||||
user = self.users.get(id="1")
|
||||
domain_id = user.domain_id
|
||||
domain = self.domains.get(id=domain_id)
|
||||
phone_number = "+81-3-1234-5678"
|
||||
|
||||
api.keystone.user_get(IsA(http.HttpRequest), '1',
|
||||
admin=True).AndReturn(user)
|
||||
@ -396,10 +403,12 @@ class UsersViewTests(test.BaseAdminViewTests):
|
||||
IgnoreArg(), user=user.id).AndReturn(
|
||||
[self.tenants.list(), False])
|
||||
|
||||
kwargs = {'phone_num': phone_number}
|
||||
api.keystone.user_update(IsA(http.HttpRequest),
|
||||
user.id,
|
||||
email=user.email,
|
||||
name=user.name).AndReturn(None)
|
||||
name=user.name,
|
||||
**kwargs).AndReturn(None)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
@ -408,8 +417,8 @@ class UsersViewTests(test.BaseAdminViewTests):
|
||||
'name': user.name,
|
||||
'description': user.description,
|
||||
'email': user.email,
|
||||
'project': self.tenant.id}
|
||||
|
||||
'project': self.tenant.id,
|
||||
'phone_num': phone_number}
|
||||
res = self.client.post(USER_UPDATE_URL, formData)
|
||||
|
||||
self.assertNoFormErrors(res)
|
||||
|
@ -19,6 +19,7 @@
|
||||
import logging
|
||||
import operator
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.core.urlresolvers import reverse_lazy
|
||||
from django.utils.decorators import method_decorator # noqa
|
||||
@ -129,13 +130,18 @@ class UpdateView(forms.ModalFormView):
|
||||
except Exception:
|
||||
exceptions.handle(self.request,
|
||||
_('Unable to retrieve project domain.'))
|
||||
return {'domain_id': domain_id,
|
||||
|
||||
data = {'domain_id': domain_id,
|
||||
'domain_name': domain_name,
|
||||
'id': user.id,
|
||||
'name': user.name,
|
||||
'project': user.project_id,
|
||||
'email': getattr(user, 'email', None),
|
||||
'description': getattr(user, 'description', None)}
|
||||
if api.keystone.VERSIONS.active >= 3:
|
||||
for key in getattr(settings, 'USER_TABLE_EXTRA_INFO', {}):
|
||||
data[key] = getattr(user, key, None)
|
||||
return data
|
||||
|
||||
|
||||
class CreateView(forms.ModalFormView):
|
||||
@ -200,7 +206,10 @@ class DetailView(views.HorizonTemplateView):
|
||||
exceptions.handle(self.request,
|
||||
_('Unable to retrieve project domain.'))
|
||||
context["description"] = getattr(user, "description", _("None"))
|
||||
|
||||
extra_info = getattr(settings, 'USER_TABLE_EXTRA_INFO', {})
|
||||
context['extras'] = dict(
|
||||
(display_key, getattr(user, key, ''))
|
||||
for key, display_key in extra_info.items())
|
||||
context["user"] = user
|
||||
if tenant:
|
||||
context["tenant_name"] = tenant.name
|
||||
|
@ -796,3 +796,19 @@ REST_API_REQUIRED_SETTINGS = ['OPENSTACK_HYPERVISOR_FEATURES',
|
||||
# 'ipv6': ['fc00::/7']
|
||||
#}
|
||||
ALLOWED_PRIVATE_SUBNET_CIDR = {'ipv4': [], 'ipv6': []}
|
||||
|
||||
# Project and user can have any attributes by keystone v3 mechanism.
|
||||
# This settings can treat these attributes on Horizon.
|
||||
# It means, when you show Create/Update modal, attribute below is
|
||||
# shown and you can specify any value.
|
||||
# If you'd like to display these extra data in project or user index table,
|
||||
# Keystone v3 allows you to add extra properties to Project and Users.
|
||||
# Horizon's customization (http://docs.openstack.org/developer/horizon/topics/customizing.html#horizon-customization-module-overrides)
|
||||
# allows you to display this extra information in the Create/Update modal and
|
||||
# the corresponding tables.
|
||||
#PROJECT_TABLE_EXTRA_INFO = {
|
||||
# 'phone_num': _('Phone Number'),
|
||||
#}
|
||||
#USER_TABLE_EXTRA_INFO = {
|
||||
# 'phone_num': _('Phone Number'),
|
||||
#}
|
||||
|
@ -0,0 +1,7 @@
|
||||
---
|
||||
features:
|
||||
- >
|
||||
[`blueprint Supports extra properties in project and user <https://blueprints.launchpad.net/horizon/+spec/support-extra-prop-for-project-and-user>`_]
|
||||
Support an ability to treat additional information for
|
||||
project and user as an extra attribute.
|
||||
|
Loading…
Reference in New Issue
Block a user