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:
Kelsey Tripp 2012-07-27 12:31:06 -07:00
parent e18be12603
commit e1635b695d
19 changed files with 1245 additions and 144 deletions

View File

@ -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)

View File

@ -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

View File

@ -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):

View File

@ -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 %}

View File

@ -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>

View File

@ -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 %}

View File

@ -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])

View File

@ -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'),
)

View File

@ -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

View File

@ -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

View File

@ -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.'))

View File

@ -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):

View File

@ -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);

View 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);
});

View File

@ -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: {}
};

View File

@ -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 %}

View 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 %}

View File

@ -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" %}

View File

@ -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;
}