project workflow: project membership UI
* Update Project workflow now includes a tab to update project members * Can add/remove existing users from project and update roles * Can add a new user via inline object creation * Can filter both lists! * Changed css/jquery to make things appear more like other tables * Fixed a few JS bugs * "Fixed" exception handling for now, although it naively redirects the user out of the workflow partially implements blueprint tenant-creation-workflow Change-Id: I38589bf3ee4c33c49df982417d995c141f4e6709
This commit is contained in:
parent
e18be12603
commit
e1635b695d
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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 %}
|
||||
<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 user to add to this project." %}</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-footer %}
|
||||
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Create User" %}" />
|
||||
<a href="{% url horizon:syspanel:projects:index %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
|
||||
{% endblock %}
|
@ -0,0 +1,40 @@
|
||||
{% load i18n %}
|
||||
|
||||
<noscript><h3>{{ step }}</h3></noscript>
|
||||
|
||||
<div class="project_membership">
|
||||
<div class="header">
|
||||
<div class="help_text">{% trans "From here you can add and remove members to this project from the list of all available users." %}</div>
|
||||
<div class="left">
|
||||
<div class="fake_table fake_table_header">
|
||||
<span class="users_title">{% trans "All Users" %}</span>
|
||||
<input type="text" name="available_users_filter" id="available_users" class="filter" value="Filter">
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="fake_table fake_table_header">
|
||||
<span class="users_title">{% trans "Project Members" %}</span>
|
||||
<input type="text" name="project_members_filter" id="project_members" class="filter" value="Filter">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="left filterable">
|
||||
<div class="fake_table" id="available_users">
|
||||
<ul class="available_users"></ul>
|
||||
<ul class="no_results" id="no_available_users"><li>{% trans "No users found." %}</li></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="right filterable">
|
||||
<div class="fake_table" id="project_members">
|
||||
<ul class="project_members"></ul>
|
||||
<ul class="no_results" id="no_project_members"><li>{% trans "No users found." %}</li></ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="add_user"></div>
|
||||
|
||||
<div class="hide">
|
||||
{% include "horizon/common/_form_fields.html" %}
|
||||
</div>
|
@ -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 %}
|
@ -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,
|
||||
['<UpdateProjectInfo: update_info>',
|
||||
'<UpdateProjectMembers: update_members>',
|
||||
'<UpdateProjectQuota: update_quotas>'])
|
||||
|
||||
@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])
|
||||
|
@ -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<tenant_id>[^/]+)/usage/$',
|
||||
TenantUsageView.as_view(), name='usage'),
|
||||
url(r'^(?P<tenant_id>[^/]+)/users/$', UsersView.as_view(), name='users'),
|
||||
url(r'^(?P<tenant_id>[^/]+)/users/(?P<user_id>[^/]+)/add/$',
|
||||
AddUserView.as_view(), name='add_user')
|
||||
url(r'^(?P<tenant_id>[^/]+)/create_user/$',
|
||||
CreateUserView.as_view(), name='create_user'),
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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.'))
|
||||
|
@ -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):
|
||||
|
@ -114,6 +114,7 @@ horizon.addInitFunction(function() {
|
||||
json_data = $.parseJSON(data);
|
||||
field_to_update = $("#" + add_to_field_header);
|
||||
field_to_update.append("<option value='" + json_data[0] + "'>" + json_data[1] + "</option>");
|
||||
field_to_update.change();
|
||||
field_to_update.val(json_data[0]);
|
||||
} else {
|
||||
horizon.modals.success(data, textStatus, jqXHR);
|
||||
|
467
horizon/static/horizon/js/horizon.projects.js
Normal file
467
horizon/static/horizon/js/horizon.projects.js
Normal file
@ -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("<option value='" + user_id + "'>" + horizon.projects.users[user_id] + "</option>")
|
||||
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);
|
||||
});
|
@ -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: {}
|
||||
};
|
||||
|
||||
|
@ -30,6 +30,7 @@
|
||||
<script src='{{ STATIC_URL }}horizon/js/horizon.tabs.js' type='text/javascript' charset='utf-8'></script>
|
||||
<script src='{{ STATIC_URL }}horizon/js/horizon.templates.js' type='text/javascript' charset='utf-8'></script>
|
||||
<script src='{{ STATIC_URL }}horizon/js/horizon.utils.js' type='text/javascript' charset='utf-8'></script>
|
||||
<script src='{{ STATIC_URL }}horizon/js/horizon.projects.js' type='text/javascript' charset='utf-8'></script>
|
||||
{% endcompress %}
|
||||
|
||||
{% comment %} Client-side Templates (These should *not* be inside the "compress" tag.) {% endcomment %}
|
||||
|
26
horizon/templates/horizon/client_side/_project_user.html
Normal file
26
horizon/templates/horizon/client_side/_project_user.html
Normal file
@ -0,0 +1,26 @@
|
||||
{% extends "horizon/client_side/template.html" %}
|
||||
{% load horizon %}
|
||||
|
||||
{% block id %}project_user_template{% endblock %}
|
||||
|
||||
{% block template %}
|
||||
{% jstemplate %}
|
||||
<ul class="nav nav-pills btn-group">
|
||||
<li class="member" data-user-id="[[user_id]]">
|
||||
<span class="user_name">[[user_name]]</span>
|
||||
</li>
|
||||
<li class="active"><a class="btn btn-primary" href="#add_remove">[[text]]</a></li>
|
||||
<li class="dropdown role_options">
|
||||
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
|
||||
<span class="selected_role">[[default_role]]</span>
|
||||
<b class="caret"></b>
|
||||
</a>
|
||||
<ul class="dropdown-menu role_dropdown clearfix">
|
||||
[[#roles]]
|
||||
<li data-role-id="[[role_id]]">[[role_name]]</li>
|
||||
[[/roles]]
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
{% endjstemplate %}
|
||||
{% endblock %}
|
@ -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" %}
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user