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
|
return conn
|
||||||
|
|
||||||
|
|
||||||
def tenant_name(request, tenant_id):
|
|
||||||
return keystoneclient(request).tenants.get(tenant_id).name
|
|
||||||
|
|
||||||
|
|
||||||
def tenant_create(request, tenant_name, description, enabled):
|
def tenant_create(request, tenant_name, description, enabled):
|
||||||
return keystoneclient(request, admin=True).tenants.create(tenant_name,
|
return keystoneclient(request, admin=True).tenants.create(tenant_name,
|
||||||
description,
|
description,
|
||||||
@ -233,6 +229,12 @@ def add_tenant_user_role(request, tenant_id, user_id, role_id):
|
|||||||
tenant_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):
|
def remove_tenant_user(request, tenant_id, user_id):
|
||||||
""" Removes all roles from a user on a tenant, removing them from it. """
|
""" Removes all roles from a user on a tenant, removing them from it. """
|
||||||
client = keystoneclient(request, admin=True)
|
client = keystoneclient(request, admin=True)
|
||||||
|
@ -18,37 +18,16 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
|
|
||||||
from horizon import api
|
from horizon import api
|
||||||
from horizon import exceptions
|
|
||||||
from horizon import forms
|
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())
|
||||||
|
|
||||||
|
def __init__(self, request, *args, **kwargs):
|
||||||
class AddUser(forms.SelfHandlingForm):
|
super(CreateUser, self).__init__(request, *args, **kwargs)
|
||||||
tenant_id = forms.CharField(widget=forms.widgets.HiddenInput())
|
tenant_id = self.request.path.split("/")[-1]
|
||||||
user_id = forms.CharField(widget=forms.widgets.HiddenInput())
|
self.fields['tenant_id'].initial = tenant_id
|
||||||
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.'))
|
|
||||||
|
@ -17,8 +17,14 @@ LOG = logging.getLogger(__name__)
|
|||||||
class ViewMembersLink(tables.LinkAction):
|
class ViewMembersLink(tables.LinkAction):
|
||||||
name = "users"
|
name = "users"
|
||||||
verbose_name = _("Modify Users")
|
verbose_name = _("Modify Users")
|
||||||
url = "horizon:syspanel:projects:users"
|
url = "horizon:syspanel:projects:update"
|
||||||
classes = ("btn-download",)
|
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):
|
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))
|
quota_data[field] = int(getattr(quota, field, None))
|
||||||
return quota_data
|
return quota_data
|
||||||
|
|
||||||
@test.create_stubs({api: ('tenant_get',
|
@test.create_stubs({api: ('get_default_role',
|
||||||
'tenant_quota_get',)})
|
'roles_for_user',
|
||||||
|
'tenant_get',
|
||||||
|
'tenant_quota_get',),
|
||||||
|
api.keystone: ('user_list',
|
||||||
|
'role_list',)})
|
||||||
def test_update_project_get(self):
|
def test_update_project_get(self):
|
||||||
project = self.tenants.first()
|
project = self.tenants.first()
|
||||||
quota = self.quotas.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) \
|
api.tenant_get(IsA(http.HttpRequest), self.tenant.id, admin=True) \
|
||||||
.AndReturn(project)
|
.AndReturn(project)
|
||||||
api.tenant_quota_get(IsA(http.HttpRequest), self.tenant.id) \
|
api.tenant_quota_get(IsA(http.HttpRequest), self.tenant.id) \
|
||||||
.AndReturn(quota)
|
.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()
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
url = reverse('horizon:syspanel:projects:update',
|
url = reverse('horizon:syspanel:projects:update',
|
||||||
@ -224,21 +240,47 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests):
|
|||||||
project.description)
|
project.description)
|
||||||
self.assertQuerysetEqual(workflow.steps,
|
self.assertQuerysetEqual(workflow.steps,
|
||||||
['<UpdateProjectInfo: update_info>',
|
['<UpdateProjectInfo: update_info>',
|
||||||
|
'<UpdateProjectMembers: update_members>',
|
||||||
'<UpdateProjectQuota: update_quotas>'])
|
'<UpdateProjectQuota: update_quotas>'])
|
||||||
|
|
||||||
@test.create_stubs({api: ('tenant_get',
|
@test.create_stubs({api: ('tenant_get',
|
||||||
'tenant_quota_get',
|
'tenant_quota_get',
|
||||||
'tenant_update',
|
'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):
|
def test_update_project_post(self):
|
||||||
project = self.tenants.first()
|
project = self.tenants.first()
|
||||||
quota = self.quotas.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)
|
.AndReturn(project)
|
||||||
api.tenant_quota_get(IsA(http.HttpRequest), project.id) \
|
api.tenant_quota_get(IsA(http.HttpRequest), self.tenant.id) \
|
||||||
.AndReturn(quota)
|
.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
|
# update some fields
|
||||||
project._info["name"] = "updated name"
|
project._info["name"] = "updated name"
|
||||||
project._info["description"] = "updated description"
|
project._info["description"] = "updated description"
|
||||||
@ -251,8 +293,47 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests):
|
|||||||
"enabled": project.enabled}
|
"enabled": project.enabled}
|
||||||
updated_quota = self._get_quota_info(quota)
|
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) \
|
api.tenant_update(IsA(http.HttpRequest), **updated_project) \
|
||||||
.AndReturn(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),
|
api.tenant_quota_update(IsA(http.HttpRequest),
|
||||||
project.id,
|
project.id,
|
||||||
**updated_quota)
|
**updated_quota)
|
||||||
@ -260,10 +341,11 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests):
|
|||||||
self.mox.ReplayAll()
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
# submit form data
|
# submit form data
|
||||||
workflow_data = {"name": project._info["name"],
|
project_data = {"name": project._info["name"],
|
||||||
"id": project.id,
|
"id": project.id,
|
||||||
"description": project._info["description"],
|
"description": project._info["description"],
|
||||||
"enabled": project.enabled}
|
"enabled": project.enabled}
|
||||||
|
workflow_data.update(project_data)
|
||||||
workflow_data.update(updated_quota)
|
workflow_data.update(updated_quota)
|
||||||
url = reverse('horizon:syspanel:projects:update',
|
url = reverse('horizon:syspanel:projects:update',
|
||||||
args=[self.tenant.id])
|
args=[self.tenant.id])
|
||||||
@ -272,14 +354,10 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests):
|
|||||||
self.assertNoFormErrors(res)
|
self.assertNoFormErrors(res)
|
||||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||||
|
|
||||||
@test.create_stubs({api: ('tenant_get',
|
@test.create_stubs({api: ('tenant_get',)})
|
||||||
'tenant_quota_get',)})
|
|
||||||
def test_update_project_get_error(self):
|
def test_update_project_get_error(self):
|
||||||
project = self.tenants.first()
|
|
||||||
|
|
||||||
api.tenant_get(IsA(http.HttpRequest), self.tenant.id, admin=True) \
|
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)
|
.AndRaise(self.exceptions.nova)
|
||||||
|
|
||||||
self.mox.ReplayAll()
|
self.mox.ReplayAll()
|
||||||
@ -292,21 +370,46 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests):
|
|||||||
|
|
||||||
@test.create_stubs({api: ('tenant_get',
|
@test.create_stubs({api: ('tenant_get',
|
||||||
'tenant_quota_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):
|
def test_update_project_tenant_update_error(self):
|
||||||
project = self.tenants.first()
|
project = self.tenants.first()
|
||||||
quota = self.quotas.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)
|
.AndReturn(project)
|
||||||
api.tenant_quota_get(IsA(http.HttpRequest), project.id) \
|
api.tenant_quota_get(IsA(http.HttpRequest), self.tenant.id) \
|
||||||
.AndReturn(quota)
|
.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
|
# update some fields
|
||||||
project._info["name"] = "updated name"
|
project._info["name"] = "updated name"
|
||||||
project._info["description"] = "updated description"
|
project._info["description"] = "updated description"
|
||||||
quota.metadata_items = '444'
|
quota.metadata_items = 444
|
||||||
quota.volumes = '444'
|
quota.volumes = 444
|
||||||
|
|
||||||
updated_project = {"tenant_name": project._info["name"],
|
updated_project = {"tenant_name": project._info["name"],
|
||||||
"tenant_id": project.id,
|
"tenant_id": project.id,
|
||||||
@ -314,16 +417,21 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests):
|
|||||||
"enabled": project.enabled}
|
"enabled": project.enabled}
|
||||||
updated_quota = self._get_quota_info(quota)
|
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) \
|
api.tenant_update(IsA(http.HttpRequest), **updated_project) \
|
||||||
.AndRaise(self.exceptions.keystone)
|
.AndRaise(self.exceptions.keystone)
|
||||||
|
|
||||||
self.mox.ReplayAll()
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
# submit form data
|
# submit form data
|
||||||
workflow_data = {"name": project._info["name"],
|
project_data = {"name": project._info["name"],
|
||||||
"id": project.id,
|
"id": project.id,
|
||||||
"description": project._info["description"],
|
"description": project._info["description"],
|
||||||
"enabled": project.enabled}
|
"enabled": project.enabled}
|
||||||
|
workflow_data.update(project_data)
|
||||||
workflow_data.update(updated_quota)
|
workflow_data.update(updated_quota)
|
||||||
url = reverse('horizon:syspanel:projects:update',
|
url = reverse('horizon:syspanel:projects:update',
|
||||||
args=[self.tenant.id])
|
args=[self.tenant.id])
|
||||||
@ -335,22 +443,46 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests):
|
|||||||
@test.create_stubs({api: ('tenant_get',
|
@test.create_stubs({api: ('tenant_get',
|
||||||
'tenant_quota_get',
|
'tenant_quota_get',
|
||||||
'tenant_update',
|
'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):
|
def test_update_project_quota_update_error(self):
|
||||||
project = self.tenants.first()
|
project = self.tenants.first()
|
||||||
quota = self.quotas.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
|
# get/init
|
||||||
api.tenant_get(IsA(http.HttpRequest), project.id, admin=True) \
|
api.tenant_get(IsA(http.HttpRequest), self.tenant.id, admin=True) \
|
||||||
.AndReturn(project)
|
.AndReturn(project)
|
||||||
api.tenant_quota_get(IsA(http.HttpRequest), project.id) \
|
api.tenant_quota_get(IsA(http.HttpRequest), self.tenant.id) \
|
||||||
.AndReturn(quota)
|
.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
|
# update some fields
|
||||||
project._info["name"] = "updated name"
|
project._info["name"] = "updated name"
|
||||||
project._info["description"] = "updated description"
|
project._info["description"] = "updated description"
|
||||||
quota.metadata_items = '444'
|
quota.metadata_items = 444
|
||||||
quota.volumes = '444'
|
quota.volumes = 444
|
||||||
|
|
||||||
updated_project = {"tenant_name": project._info["name"],
|
updated_project = {"tenant_name": project._info["name"],
|
||||||
"tenant_id": project.id,
|
"tenant_id": project.id,
|
||||||
@ -358,20 +490,159 @@ class UpdateProjectWorkflowTests(test.BaseAdminViewTests):
|
|||||||
"enabled": project.enabled}
|
"enabled": project.enabled}
|
||||||
updated_quota = self._get_quota_info(quota)
|
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) \
|
api.tenant_update(IsA(http.HttpRequest), **updated_project) \
|
||||||
.AndReturn(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),
|
api.tenant_quota_update(IsA(http.HttpRequest),
|
||||||
project.id,
|
project.id,
|
||||||
**updated_quota) \
|
**updated_quota).AndRaise(self.exceptions.nova)
|
||||||
.AndRaise(self.exceptions.nova)
|
|
||||||
|
|
||||||
self.mox.ReplayAll()
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
# submit form data
|
# submit form data
|
||||||
workflow_data = {"name": updated_project["tenant_name"],
|
project_data = {"name": project._info["name"],
|
||||||
"id": project.id,
|
"id": project.id,
|
||||||
"description": updated_project["description"],
|
"description": project._info["description"],
|
||||||
"enabled": project.enabled}
|
"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)
|
workflow_data.update(updated_quota)
|
||||||
url = reverse('horizon:syspanel:projects:update',
|
url = reverse('horizon:syspanel:projects:update',
|
||||||
args=[self.tenant.id])
|
args=[self.tenant.id])
|
||||||
|
@ -20,9 +20,9 @@
|
|||||||
|
|
||||||
from django.conf.urls.defaults import patterns, url
|
from django.conf.urls.defaults import patterns, url
|
||||||
|
|
||||||
from .views import (IndexView, UsersView,
|
from .views import (IndexView, TenantUsageView,
|
||||||
AddUserView, TenantUsageView,
|
CreateProjectView, UpdateProjectView,
|
||||||
CreateProjectView, UpdateProjectView)
|
CreateUserView)
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
urlpatterns = patterns('',
|
||||||
@ -32,7 +32,6 @@ urlpatterns = patterns('',
|
|||||||
UpdateProjectView.as_view(), name='update'),
|
UpdateProjectView.as_view(), name='update'),
|
||||||
url(r'^(?P<tenant_id>[^/]+)/usage/$',
|
url(r'^(?P<tenant_id>[^/]+)/usage/$',
|
||||||
TenantUsageView.as_view(), name='usage'),
|
TenantUsageView.as_view(), name='usage'),
|
||||||
url(r'^(?P<tenant_id>[^/]+)/users/$', UsersView.as_view(), name='users'),
|
url(r'^(?P<tenant_id>[^/]+)/create_user/$',
|
||||||
url(r'^(?P<tenant_id>[^/]+)/users/(?P<user_id>[^/]+)/add/$',
|
CreateUserView.as_view(), name='create_user'),
|
||||||
AddUserView.as_view(), name='add_user')
|
|
||||||
)
|
)
|
||||||
|
@ -19,19 +19,18 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import operator
|
|
||||||
|
|
||||||
from django.core.urlresolvers import reverse, reverse_lazy
|
from django.core.urlresolvers import reverse, reverse_lazy
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from horizon import api
|
from horizon import api
|
||||||
from horizon import exceptions
|
from horizon import exceptions
|
||||||
from horizon import forms
|
|
||||||
from horizon import tables
|
from horizon import tables
|
||||||
from horizon import usage
|
from horizon import usage
|
||||||
from horizon import workflows
|
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 .tables import TenantsTable, TenantUsersTable, AddUsersTable
|
||||||
from .workflows import CreateProject, UpdateProject
|
from .workflows import CreateProject, UpdateProject
|
||||||
|
|
||||||
@ -52,6 +51,8 @@ PROJECT_INFO_FIELDS = ("name",
|
|||||||
"description",
|
"description",
|
||||||
"enabled")
|
"enabled")
|
||||||
|
|
||||||
|
INDEX_URL = "horizon:syspanel:projects:index"
|
||||||
|
|
||||||
|
|
||||||
class TenantContextMixin(object):
|
class TenantContextMixin(object):
|
||||||
def get_object(self):
|
def get_object(self):
|
||||||
@ -62,10 +63,9 @@ class TenantContextMixin(object):
|
|||||||
tenant_id,
|
tenant_id,
|
||||||
admin=True)
|
admin=True)
|
||||||
except:
|
except:
|
||||||
redirect = reverse("horizon:syspanel:projects:index")
|
|
||||||
exceptions.handle(self.request,
|
exceptions.handle(self.request,
|
||||||
_('Unable to retrieve project information.'),
|
_('Unable to retrieve project information.'),
|
||||||
redirect=redirect)
|
redirect=reverse(INDEX_URL))
|
||||||
return self._object
|
return self._object
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
@ -106,10 +106,9 @@ class UsersView(tables.MultiTableView):
|
|||||||
'all_users': all_users,
|
'all_users': all_users,
|
||||||
'tenant_users': tenant_users}
|
'tenant_users': tenant_users}
|
||||||
except:
|
except:
|
||||||
redirect = reverse("horizon:syspanel:projects:index")
|
|
||||||
exceptions.handle(self.request,
|
exceptions.handle(self.request,
|
||||||
_("Unable to retrieve users."),
|
_("Unable to retrieve users."),
|
||||||
redirect=redirect)
|
redirect=reverse(INDEX_URL))
|
||||||
return self._shared_data
|
return self._shared_data
|
||||||
|
|
||||||
def get_tenant_users_data(self):
|
def get_tenant_users_data(self):
|
||||||
@ -127,42 +126,6 @@ class UsersView(tables.MultiTableView):
|
|||||||
return context
|
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):
|
class TenantUsageView(usage.UsageView):
|
||||||
table_class = usage.TenantUsageTable
|
table_class = usage.TenantUsageTable
|
||||||
usage_class = usage.TenantUsage
|
usage_class = usage.TenantUsage
|
||||||
@ -215,8 +178,26 @@ class UpdateProjectView(workflows.WorkflowView):
|
|||||||
for field in QUOTA_FIELDS:
|
for field in QUOTA_FIELDS:
|
||||||
initial[field] = getattr(quota_data, field, None)
|
initial[field] = getattr(quota_data, field, None)
|
||||||
except:
|
except:
|
||||||
redirect = reverse("horizon:syspanel:projects:index")
|
|
||||||
exceptions.handle(self.request,
|
exceptions.handle(self.request,
|
||||||
_('Unable to retrieve project details.'),
|
_('Unable to retrieve project details.'),
|
||||||
redirect=redirect)
|
redirect=reverse(INDEX_URL))
|
||||||
return initial
|
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.
|
# under the License.
|
||||||
|
|
||||||
|
|
||||||
from django import forms
|
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
|
||||||
from horizon import api
|
from horizon import api
|
||||||
from horizon import exceptions
|
from horizon import exceptions
|
||||||
from horizon import workflows
|
from horizon import workflows
|
||||||
|
from horizon import forms
|
||||||
|
|
||||||
|
|
||||||
class UpdateProjectQuotaAction(workflows.Action):
|
class UpdateProjectQuotaAction(workflows.Action):
|
||||||
@ -146,6 +147,96 @@ class UpdateProjectInfo(workflows.Step):
|
|||||||
"enabled")
|
"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):
|
class UpdateProject(workflows.Workflow):
|
||||||
slug = "update_project"
|
slug = "update_project"
|
||||||
name = _("Edit Project")
|
name = _("Edit Project")
|
||||||
@ -154,17 +245,18 @@ class UpdateProject(workflows.Workflow):
|
|||||||
failure_message = _('Unable to modify project "%s".')
|
failure_message = _('Unable to modify project "%s".')
|
||||||
success_url = "horizon:syspanel:projects:index"
|
success_url = "horizon:syspanel:projects:index"
|
||||||
default_steps = (UpdateProjectInfo,
|
default_steps = (UpdateProjectInfo,
|
||||||
|
UpdateProjectMembers,
|
||||||
UpdateProjectQuota)
|
UpdateProjectQuota)
|
||||||
|
|
||||||
def format_status_message(self, message):
|
def format_status_message(self, message):
|
||||||
return message % self.context.get('name', 'unknown project')
|
return message % self.context.get('name', 'unknown project')
|
||||||
|
|
||||||
def handle(self, request, data):
|
def handle(self, request, data):
|
||||||
|
project_id = data['project_id']
|
||||||
# update project info
|
# update project info
|
||||||
try:
|
try:
|
||||||
api.tenant_update(request,
|
api.tenant_update(request,
|
||||||
tenant_id=data['project_id'],
|
tenant_id=project_id,
|
||||||
tenant_name=data['name'],
|
tenant_name=data['name'],
|
||||||
description=data['description'],
|
description=data['description'],
|
||||||
enabled=data['enabled'])
|
enabled=data['enabled'])
|
||||||
@ -172,11 +264,64 @@ class UpdateProject(workflows.Workflow):
|
|||||||
exceptions.handle(request, ignore=True)
|
exceptions.handle(request, ignore=True)
|
||||||
return False
|
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
|
# update the project quota
|
||||||
ifcb = data['injected_file_content_bytes']
|
ifcb = data['injected_file_content_bytes']
|
||||||
try:
|
try:
|
||||||
api.tenant_quota_update(request,
|
api.tenant_quota_update(request,
|
||||||
data['project_id'],
|
project_id,
|
||||||
metadata_items=data['metadata_items'],
|
metadata_items=data['metadata_items'],
|
||||||
injected_file_content_bytes=ifcb,
|
injected_file_content_bytes=ifcb,
|
||||||
volumes=data['volumes'],
|
volumes=data['volumes'],
|
||||||
@ -188,6 +333,7 @@ class UpdateProject(workflows.Workflow):
|
|||||||
cores=data['cores'])
|
cores=data['cores'])
|
||||||
return True
|
return True
|
||||||
except:
|
except:
|
||||||
exceptions.handle(request, _('Modified project information, but'
|
exceptions.handle(request, _('Modified project information and '
|
||||||
'unable to modify project quotas.'))
|
'members, but unable to modify '
|
||||||
|
'project quotas.'))
|
||||||
return True
|
return True
|
||||||
|
@ -94,14 +94,16 @@ class CreateUserForm(BaseUserForm):
|
|||||||
messages.success(request,
|
messages.success(request,
|
||||||
_('User "%s" was successfully created.')
|
_('User "%s" was successfully created.')
|
||||||
% data['name'])
|
% data['name'])
|
||||||
try:
|
if data['role_id']:
|
||||||
api.add_tenant_user_role(request,
|
try:
|
||||||
data['tenant_id'],
|
api.add_tenant_user_role(request,
|
||||||
new_user.id,
|
data['tenant_id'],
|
||||||
data['role_id'])
|
new_user.id,
|
||||||
except:
|
data['role_id'])
|
||||||
exceptions.handle(request,
|
except:
|
||||||
_('Unable to add user to primary project.'))
|
exceptions.handle(request,
|
||||||
|
_('Unable to add user'
|
||||||
|
'to primary project.'))
|
||||||
return new_user
|
return new_user
|
||||||
except:
|
except:
|
||||||
exceptions.handle(request, _('Unable to create user.'))
|
exceptions.handle(request, _('Unable to create user.'))
|
||||||
|
@ -35,7 +35,11 @@ class DynamicSelectWidget(widgets.Select):
|
|||||||
if callable(self.add_item_link):
|
if callable(self.add_item_link):
|
||||||
return self.add_item_link()
|
return self.add_item_link()
|
||||||
try:
|
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:
|
except urlresolvers.NoReverseMatch:
|
||||||
return self.add_item_link
|
return self.add_item_link
|
||||||
|
|
||||||
@ -51,9 +55,14 @@ class DynamicChoiceField(fields.ChoiceField):
|
|||||||
"""
|
"""
|
||||||
widget = DynamicSelectWidget
|
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)
|
super(DynamicChoiceField, self).__init__(*args, **kwargs)
|
||||||
self.widget.add_item_link = add_item_link
|
self.widget.add_item_link = add_item_link
|
||||||
|
self.widget.add_item_link_args = add_item_link_args
|
||||||
|
|
||||||
|
|
||||||
class DynamicTypedChoiceField(DynamicChoiceField, fields.TypedChoiceField):
|
class DynamicTypedChoiceField(DynamicChoiceField, fields.TypedChoiceField):
|
||||||
|
@ -114,6 +114,7 @@ horizon.addInitFunction(function() {
|
|||||||
json_data = $.parseJSON(data);
|
json_data = $.parseJSON(data);
|
||||||
field_to_update = $("#" + add_to_field_header);
|
field_to_update = $("#" + add_to_field_header);
|
||||||
field_to_update.append("<option value='" + json_data[0] + "'>" + json_data[1] + "</option>");
|
field_to_update.append("<option value='" + json_data[0] + "'>" + json_data[1] + "</option>");
|
||||||
|
field_to_update.change();
|
||||||
field_to_update.val(json_data[0]);
|
field_to_update.val(json_data[0]);
|
||||||
} else {
|
} else {
|
||||||
horizon.modals.success(data, textStatus, jqXHR);
|
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. */
|
/* Namespace for core functionality related to client-side templating. */
|
||||||
horizon.templates = {
|
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: {}
|
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.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.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.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 %}
|
{% endcompress %}
|
||||||
|
|
||||||
{% comment %} Client-side Templates (These should *not* be inside the "compress" tag.) {% endcomment %}
|
{% 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/_table_row.html" %}
|
||||||
{% include "horizon/client_side/_alert_message.html" %}
|
{% include "horizon/client_side/_alert_message.html" %}
|
||||||
{% include "horizon/client_side/_loading.html" %}
|
{% include "horizon/client_side/_loading.html" %}
|
||||||
|
{% include "horizon/client_side/_project_user.html" %}
|
||||||
|
@ -818,7 +818,7 @@ td.select {
|
|||||||
/* Actions dropdown */
|
/* Actions dropdown */
|
||||||
|
|
||||||
td.actions_column {
|
td.actions_column {
|
||||||
width: 175px;
|
width: 150px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
position: relative;
|
position: relative;
|
||||||
min-width: 140px;
|
min-width: 140px;
|
||||||
@ -845,15 +845,6 @@ td.actions_column .row_actions .hide {
|
|||||||
display: none;
|
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" */
|
/* Makes size consistent across browsers when mixing "btn-group" and "small" */
|
||||||
.btn.hide, .btn-group .hide {
|
.btn.hide, .btn-group .hide {
|
||||||
display: none;
|
display: none;
|
||||||
@ -882,6 +873,7 @@ select ~ .btn.ajax-add {
|
|||||||
.dropdown-menu li.divider:hover {
|
.dropdown-menu li.divider:hover {
|
||||||
background-color: #E5E5E5;
|
background-color: #E5E5E5;
|
||||||
}
|
}
|
||||||
|
|
||||||
td.actions_column .dropdown-menu a:hover,
|
td.actions_column .dropdown-menu a:hover,
|
||||||
td.actions_column .dropdown-menu button:hover {
|
td.actions_column .dropdown-menu button:hover {
|
||||||
background-color: #CDCDCD;
|
background-color: #CDCDCD;
|
||||||
@ -1215,11 +1207,9 @@ label.log-length {
|
|||||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#c43c35', GradientType=0);
|
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#c43c35', GradientType=0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.split_five div.control-group input[type="text"] {
|
.split_five div.control-group input[type="text"],
|
||||||
width: 120px;
|
|
||||||
}
|
|
||||||
.split_five div.control-group select {
|
.split_five div.control-group select {
|
||||||
width: 130px;
|
width: 120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.warning {
|
.warning {
|
||||||
@ -1247,3 +1237,146 @@ label.log-length {
|
|||||||
.no_split {
|
.no_split {
|
||||||
margin-top: -60px;
|
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