diff --git a/horizon/api/keystone.py b/horizon/api/keystone.py
index dbf614945..aca6a0350 100644
--- a/horizon/api/keystone.py
+++ b/horizon/api/keystone.py
@@ -123,10 +123,6 @@ def keystoneclient(request, admin=False):
return conn
-def tenant_name(request, tenant_id):
- return keystoneclient(request).tenants.get(tenant_id).name
-
-
def tenant_create(request, tenant_name, description, enabled):
return keystoneclient(request, admin=True).tenants.create(tenant_name,
description,
@@ -233,6 +229,12 @@ def add_tenant_user_role(request, tenant_id, user_id, role_id):
tenant_id)
+def remove_tenant_user_role(request, tenant_id, user_id, role_id):
+ """ Removes a given single role for a user from a tenant. """
+ client = keystoneclient(request, admin=True)
+ client.roles.remove_user_role(user_id, role_id, tenant_id)
+
+
def remove_tenant_user(request, tenant_id, user_id):
""" Removes all roles from a user on a tenant, removing them from it. """
client = keystoneclient(request, admin=True)
diff --git a/horizon/dashboards/syspanel/projects/forms.py b/horizon/dashboards/syspanel/projects/forms.py
index 8ed56a666..ebccf2416 100644
--- a/horizon/dashboards/syspanel/projects/forms.py
+++ b/horizon/dashboards/syspanel/projects/forms.py
@@ -18,37 +18,16 @@
# 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 api
-from horizon import exceptions
from horizon import forms
-from horizon import messages
+from horizon.dashboards.syspanel.users.forms import CreateUserForm
-LOG = logging.getLogger(__name__)
+class CreateUser(CreateUserForm):
+ role_id = forms.ChoiceField(widget=forms.HiddenInput())
+ tenant_id = forms.CharField(widget=forms.HiddenInput())
-
-class AddUser(forms.SelfHandlingForm):
- tenant_id = forms.CharField(widget=forms.widgets.HiddenInput())
- user_id = forms.CharField(widget=forms.widgets.HiddenInput())
- role_id = forms.ChoiceField(label=_("Role"))
-
- def __init__(self, *args, **kwargs):
- roles = kwargs.pop('roles')
- super(AddUser, self).__init__(*args, **kwargs)
- role_choices = [(role.id, role.name) for role in roles]
- self.fields['role_id'].choices = role_choices
-
- def handle(self, request, data):
- try:
- api.add_tenant_user_role(request,
- data['tenant_id'],
- data['user_id'],
- data['role_id'])
- messages.success(request, _('Successfully added user to project.'))
- return True
- except:
- exceptions.handle(request, _('Unable to add user to project.'))
+ def __init__(self, request, *args, **kwargs):
+ super(CreateUser, self).__init__(request, *args, **kwargs)
+ tenant_id = self.request.path.split("/")[-1]
+ self.fields['tenant_id'].initial = tenant_id
diff --git a/horizon/dashboards/syspanel/projects/tables.py b/horizon/dashboards/syspanel/projects/tables.py
index fdf4f4e23..ddc2ea8ce 100644
--- a/horizon/dashboards/syspanel/projects/tables.py
+++ b/horizon/dashboards/syspanel/projects/tables.py
@@ -17,8 +17,14 @@ LOG = logging.getLogger(__name__)
class ViewMembersLink(tables.LinkAction):
name = "users"
verbose_name = _("Modify Users")
- url = "horizon:syspanel:projects:users"
- classes = ("btn-download",)
+ url = "horizon:syspanel:projects:update"
+ classes = ("ajax-modal", "btn-edit")
+
+ def get_link_url(self, project):
+ step = 'update_members'
+ base_url = reverse(self.url, args=[project.id])
+ param = urlencode({"step": step})
+ return "?".join([base_url, param])
class UsageLink(tables.LinkAction):
diff --git a/horizon/dashboards/syspanel/projects/templates/projects/_create_user.html b/horizon/dashboards/syspanel/projects/templates/projects/_create_user.html
new file mode 100644
index 000000000..bf2005f60
--- /dev/null
+++ b/horizon/dashboards/syspanel/projects/templates/projects/_create_user.html
@@ -0,0 +1,25 @@
+{% extends "horizon/common/_modal_form.html" %}
+{% load i18n %}
+
+{% block form_id %}create_user_form{% endblock %}
+{% block form_action %}{% url horizon:syspanel:projects:create_user tenant_id %}{% endblock %}
+
+{% block modal-header %}{% blocktrans %}Create User for project '{{ tenant_name }}'.{% endblocktrans %}{% endblock %}
+
+
+{% block modal-body %}
+
+
+
+
+
{% trans "Description" %}:
+
{% trans "From here you can create a new user to add to this project." %}
+
+{% endblock %}
+
+{% block modal-footer %}
+
+ {% trans "Cancel" %}
+{% endblock %}
diff --git a/horizon/dashboards/syspanel/projects/templates/projects/_update_members.html b/horizon/dashboards/syspanel/projects/templates/projects/_update_members.html
new file mode 100644
index 000000000..64c9d298c
--- /dev/null
+++ b/horizon/dashboards/syspanel/projects/templates/projects/_update_members.html
@@ -0,0 +1,40 @@
+{% load i18n %}
+
+
+
+
+
+
+
+
+
+
- {% trans "No users found." %}
+
+
+
+
+
+
+
- {% trans "No users found." %}
+
+
+
+
+
+
+ {% include "horizon/common/_form_fields.html" %}
+
diff --git a/horizon/dashboards/syspanel/projects/templates/projects/create_user.html b/horizon/dashboards/syspanel/projects/templates/projects/create_user.html
new file mode 100644
index 000000000..306bf669d
--- /dev/null
+++ b/horizon/dashboards/syspanel/projects/templates/projects/create_user.html
@@ -0,0 +1,12 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Add New User" %}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Add New User") %}
+{% endblock page_header %}
+
+
+{% block main %}
+ {% include 'horizon/common/_create_user.html' %}
+{% endblock %}
diff --git a/horizon/dashboards/syspanel/projects/tests.py b/horizon/dashboards/syspanel/projects/tests.py
index a85216fdc..5697bf3e8 100644
--- a/horizon/dashboards/syspanel/projects/tests.py
+++ b/horizon/dashboards/syspanel/projects/tests.py
@@ -193,17 +193,33 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests):
quota_data[field] = int(getattr(quota, field, None))
return quota_data
- @test.create_stubs({api: ('tenant_get',
- 'tenant_quota_get',)})
+ @test.create_stubs({api: ('get_default_role',
+ 'roles_for_user',
+ 'tenant_get',
+ 'tenant_quota_get',),
+ api.keystone: ('user_list',
+ 'role_list',)})
def test_update_project_get(self):
project = self.tenants.first()
quota = self.quotas.first()
+ default_role = self.roles.first()
+ users = self.users.list()
+ roles = self.roles.list()
api.tenant_get(IsA(http.HttpRequest), self.tenant.id, admin=True) \
.AndReturn(project)
api.tenant_quota_get(IsA(http.HttpRequest), self.tenant.id) \
.AndReturn(quota)
+ api.get_default_role(IsA(http.HttpRequest)).AndReturn(default_role)
+ api.keystone.user_list(IsA(http.HttpRequest)).AndReturn(users)
+ api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
+
+ for user in users:
+ api.roles_for_user(IsA(http.HttpRequest),
+ user.id,
+ self.tenant.id).AndReturn(roles)
+
self.mox.ReplayAll()
url = reverse('horizon:syspanel:projects:update',
@@ -224,21 +240,47 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests):
project.description)
self.assertQuerysetEqual(workflow.steps,
['',
+ '',
''])
@test.create_stubs({api: ('tenant_get',
'tenant_quota_get',
'tenant_update',
- 'tenant_quota_update',)})
+ 'tenant_quota_update',
+ 'get_default_role',
+ 'roles_for_user',
+ 'remove_tenant_user_role',
+ 'add_tenant_user_role'),
+ api.keystone: ('user_list',
+ 'role_list',)})
def test_update_project_post(self):
project = self.tenants.first()
quota = self.quotas.first()
+ default_role = self.roles.first()
+ users = self.users.list()
+ roles = self.roles.list()
+ current_roles = self.roles.list()
- api.tenant_get(IsA(http.HttpRequest), project.id, admin=True) \
+ # get/init
+ api.tenant_get(IsA(http.HttpRequest), self.tenant.id, admin=True) \
.AndReturn(project)
- api.tenant_quota_get(IsA(http.HttpRequest), project.id) \
+ api.tenant_quota_get(IsA(http.HttpRequest), self.tenant.id) \
.AndReturn(quota)
+ api.get_default_role(IsA(http.HttpRequest)).AndReturn(default_role)
+ api.keystone.user_list(IsA(http.HttpRequest)).AndReturn(users)
+ api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
+
+ workflow_data = {}
+ for user in users:
+ api.roles_for_user(IsA(http.HttpRequest),
+ user.id,
+ self.tenant.id).AndReturn(roles)
+ role_ids = [role.id for role in roles]
+ if role_ids:
+ workflow_data.setdefault("role_" + role_ids[0], []) \
+ .append(user.id)
+
# update some fields
project._info["name"] = "updated name"
project._info["description"] = "updated description"
@@ -251,8 +293,47 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests):
"enabled": project.enabled}
updated_quota = self._get_quota_info(quota)
+ # contribute
+ api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
+
+ # handle
api.tenant_update(IsA(http.HttpRequest), **updated_project) \
.AndReturn(project)
+
+ api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
+ api.keystone.user_list(IsA(http.HttpRequest),
+ tenant_id=self.tenant.id).AndReturn(users)
+
+ for user in users:
+ api.roles_for_user(IsA(http.HttpRequest),
+ user.id,
+ self.tenant.id) \
+ .AndReturn(current_roles)
+ for role in roles:
+ if "role_" + role.id in workflow_data:
+ ulist = workflow_data["role_" + role.id]
+ if role not in current_roles:
+ api.add_tenant_user_role(IsA(http.HttpRequest),
+ tenant_id=self.tenant.id,
+ user_id=user,
+ role_id=role.id)
+ else:
+ current_roles.pop(current_roles.index(role))
+ for to_delete in current_roles:
+ api.remove_tenant_user_role(IsA(http.HttpRequest),
+ tenant_id=self.tenant.id,
+ user_id=user.id,
+ role_id=to_delete.id)
+ for role in roles:
+ if "role_" + role.id in workflow_data:
+ ulist = workflow_data["role_" + role.id]
+ for user in ulist:
+ if not filter(lambda x: user == x.id, users):
+ api.add_tenant_user_role(IsA(http.HttpRequest),
+ tenant_id=self.tenant.id,
+ user_id=user,
+ role_id=role.id)
+
api.tenant_quota_update(IsA(http.HttpRequest),
project.id,
**updated_quota)
@@ -260,10 +341,11 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests):
self.mox.ReplayAll()
# submit form data
- workflow_data = {"name": project._info["name"],
+ project_data = {"name": project._info["name"],
"id": project.id,
"description": project._info["description"],
"enabled": project.enabled}
+ workflow_data.update(project_data)
workflow_data.update(updated_quota)
url = reverse('horizon:syspanel:projects:update',
args=[self.tenant.id])
@@ -272,14 +354,10 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests):
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
- @test.create_stubs({api: ('tenant_get',
- 'tenant_quota_get',)})
+ @test.create_stubs({api: ('tenant_get',)})
def test_update_project_get_error(self):
- project = self.tenants.first()
api.tenant_get(IsA(http.HttpRequest), self.tenant.id, admin=True) \
- .AndReturn(project)
- api.tenant_quota_get(IsA(http.HttpRequest), self.tenant.id) \
.AndRaise(self.exceptions.nova)
self.mox.ReplayAll()
@@ -292,21 +370,46 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests):
@test.create_stubs({api: ('tenant_get',
'tenant_quota_get',
- 'tenant_update',)})
+ 'tenant_update',
+ 'tenant_quota_update',
+ 'get_default_role',
+ 'roles_for_user',
+ 'remove_tenant_user',
+ 'add_tenant_user_role'),
+ api.keystone: ('user_list',
+ 'role_list',)})
def test_update_project_tenant_update_error(self):
project = self.tenants.first()
quota = self.quotas.first()
+ default_role = self.roles.first()
+ users = self.users.list()
+ roles = self.roles.list()
- api.tenant_get(IsA(http.HttpRequest), project.id, admin=True) \
+ # get/init
+ api.tenant_get(IsA(http.HttpRequest), self.tenant.id, admin=True) \
.AndReturn(project)
- api.tenant_quota_get(IsA(http.HttpRequest), project.id) \
+ api.tenant_quota_get(IsA(http.HttpRequest), self.tenant.id) \
.AndReturn(quota)
+ api.get_default_role(IsA(http.HttpRequest)).AndReturn(default_role)
+ api.keystone.user_list(IsA(http.HttpRequest)).AndReturn(users)
+ api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
+
+ workflow_data = {}
+ for user in users:
+ api.roles_for_user(IsA(http.HttpRequest),
+ user.id,
+ self.tenant.id).AndReturn(roles)
+ role_ids = [role.id for role in roles]
+ if role_ids:
+ workflow_data.setdefault("role_" + role_ids[0], []) \
+ .append(user.id)
+
# update some fields
project._info["name"] = "updated name"
project._info["description"] = "updated description"
- quota.metadata_items = '444'
- quota.volumes = '444'
+ quota.metadata_items = 444
+ quota.volumes = 444
updated_project = {"tenant_name": project._info["name"],
"tenant_id": project.id,
@@ -314,16 +417,21 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests):
"enabled": project.enabled}
updated_quota = self._get_quota_info(quota)
+ # contribute
+ api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
+
+ # handle
api.tenant_update(IsA(http.HttpRequest), **updated_project) \
.AndRaise(self.exceptions.keystone)
self.mox.ReplayAll()
# submit form data
- workflow_data = {"name": project._info["name"],
+ project_data = {"name": project._info["name"],
"id": project.id,
"description": project._info["description"],
"enabled": project.enabled}
+ workflow_data.update(project_data)
workflow_data.update(updated_quota)
url = reverse('horizon:syspanel:projects:update',
args=[self.tenant.id])
@@ -335,22 +443,46 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests):
@test.create_stubs({api: ('tenant_get',
'tenant_quota_get',
'tenant_update',
- 'tenant_quota_update',)})
+ 'tenant_quota_update',
+ 'get_default_role',
+ 'roles_for_user',
+ 'remove_tenant_user_role',
+ 'add_tenant_user_role'),
+ api.keystone: ('user_list',
+ 'role_list',)})
def test_update_project_quota_update_error(self):
project = self.tenants.first()
quota = self.quotas.first()
+ default_role = self.roles.first()
+ users = self.users.list()
+ roles = self.roles.list()
+ current_roles = self.roles.list()
- # first set of calls for 'get' because the url takes an arg
- api.tenant_get(IsA(http.HttpRequest), project.id, admin=True) \
+ # get/init
+ api.tenant_get(IsA(http.HttpRequest), self.tenant.id, admin=True) \
.AndReturn(project)
- api.tenant_quota_get(IsA(http.HttpRequest), project.id) \
+ api.tenant_quota_get(IsA(http.HttpRequest), self.tenant.id) \
.AndReturn(quota)
+ api.get_default_role(IsA(http.HttpRequest)).AndReturn(default_role)
+ api.keystone.user_list(IsA(http.HttpRequest)).AndReturn(users)
+ api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
+
+ workflow_data = {}
+ for user in users:
+ api.roles_for_user(IsA(http.HttpRequest),
+ user.id,
+ self.tenant.id).AndReturn(roles)
+ role_ids = [role.id for role in roles]
+ if role_ids:
+ workflow_data.setdefault("role_" + role_ids[0], []) \
+ .append(user.id)
+
# update some fields
project._info["name"] = "updated name"
project._info["description"] = "updated description"
- quota.metadata_items = '444'
- quota.volumes = '444'
+ quota.metadata_items = 444
+ quota.volumes = 444
updated_project = {"tenant_name": project._info["name"],
"tenant_id": project.id,
@@ -358,20 +490,159 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests):
"enabled": project.enabled}
updated_quota = self._get_quota_info(quota)
+ # contribute
+ api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
+
+ # handle
+ # handle
api.tenant_update(IsA(http.HttpRequest), **updated_project) \
.AndReturn(project)
+
+ api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
+ api.keystone.user_list(IsA(http.HttpRequest),
+ tenant_id=self.tenant.id).AndReturn(users)
+
+ for user in users:
+ api.roles_for_user(IsA(http.HttpRequest),
+ user.id,
+ self.tenant.id) \
+ .AndReturn(current_roles)
+ for role in roles:
+ if "role_" + role.id in workflow_data:
+ ulist = workflow_data["role_" + role.id]
+ if role not in current_roles:
+ api.add_tenant_user_role(IsA(http.HttpRequest),
+ tenant_id=self.tenant.id,
+ user_id=user,
+ role_id=role.id)
+ else:
+ current_roles.pop(current_roles.index(role))
+ for to_delete in current_roles:
+ api.remove_tenant_user_role(IsA(http.HttpRequest),
+ tenant_id=self.tenant.id,
+ user_id=user.id,
+ role_id=to_delete.id)
+ for role in roles:
+ if "role_" + role.id in workflow_data:
+ ulist = workflow_data["role_" + role.id]
+ for user in ulist:
+ if not filter(lambda x: user == x.id, users):
+ api.add_tenant_user_role(IsA(http.HttpRequest),
+ tenant_id=self.tenant.id,
+ user_id=user,
+ role_id=role.id)
+
api.tenant_quota_update(IsA(http.HttpRequest),
project.id,
- **updated_quota) \
- .AndRaise(self.exceptions.nova)
+ **updated_quota).AndRaise(self.exceptions.nova)
self.mox.ReplayAll()
# submit form data
- workflow_data = {"name": updated_project["tenant_name"],
+ project_data = {"name": project._info["name"],
"id": project.id,
- "description": updated_project["description"],
+ "description": project._info["description"],
"enabled": project.enabled}
+ workflow_data.update(project_data)
+ workflow_data.update(updated_quota)
+ url = reverse('horizon:syspanel:projects:update',
+ args=[self.tenant.id])
+ res = self.client.post(url, workflow_data)
+
+ self.assertNoFormErrors(res)
+ self.assertRedirectsNoFollow(res, INDEX_URL)
+
+ @test.create_stubs({api: ('tenant_get',
+ 'tenant_quota_get',
+ 'tenant_update',
+ 'get_default_role',
+ 'roles_for_user',
+ 'remove_tenant_user_role',
+ 'add_tenant_user_role'),
+ api.keystone: ('user_list',
+ 'role_list',)})
+ def test_update_project_member_update_error(self):
+ project = self.tenants.first()
+ quota = self.quotas.first()
+ default_role = self.roles.first()
+ users = self.users.list()
+ roles = self.roles.list()
+ current_roles = self.roles.list()
+
+ # get/init
+ api.tenant_get(IsA(http.HttpRequest), self.tenant.id, admin=True) \
+ .AndReturn(project)
+ api.tenant_quota_get(IsA(http.HttpRequest), self.tenant.id) \
+ .AndReturn(quota)
+
+ api.get_default_role(IsA(http.HttpRequest)).AndReturn(default_role)
+ api.keystone.user_list(IsA(http.HttpRequest)).AndReturn(users)
+ api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
+
+ workflow_data = {}
+ for user in users:
+ api.roles_for_user(IsA(http.HttpRequest),
+ user.id,
+ self.tenant.id).AndReturn(roles)
+ role_ids = [role.id for role in roles]
+ if role_ids:
+ workflow_data.setdefault("role_" + role_ids[0], []) \
+ .append(user.id)
+
+ # update some fields
+ project._info["name"] = "updated name"
+ project._info["description"] = "updated description"
+ quota.metadata_items = 444
+ quota.volumes = 444
+
+ updated_project = {"tenant_name": project._info["name"],
+ "tenant_id": project.id,
+ "description": project._info["description"],
+ "enabled": project.enabled}
+ updated_quota = self._get_quota_info(quota)
+
+ # contribute
+ api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
+
+ # handle
+ api.tenant_update(IsA(http.HttpRequest), **updated_project) \
+ .AndReturn(project)
+
+ api.keystone.role_list(IsA(http.HttpRequest)).AndReturn(roles)
+ api.keystone.user_list(IsA(http.HttpRequest),
+ tenant_id=self.tenant.id).AndReturn(users)
+
+ for user in users:
+ api.roles_for_user(IsA(http.HttpRequest),
+ user.id,
+ self.tenant.id) \
+ .AndReturn(current_roles)
+ for role in roles:
+ if "role_" + role.id in workflow_data:
+ if role not in current_roles:
+ api.add_tenant_user_role(IsA(http.HttpRequest),
+ tenant_id=self.tenant.id,
+ user_id=user,
+ role_id=role.id)
+ else:
+ current_roles.pop(current_roles.index(role))
+ for to_delete in current_roles:
+ api.remove_tenant_user_role(IsA(http.HttpRequest),
+ tenant_id=self.tenant.id,
+ user_id=user.id,
+ role_id=to_delete.id) \
+ .AndRaise(self.exceptions.nova)
+ break
+ break
+
+ self.mox.ReplayAll()
+
+ # submit form data
+ project_data = {"name": project._info["name"],
+ "id": project.id,
+ "description": project._info["description"],
+ "enabled": project.enabled}
+ workflow_data.update(project_data)
workflow_data.update(updated_quota)
url = reverse('horizon:syspanel:projects:update',
args=[self.tenant.id])
diff --git a/horizon/dashboards/syspanel/projects/urls.py b/horizon/dashboards/syspanel/projects/urls.py
index 9387dd696..59369302c 100644
--- a/horizon/dashboards/syspanel/projects/urls.py
+++ b/horizon/dashboards/syspanel/projects/urls.py
@@ -20,9 +20,9 @@
from django.conf.urls.defaults import patterns, url
-from .views import (IndexView, UsersView,
- AddUserView, TenantUsageView,
- CreateProjectView, UpdateProjectView)
+from .views import (IndexView, TenantUsageView,
+ CreateProjectView, UpdateProjectView,
+ CreateUserView)
urlpatterns = patterns('',
@@ -32,7 +32,6 @@ urlpatterns = patterns('',
UpdateProjectView.as_view(), name='update'),
url(r'^(?P[^/]+)/usage/$',
TenantUsageView.as_view(), name='usage'),
- url(r'^(?P[^/]+)/users/$', UsersView.as_view(), name='users'),
- url(r'^(?P[^/]+)/users/(?P[^/]+)/add/$',
- AddUserView.as_view(), name='add_user')
+ url(r'^(?P[^/]+)/create_user/$',
+ CreateUserView.as_view(), name='create_user'),
)
diff --git a/horizon/dashboards/syspanel/projects/views.py b/horizon/dashboards/syspanel/projects/views.py
index 8abb53058..a46ba4e45 100644
--- a/horizon/dashboards/syspanel/projects/views.py
+++ b/horizon/dashboards/syspanel/projects/views.py
@@ -19,19 +19,18 @@
# under the License.
import logging
-import operator
from django.core.urlresolvers import reverse, reverse_lazy
from django.utils.translation import ugettext_lazy as _
from horizon import api
from horizon import exceptions
-from horizon import forms
from horizon import tables
from horizon import usage
from horizon import workflows
+from horizon.dashboards.syspanel.users.views import CreateView
-from .forms import AddUser
+from .forms import CreateUser
from .tables import TenantsTable, TenantUsersTable, AddUsersTable
from .workflows import CreateProject, UpdateProject
@@ -52,6 +51,8 @@ PROJECT_INFO_FIELDS = ("name",
"description",
"enabled")
+INDEX_URL = "horizon:syspanel:projects:index"
+
class TenantContextMixin(object):
def get_object(self):
@@ -62,10 +63,9 @@ class TenantContextMixin(object):
tenant_id,
admin=True)
except:
- redirect = reverse("horizon:syspanel:projects:index")
exceptions.handle(self.request,
_('Unable to retrieve project information.'),
- redirect=redirect)
+ redirect=reverse(INDEX_URL))
return self._object
def get_context_data(self, **kwargs):
@@ -106,10 +106,9 @@ class UsersView(tables.MultiTableView):
'all_users': all_users,
'tenant_users': tenant_users}
except:
- redirect = reverse("horizon:syspanel:projects:index")
exceptions.handle(self.request,
_("Unable to retrieve users."),
- redirect=redirect)
+ redirect=reverse(INDEX_URL))
return self._shared_data
def get_tenant_users_data(self):
@@ -127,42 +126,6 @@ class UsersView(tables.MultiTableView):
return context
-class AddUserView(TenantContextMixin, forms.ModalFormView):
- form_class = AddUser
- template_name = 'syspanel/projects/add_user.html'
- success_url = 'horizon:syspanel:projects:users'
-
- def get_success_url(self):
- return reverse(self.success_url,
- args=(self.request.POST['tenant_id'],))
-
- def get_context_data(self, **kwargs):
- context = super(AddUserView, self).get_context_data(**kwargs)
- context['tenant_id'] = self.kwargs["tenant_id"]
- context['user_id'] = self.kwargs["user_id"]
- return context
-
- def get_form_kwargs(self):
- kwargs = super(AddUserView, self).get_form_kwargs()
- try:
- roles = api.keystone.role_list(self.request)
- except:
- redirect = reverse("horizon:syspanel:projects:users",
- args=(self.kwargs["tenant_id"],))
- exceptions.handle(self.request,
- _("Unable to retrieve roles."),
- redirect=redirect)
- roles.sort(key=operator.attrgetter("id"))
- kwargs['roles'] = roles
- return kwargs
-
- def get_initial(self):
- default_role = api.keystone.get_default_role(self.request)
- return {'tenant_id': self.kwargs['tenant_id'],
- 'user_id': self.kwargs['user_id'],
- 'role_id': getattr(default_role, "id", None)}
-
-
class TenantUsageView(usage.UsageView):
table_class = usage.TenantUsageTable
usage_class = usage.TenantUsage
@@ -215,8 +178,26 @@ class UpdateProjectView(workflows.WorkflowView):
for field in QUOTA_FIELDS:
initial[field] = getattr(quota_data, field, None)
except:
- redirect = reverse("horizon:syspanel:projects:index")
exceptions.handle(self.request,
_('Unable to retrieve project details.'),
- redirect=redirect)
+ redirect=reverse(INDEX_URL))
return initial
+
+
+class CreateUserView(CreateView):
+ form_class = CreateUser
+ template_name = "syspanel/projects/create_user.html"
+ success_url = reverse_lazy('horizon:syspanel:projects:index')
+
+ def get_initial(self):
+ default_role = api.keystone.get_default_role(self.request)
+ return {'role_id': getattr(default_role, "id", None),
+ 'tenant_id': self.kwargs['tenant_id']}
+
+ def get_context_data(self, **kwargs):
+ context = super(CreateUserView, self).get_context_data(**kwargs)
+ context['tenant_id'] = self.kwargs['tenant_id']
+ context['tenant_name'] = api.tenant_get(self.request,
+ self.kwargs['tenant_id'],
+ admin=True).name
+ return context
diff --git a/horizon/dashboards/syspanel/projects/workflows.py b/horizon/dashboards/syspanel/projects/workflows.py
index 6a7ec99b4..98f599320 100644
--- a/horizon/dashboards/syspanel/projects/workflows.py
+++ b/horizon/dashboards/syspanel/projects/workflows.py
@@ -19,12 +19,13 @@
# under the License.
-from django import forms
from django.utils.translation import ugettext as _
+from django.core.urlresolvers import reverse
from horizon import api
from horizon import exceptions
from horizon import workflows
+from horizon import forms
class UpdateProjectQuotaAction(workflows.Action):
@@ -146,6 +147,96 @@ class UpdateProjectInfo(workflows.Step):
"enabled")
+INDEX_URL = "horizon:syspanel:projects:index"
+ADD_USER_URL = "horizon:syspanel:projects:create_user"
+
+
+class UpdateProjectMembersAction(workflows.Action):
+ default_role = forms.CharField(required=False)
+
+ def __init__(self, request, *args, **kwargs):
+ super(UpdateProjectMembersAction, self).__init__(request,
+ *args,
+ **kwargs)
+ err_msg = _('Unable to retrieve user list. Please try again later.')
+ project_id = args[0]['project_id']
+
+ # set up the inline user creation
+ self.fields['new_user'] = forms.DynamicChoiceField(
+ required=False,
+ label=_("Create New User"),
+ add_item_link=ADD_USER_URL,
+ add_item_link_args=project_id)
+
+ # Get the default role
+ try:
+ default_role = api.get_default_role(self.request).id
+ except:
+ exceptions.handle(self.request,
+ err_msg,
+ redirect=reverse(INDEX_URL))
+ self.fields['default_role'].initial = default_role
+
+ # Get list of available users
+ all_users = []
+ try:
+ all_users = api.keystone.user_list(request)
+ except:
+ exceptions.handle(request, err_msg)
+ users_list = [(user.id, user.name) for user in all_users]
+
+ # Get list of roles
+ role_list = []
+ try:
+ role_list = api.keystone.role_list(request)
+ except:
+ exceptions.handle(request,
+ err_msg,
+ redirect=reverse(INDEX_URL))
+ for role in role_list:
+ field_name = "role_" + role.id
+ label = _(role.name)
+ self.fields[field_name] = forms.MultipleChoiceField(required=False,
+ label=label)
+ self.fields[field_name].choices = users_list
+ self.fields[field_name].initial = []
+
+ # Figure out users & roles
+ for user in all_users:
+ try:
+ roles = api.roles_for_user(self.request, user.id, project_id)
+ except:
+ exceptions.handle(request,
+ err_msg,
+ redirect=reverse(INDEX_URL))
+ if roles:
+ primary_role = roles[0].id
+ self.fields["role_" + primary_role].initial.append(user.id)
+
+ class Meta:
+ name = _("Project Members")
+ slug = "update_members"
+
+
+class UpdateProjectMembers(workflows.Step):
+ action_class = UpdateProjectMembersAction
+ template_name = "syspanel/projects/_update_members.html"
+
+ def contribute(self, data, context):
+ if data:
+ try:
+ roles = api.keystone.role_list(self.workflow.request)
+ except:
+ exceptions.handle(self.workflow.request,
+ _('Unable to retrieve user list.'))
+
+ post = self.workflow.request.POST
+ for role in roles:
+ field = "role_" + role.id
+ context[field] = post.getlist(field)
+ return context
+
+
class UpdateProject(workflows.Workflow):
slug = "update_project"
name = _("Edit Project")
@@ -154,17 +245,18 @@ class UpdateProject(workflows.Workflow):
failure_message = _('Unable to modify project "%s".')
success_url = "horizon:syspanel:projects:index"
default_steps = (UpdateProjectInfo,
+ UpdateProjectMembers,
UpdateProjectQuota)
def format_status_message(self, message):
return message % self.context.get('name', 'unknown project')
def handle(self, request, data):
-
+ project_id = data['project_id']
# update project info
try:
api.tenant_update(request,
- tenant_id=data['project_id'],
+ tenant_id=project_id,
tenant_name=data['name'],
description=data['description'],
enabled=data['enabled'])
@@ -172,11 +264,64 @@ class UpdateProject(workflows.Workflow):
exceptions.handle(request, ignore=True)
return False
+ # update project members
+ users_to_modify = 0
+ try:
+ available_roles = api.keystone.role_list(request)
+ project_members = api.keystone.user_list(request,
+ tenant_id=project_id)
+ users_to_modify = len(project_members)
+ for user in project_members:
+ current_roles = api.roles_for_user(self.request,
+ user.id,
+ project_id)
+ for role in available_roles:
+ role_list = data["role_" + role.id]
+ if user.id in role_list:
+ if role not in current_roles:
+ # user role has changed
+ api.add_tenant_user_role(request,
+ tenant_id=project_id,
+ user_id=user.id,
+ role_id=role.id)
+ else:
+ # user role is unchanged
+ current_roles.pop(current_roles.index(role))
+ # delete user's removed roles
+ for to_delete in current_roles:
+ api.remove_tenant_user_role(request,
+ tenant_id=project_id,
+ user_id=user.id,
+ role_id=to_delete.id)
+ users_to_modify -= 1
+
+ # add new roles to project
+ for role in available_roles:
+ # count how many users may be added for exception handling
+ role_list = data["role_" + role.id]
+ users_to_modify += len(role_list)
+ for role in available_roles:
+ role_list = data["role_" + role.id]
+ users_added = 0
+ for user in role_list:
+ if not filter(lambda x: user == x.id, project_members):
+ api.add_tenant_user_role(request,
+ tenant_id=project_id,
+ user_id=user,
+ role_id=role.id)
+ users_added += 1
+ users_to_modify -= users_added
+ except:
+ exceptions.handle(request, _('Failed to modify %s project members '
+ 'and update project quotas.'
+ % users_to_modify))
+ return True
+
# update the project quota
ifcb = data['injected_file_content_bytes']
try:
api.tenant_quota_update(request,
- data['project_id'],
+ project_id,
metadata_items=data['metadata_items'],
injected_file_content_bytes=ifcb,
volumes=data['volumes'],
@@ -188,6 +333,7 @@ class UpdateProject(workflows.Workflow):
cores=data['cores'])
return True
except:
- exceptions.handle(request, _('Modified project information, but'
- 'unable to modify project quotas.'))
+ exceptions.handle(request, _('Modified project information and '
+ 'members, but unable to modify '
+ 'project quotas.'))
return True
diff --git a/horizon/dashboards/syspanel/users/forms.py b/horizon/dashboards/syspanel/users/forms.py
index 7077ff86e..31bbd99ab 100644
--- a/horizon/dashboards/syspanel/users/forms.py
+++ b/horizon/dashboards/syspanel/users/forms.py
@@ -94,14 +94,16 @@ class CreateUserForm(BaseUserForm):
messages.success(request,
_('User "%s" was successfully created.')
% data['name'])
- try:
- api.add_tenant_user_role(request,
- data['tenant_id'],
- new_user.id,
- data['role_id'])
- except:
- exceptions.handle(request,
- _('Unable to add user to primary project.'))
+ if data['role_id']:
+ try:
+ api.add_tenant_user_role(request,
+ data['tenant_id'],
+ new_user.id,
+ data['role_id'])
+ except:
+ exceptions.handle(request,
+ _('Unable to add user'
+ 'to primary project.'))
return new_user
except:
exceptions.handle(request, _('Unable to create user.'))
diff --git a/horizon/forms/fields.py b/horizon/forms/fields.py
index b73a975c3..b728e74ee 100644
--- a/horizon/forms/fields.py
+++ b/horizon/forms/fields.py
@@ -35,7 +35,11 @@ class DynamicSelectWidget(widgets.Select):
if callable(self.add_item_link):
return self.add_item_link()
try:
- return urlresolvers.reverse(self.add_item_link)
+ if self.add_item_link_args:
+ return urlresolvers.reverse(self.add_item_link,
+ args=[self.add_item_link_args])
+ else:
+ return urlresolvers.reverse(self.add_item_link)
except urlresolvers.NoReverseMatch:
return self.add_item_link
@@ -51,9 +55,14 @@ class DynamicChoiceField(fields.ChoiceField):
"""
widget = DynamicSelectWidget
- def __init__(self, add_item_link=None, *args, **kwargs):
+ def __init__(self,
+ add_item_link=None,
+ add_item_link_args=None,
+ *args,
+ **kwargs):
super(DynamicChoiceField, self).__init__(*args, **kwargs)
self.widget.add_item_link = add_item_link
+ self.widget.add_item_link_args = add_item_link_args
class DynamicTypedChoiceField(DynamicChoiceField, fields.TypedChoiceField):
diff --git a/horizon/static/horizon/js/horizon.modals.js b/horizon/static/horizon/js/horizon.modals.js
index 6e0fe814a..c845487d5 100644
--- a/horizon/static/horizon/js/horizon.modals.js
+++ b/horizon/static/horizon/js/horizon.modals.js
@@ -114,6 +114,7 @@ horizon.addInitFunction(function() {
json_data = $.parseJSON(data);
field_to_update = $("#" + add_to_field_header);
field_to_update.append("");
+ field_to_update.change();
field_to_update.val(json_data[0]);
} else {
horizon.modals.success(data, textStatus, jqXHR);
diff --git a/horizon/static/horizon/js/horizon.projects.js b/horizon/static/horizon/js/horizon.projects.js
new file mode 100644
index 000000000..0091424ee
--- /dev/null
+++ b/horizon/static/horizon/js/horizon.projects.js
@@ -0,0 +1,467 @@
+/* Namespace for core functionality related to Project Workflows. */
+horizon.projects = {
+
+ current_membership: [],
+ users: [],
+ roles: [],
+ default_role_id: "",
+ workflow_loaded: false,
+ no_project_members: 'This project currently has no members.',
+ no_available_users: 'No more available users to add.',
+ no_filter_results: 'No users found.',
+ filter_btn_text: 'Filter',
+
+
+ /* Parses the form field selector's ID to get either the
+ * role or user id (i.e. returns "id12345" when
+ * passed the selector with id: "id_user_id12345").
+ **/
+ get_field_id: function(id_string) {
+ return id_string.slice(id_string.lastIndexOf("_") + 1);
+ },
+
+
+ /*
+ * Gets the html select element associated with a given
+ * role id for role_id.
+ **/
+ get_role_element: function(role_id) {
+ return $('select[id^="id_role_' + role_id + '"]');
+ },
+
+
+ /*
+ * Initializes all of the horizon.projects lists with
+ * data parsed from the hidden form fields, as well as the
+ * default role id.
+ **/
+ init_properties: function() {
+ horizon.projects.default_role_id = $('#id_default_role').attr('value');
+ horizon.projects.init_user_list();
+ horizon.projects.init_role_list();
+ horizon.projects.init_current_membership();
+ },
+
+
+ /*
+ * Initializes an associative array mapping user ids to user names.
+ **/
+ init_user_list: function() {
+ _.each($(this.get_role_element("")).find("option"), function (option) {
+ horizon.projects.users[option.value] = option.text;
+ });
+ },
+
+
+ /*
+ * Initializes an associative array mapping role ids to role names.
+ **/
+ init_role_list: function() {
+ _.each($('label[for^="id_role_"]'), function(role) {
+ var id = horizon.projects.get_field_id($(role).attr('for'));
+ horizon.projects.roles[id] = $(role).text();
+ });
+ },
+
+
+ /*
+ * Initializes an associative array of lists of the current
+ * members for each available role.
+ **/
+ init_current_membership: function() {
+ var members_list = [];
+ var role_name, role_id, selected_members;
+ _.each(this.get_role_element(''), function(value, key) {
+ role_id = horizon.projects.get_field_id($(value).attr('id'));
+ role_name = $('label[for="id_role_' + role_id + '"]').text();
+
+ // get the array of members who are selected in this list
+ selected_members = $(value).find("option:selected");
+ // extract the member names and add them to the dictionary of lists
+ members_list = [];
+ if (selected_members) {
+ _.each(selected_members, function(member) {
+ members_list.push(member.value);
+ });
+ }
+ horizon.projects.current_membership[role_id] = members_list;
+ });
+ },
+
+
+ /*
+ * Checks to see whether a user is a member of the current project.
+ * If they are, returns the id of their primary role.
+ **/
+ is_project_member: function(user_id) {
+ for (role in horizon.projects.current_membership) {
+ if ($.inArray(user_id, horizon.projects.current_membership[role]) >= 0) {
+ return role;
+ }
+ }
+ return false;
+ },
+
+
+ /*
+ * Updates the selected values on the role_list's form field, as
+ * well as the current_membership dictionary's list.
+ **/
+ update_role_lists: function(role_id, new_list) {
+ this.get_role_element(role_id).val(new_list);
+ this.get_role_element(role_id).find("option[value='" + role_id + "").attr("selected", "selected");
+
+ horizon.projects.current_membership[role_id] = new_list;
+ },
+
+
+ /*
+ * Helper function for remove_user_from_role.
+ **/
+ remove_user: function(user_id, role_id, role_list) {
+ var index = role_list.indexOf(user_id);
+ if (index >= 0) {
+ // remove member from list
+ role_list.splice(index, 1);
+ horizon.projects.update_role_lists(role_id, role_list);
+ }
+ },
+
+
+ /*
+ * Searches through the role lists and removes a given user
+ * from the lists.
+ **/
+ remove_user_from_role: function(user_id, role_id) {
+ if (role_id) {
+ var role_list = horizon.projects.current_membership[role_id];
+ horizon.projects.remove_user(user_id, role_id, role_list)
+ }
+ else {
+ // search for membership in role lists
+ for (var role in horizon.projects.current_membership) {
+ var role_list = horizon.projects.current_membership[role];
+ horizon.projects.remove_user(user_id, role, role_list)
+ }
+ }
+ },
+
+
+ /*
+ * Adds a given user to a given role list.
+ **/
+ add_user_to_role: function(user_id, role_id) {
+ var role_list = horizon.projects.current_membership[role_id];
+ role_list.push(user_id);
+ horizon.projects.update_role_lists(role_id, role_list)
+ },
+
+
+ /*
+ * Generates the HTML structure for a user that will be displayed
+ * as a list item in the project member list.
+ **/
+ generate_user_element: function(user_name, user_id, text) {
+ var str_id = "id_user_" + user_id;
+
+ var roles = [];
+ for (var r in horizon.projects.roles) {
+ var role = {};
+ role['role_id'] = r;
+ role['role_name'] = horizon.projects.roles[r];
+ roles.push(role);
+ }
+
+ var template = horizon.templates.compiled_templates["#project_user_template"],
+ params = {user_id: str_id,
+ default_role: horizon.projects.roles[horizon.projects.default_role_id],
+ user_name: user_name,
+ text: text,
+ roles: roles},
+ user_el = $(template.render(params));
+ return $(user_el);
+ },
+
+ set_selected_role: function(selected_el, role_id) {
+ $(selected_el).text(horizon.projects.roles[role_id]);
+ $(selected_el).attr('data-role-id', role_id);
+ },
+
+ /*
+ * Generates the HTML structure for the project membership UI.
+ **/
+ generate_html: function() {
+ for (user in horizon.projects.users) {
+ var user_id = user;
+ var user_name = horizon.projects.users[user];
+ var role_id = this.is_project_member(user_id);
+ if (role_id) {
+ $(".project_members").append(this.generate_user_element(user_name, user_id, "-"));
+ var $selected_role = $("li[data-user-id$='" + user_id + "']").siblings('.dropdown').children('.dropdown-toggle').children('span');
+ horizon.projects.set_selected_role($selected_role, role_id)
+ }
+ else {
+ $(".available_users").append(this.generate_user_element(user_name, user_id, "+"));
+ }
+ }
+ horizon.projects.detect_no_results();
+ },
+
+
+ /*
+ * Triggers on click of link to add/remove member from the project.
+ **/
+ update_membership: function() {
+ $(".available_users, .project_members").on('click', ".btn-group a[href='#add_remove']", function (evt) {
+ var available = $(".available_users").has($(this)).length;
+ var user_id = horizon.projects.get_field_id($(this).parent().siblings().attr('data-user-id'))
+
+ if (available) {
+ $(this).text("-");
+ $(this).parent().siblings(".role_options").show();
+ $(".project_members").append($(this).parent().parent());
+
+ horizon.projects.add_user_to_role(user_id, horizon.projects.default_role_id);
+ }
+ else {
+ $(this).text("+");
+ $(this).parent().siblings(".role_options").hide();
+ $(".available_users").append($(this).parent().parent());
+
+ horizon.projects.remove_user_from_role(user_id);
+
+ // set the selection back to default role
+ var $selected_role = $(this).parent().siblings('.dropdown').children('.dropdown-toggle').children('.selected_role');
+ horizon.projects.set_selected_role($selected_role, horizon.projects.default_role_id)
+ }
+
+ // update lists
+ horizon.projects.list_filtering();
+ horizon.projects.detect_no_results();
+
+ // remove input filters
+ $("input.filter").val(horizon.projects.filter_btn_text);
+ });
+ },
+
+
+ /*
+ * Detects whether each list has members and if it does not
+ * displays a message to the user.
+ **/
+ detect_no_results: function () {
+ $('.filterable').each( function () {
+ var filter = $(this).find('ul').attr('class');
+ if (filter == 'project_members')
+ var text = horizon.projects.no_project_members;
+ else
+ var text = horizon.projects.no_available_users;
+
+ if ($('.' + filter).children('ul').length == 0) {
+ $('#no_' + filter).text(text)
+ $('#no_' + filter).show();
+ $("input[id='" + filter + "']").attr('disabled', 'disabled');
+ }
+ else {
+ $('#no_' + filter).hide();
+ $("input[id='" + filter + "']").removeAttr('disabled');
+ }
+ });
+ },
+
+
+ /*
+ * Triggers on selection of new role for a member.
+ **/
+ select_member_role: function() {
+ $(".available_users, .project_members").on('click', '.role_dropdown li', function (evt) {
+ var $selected_el = $(this).parent().prev().children('.selected_role');
+ $selected_el.text($(this).text())
+
+ // get the newly selected role and the member's name
+ var new_role_id = $(this).attr("data-role-id");
+ var id_str = $(this).parent().parent().siblings(".member").attr("data-user-id");
+ var user_id = horizon.projects.get_field_id(id_str);
+
+ // update role lists
+ horizon.projects.remove_user_from_role(user_id, $selected_el.attr('data-role-id'));
+ horizon.projects.add_user_to_role(user_id, new_role_id)
+ });
+ },
+
+
+ /*
+ * Triggers on the addition of a new user via the inline object creation field.
+ **/
+ add_new_user: function() {
+ $("select[id='id_new_user']").on('change', function (evt) {
+ // add the user to the visible list
+ var user_name = $(this).find("option").text();
+ var user_id = $(this).find("option").attr("value");
+ $(".project_members").append(horizon.projects.generate_user_element(user_name, user_id, "-"));
+
+ // add the user to the hidden role lists and the users list
+ horizon.projects.users[user_id] = user_name;
+ $("select[multiple='multiple']").append("")
+ horizon.projects.add_user_to_role(user_id, horizon.projects.default_role_id);
+
+ // remove option from hidden select
+ $(this).text("");
+
+ // reset lists and input filters
+ horizon.projects.list_filtering();
+ horizon.projects.detect_no_results();
+ $("input.filter").val(horizon.projects.filter_btn_text);
+
+ // fix styling
+ $(".project_members .btn-group").css('border-bottom','none')
+ $(".project_members .btn-group:last").css('border-bottom','1px solid #ddd')
+ });
+ },
+
+
+ /*
+ * Style the inline object creation button, hide the associated field.
+ **/
+ add_new_user_styling: function() {
+ var add_user_el = $("label[for='id_new_user']").parent();
+ $(add_user_el).find("select").hide();
+ $("#add_user").append($(add_user_el));
+ $(add_user_el).addClass("add_user");
+ $(add_user_el).find("label, .input").addClass("add_user_btn");
+ },
+
+
+ /*
+ * Fixes the striping of the fake table upon modification of the lists.
+ **/
+ fix_stripes: function() {
+ $('.fake_table').each( function () {
+ var filter = "." + $(this).attr('id');
+ var visible = " .btn-group:visible";
+ var even = " .btn-group:visible:even";
+ var last = " .btn-group:visible:last";
+
+ // fix striping of rows
+ $(filter + visible).css('background-color', 'white');
+ $(filter + even).css('background-color', '#F9F9F9');
+
+ // fix bottom border of new last element
+ $(filter + visible).css('border-bottom','none');
+ $(filter + last).css('border-bottom','1px solid #ddd');
+
+ // fix hovering actions
+ $('.fake_table ul.btn-group').hover(
+ function() {
+ $(this).css('background-color', '#DDD');
+ },
+ function() {
+ $(filter + visible).css('background-color', 'white');
+ $(filter + even).css('background-color', '#F9F9F9');
+ });
+ });
+ },
+
+
+ /*
+ * Sets up filtering for each list of users.
+ **/
+ list_filtering: function () {
+ // remove previous lists' quicksearch events
+ $('input.filter').unbind();
+
+ // set up what happens on focus of input boxes
+ $("input.filter").on('focus', function() {
+ if ($(this).val() === horizon.projects.filter_btn_text) {
+ $(this).val("");
+ }
+ });
+
+ // set up quicksearch to filter on input
+ $('.filterable').each(function () {
+ var filter = $(this).children().children('ul').attr('class');
+ var input = $("input[id='" + filter +"']");
+ input.quicksearch('ul.' + filter + ' ul li span.user_name', {
+ 'delay': 200,
+ 'loader': 'span.loading',
+ 'show': function () {
+ $(this).parent().parent().show();
+ if (filter == "available_users") {
+ $(this).parent('.dropdown-toggle').hide();
+ }
+ },
+ 'hide': function () {
+ $(this).parent().parent().hide();
+ },
+ 'noResults': 'ul#no_' + filter,
+ 'onBefore': function () {
+ $('ul#no_' + filter).text(horizon.projects.no_filter_results);
+ },
+ 'onAfter': function () {
+ horizon.projects.fix_stripes();
+ },
+ 'prepareQuery': function (val) {
+ return new RegExp(val, "i");
+ },
+ 'testQuery': function (query, txt, span) {
+ if ($(input).attr('id') == filter) {
+ $(input).prev().removeAttr('disabled');
+ return query.test($(span).text());
+ }
+ else
+ return true;
+ }
+ });
+ });
+ },
+
+
+ /*
+ * Calls set-up functions upon loading the workflow.
+ **/
+ workflow_init: function(modal) {
+ if (!horizon.projects.workflow_loaded) {
+ $(modal).find('form').each( function () {
+ // call the initalization functions
+ horizon.projects.init_properties();
+ horizon.projects.generate_html();
+ horizon.projects.update_membership();
+ horizon.projects.select_member_role();
+ horizon.projects.add_new_user();
+
+ // initially hide role dropdowns for available users list
+ $(".available_users .role_options").hide();
+
+ // fix the dropdown menu overflow issues
+ $(".tab-content, .workflow").addClass("dropdown_fix");
+
+ // unfocus filter fields
+ $("input").blur();
+
+ // prevent filter inputs from submitting form on 'enter'
+ $('.project_membership').keydown(function(event){
+ if(event.keyCode == 13) {
+ event.preventDefault();
+ return false;
+ }
+ });
+
+ // add filtering + styling to the inline obj creation btn
+ horizon.projects.add_new_user_styling();
+ horizon.projects.list_filtering();
+ horizon.projects.detect_no_results();
+
+ horizon.projects.workflow_loaded = true;
+ });
+ }
+ }
+
+};
+
+
+horizon.addInitFunction(function() {
+ $('.btn').on('click', function (evt) {
+ horizon.projects.workflow_loaded = false;
+ });
+ horizon.modals.addModalInitFunction(horizon.projects.workflow_init);
+});
diff --git a/horizon/static/horizon/js/horizon.templates.js b/horizon/static/horizon/js/horizon.templates.js
index d9660a7d7..314be3c0a 100644
--- a/horizon/static/horizon/js/horizon.templates.js
+++ b/horizon/static/horizon/js/horizon.templates.js
@@ -1,6 +1,6 @@
/* Namespace for core functionality related to client-side templating. */
horizon.templates = {
- template_ids: ["#modal_template", "#empty_row_template", "#alert_message_template", "#spinner-modal"],
+ template_ids: ["#modal_template", "#empty_row_template", "#alert_message_template", "#spinner-modal", "#project_user_template"],
compiled_templates: {}
};
diff --git a/horizon/templates/horizon/_scripts.html b/horizon/templates/horizon/_scripts.html
index def1e1952..0d0b75b69 100644
--- a/horizon/templates/horizon/_scripts.html
+++ b/horizon/templates/horizon/_scripts.html
@@ -30,6 +30,7 @@
+
{% endcompress %}
{% comment %} Client-side Templates (These should *not* be inside the "compress" tag.) {% endcomment %}
diff --git a/horizon/templates/horizon/client_side/_project_user.html b/horizon/templates/horizon/client_side/_project_user.html
new file mode 100644
index 000000000..7552e01e7
--- /dev/null
+++ b/horizon/templates/horizon/client_side/_project_user.html
@@ -0,0 +1,26 @@
+{% extends "horizon/client_side/template.html" %}
+{% load horizon %}
+
+{% block id %}project_user_template{% endblock %}
+
+{% block template %}
+{% jstemplate %}
+
+{% endjstemplate %}
+{% endblock %}
diff --git a/horizon/templates/horizon/client_side/templates.html b/horizon/templates/horizon/client_side/templates.html
index 59aa74652..81fe0852f 100644
--- a/horizon/templates/horizon/client_side/templates.html
+++ b/horizon/templates/horizon/client_side/templates.html
@@ -2,3 +2,4 @@
{% include "horizon/client_side/_table_row.html" %}
{% include "horizon/client_side/_alert_message.html" %}
{% include "horizon/client_side/_loading.html" %}
+{% include "horizon/client_side/_project_user.html" %}
diff --git a/openstack_dashboard/static/dashboard/less/horizon.less b/openstack_dashboard/static/dashboard/less/horizon.less
index cdeb02ff7..87f559459 100644
--- a/openstack_dashboard/static/dashboard/less/horizon.less
+++ b/openstack_dashboard/static/dashboard/less/horizon.less
@@ -818,7 +818,7 @@ td.select {
/* Actions dropdown */
td.actions_column {
- width: 175px;
+ width: 150px;
padding: 10px;
position: relative;
min-width: 140px;
@@ -845,15 +845,6 @@ td.actions_column .row_actions .hide {
display: none;
}
-.modal fieldset .form-field select[data-add-item-url] {
- width: 273px;
-}
-
-select ~ .btn.ajax-add {
- vertical-align: top;
- margin-left: 5px;
-}
-
/* Makes size consistent across browsers when mixing "btn-group" and "small" */
.btn.hide, .btn-group .hide {
display: none;
@@ -882,6 +873,7 @@ select ~ .btn.ajax-add {
.dropdown-menu li.divider:hover {
background-color: #E5E5E5;
}
+
td.actions_column .dropdown-menu a:hover,
td.actions_column .dropdown-menu button:hover {
background-color: #CDCDCD;
@@ -1215,11 +1207,9 @@ label.log-length {
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#c43c35', GradientType=0);
}
-.split_five div.control-group input[type="text"] {
- width: 120px;
-}
+.split_five div.control-group input[type="text"],
.split_five div.control-group select {
- width: 130px;
+ width: 120px;
}
.warning {
@@ -1247,3 +1237,146 @@ label.log-length {
.no_split {
margin-top: -60px;
}
+
+/* Project Membership UI */
+.project_membership {
+ min-height: 200px;
+
+ /* Buttons */
+ .btn-group {
+ margin-left:0px;
+ padding: 2px 10px 0 0;
+ margin-bottom: 0px;
+ border: 1px solid #DDD;
+ border-bottom: none;
+ }
+ .btn-group:last-child {
+ border-bottom: 1px solid #DDD;
+ }
+ .btn-group:nth-child(odd) {
+ background-color: #F9F9F9;
+ }
+ .btn-group .active {
+ float: right;
+ }
+ a.btn-primary:hover {
+ background-color: #04C;
+ }
+
+ /* Header */
+ .help_text {
+ margin-left: 15px;
+ margin-bottom: 15px;
+ }
+ .users_title {
+ color: #555;
+ font-weight: bold;
+ padding-left: 10px;
+ float: left;
+ }
+ input {
+ background: url(/static/dashboard/img/search.png) no-repeat 105px 5px whiteSmoke;
+ }
+ .fake_table_header {
+ background-color: #F1F1F1;
+ width: 306px;
+ height: 38px;
+ padding-top: 15px;
+ border: 1px solid #DDD;
+ border-bottom: none;
+ }
+
+ /* 'Fake table' body */
+ .fake_table {
+ margin-left: 5px;
+ width: 315px;
+ ul.no_results {
+ width: 298px;
+ }
+ ul.btn-group:hover {
+ background-color: #DDD;
+ }
+ }
+ .left {
+ .fake_table_header {
+ width: 318px;
+ }
+ }
+ .right {
+ .fake_table_header {
+ width: 318px;
+ margin-left: -15px;
+ }
+ .fake_table ul.no_results {
+ margin-left: -20px;
+ }
+ }
+
+ /* User lists */
+ .member {
+ padding: 10px;
+ text-align: left;
+ }
+ .project_members {
+ margin-left: -20px;
+ }
+ .project_members ul.btn-group,
+ .available_users ul.btn-group {
+ width: 308px;
+ }
+
+ /* List filtering */
+ .filter {
+ width: 120px;
+ margin: -5px 13px 15px 0px;
+ float: right;
+ }
+ .no_results {
+ border: 1px solid #DDD;
+ padding: 10px;
+ color: #08C;
+ }
+
+ /* Role dropdown menus */
+ .role_dropdown li {
+ cursor: pointer;
+ background: none;
+ float: none;
+ display: block;
+ padding: 5px 10px;
+ color: black;
+ text-align: left;
+ border-radius: 0;
+ border: 0 none;
+ -webkit-box-shadow: none;
+ -moz-box-shadow: none;
+ box-shadow: none;
+ z-index: 99999;
+
+ &:hover {
+ background-color: #CDCDCD;
+ }
+ }
+ .nav .role_options {
+ float: right;
+ padding-right: 5px;
+ }
+}
+
+/* Inline user creation */
+.add_user_btn {
+ display: inline;
+}
+#add_user {
+ clear: both;
+}
+.add_user {
+ float: right;
+ margin-top: 10px;
+ margin-right: 15px;
+}
+
+/* Fixes overflow on dropdowns in modal */
+.dropdown_fix {
+ overflow: visible;
+}