diff --git a/horizon/api/keystone.py b/horizon/api/keystone.py index dbf614945..aca6a0350 100644 --- a/horizon/api/keystone.py +++ b/horizon/api/keystone.py @@ -123,10 +123,6 @@ def keystoneclient(request, admin=False): return conn -def tenant_name(request, tenant_id): - return keystoneclient(request).tenants.get(tenant_id).name - - def tenant_create(request, tenant_name, description, enabled): return keystoneclient(request, admin=True).tenants.create(tenant_name, description, @@ -233,6 +229,12 @@ def add_tenant_user_role(request, tenant_id, user_id, role_id): tenant_id) +def remove_tenant_user_role(request, tenant_id, user_id, role_id): + """ Removes a given single role for a user from a tenant. """ + client = keystoneclient(request, admin=True) + client.roles.remove_user_role(user_id, role_id, tenant_id) + + def remove_tenant_user(request, tenant_id, user_id): """ Removes all roles from a user on a tenant, removing them from it. """ client = keystoneclient(request, admin=True) diff --git a/horizon/dashboards/syspanel/projects/forms.py b/horizon/dashboards/syspanel/projects/forms.py index 8ed56a666..ebccf2416 100644 --- a/horizon/dashboards/syspanel/projects/forms.py +++ b/horizon/dashboards/syspanel/projects/forms.py @@ -18,37 +18,16 @@ # License for the specific language governing permissions and limitations # under the License. -import logging - -from django.utils.translation import ugettext_lazy as _ - from horizon import api -from horizon import exceptions from horizon import forms -from horizon import messages +from horizon.dashboards.syspanel.users.forms import CreateUserForm -LOG = logging.getLogger(__name__) +class CreateUser(CreateUserForm): + role_id = forms.ChoiceField(widget=forms.HiddenInput()) + tenant_id = forms.CharField(widget=forms.HiddenInput()) - -class AddUser(forms.SelfHandlingForm): - tenant_id = forms.CharField(widget=forms.widgets.HiddenInput()) - user_id = forms.CharField(widget=forms.widgets.HiddenInput()) - role_id = forms.ChoiceField(label=_("Role")) - - def __init__(self, *args, **kwargs): - roles = kwargs.pop('roles') - super(AddUser, self).__init__(*args, **kwargs) - role_choices = [(role.id, role.name) for role in roles] - self.fields['role_id'].choices = role_choices - - def handle(self, request, data): - try: - api.add_tenant_user_role(request, - data['tenant_id'], - data['user_id'], - data['role_id']) - messages.success(request, _('Successfully added user to project.')) - return True - except: - exceptions.handle(request, _('Unable to add user to project.')) + def __init__(self, request, *args, **kwargs): + super(CreateUser, self).__init__(request, *args, **kwargs) + tenant_id = self.request.path.split("/")[-1] + self.fields['tenant_id'].initial = tenant_id diff --git a/horizon/dashboards/syspanel/projects/tables.py b/horizon/dashboards/syspanel/projects/tables.py index fdf4f4e23..ddc2ea8ce 100644 --- a/horizon/dashboards/syspanel/projects/tables.py +++ b/horizon/dashboards/syspanel/projects/tables.py @@ -17,8 +17,14 @@ LOG = logging.getLogger(__name__) class ViewMembersLink(tables.LinkAction): name = "users" verbose_name = _("Modify Users") - url = "horizon:syspanel:projects:users" - classes = ("btn-download",) + url = "horizon:syspanel:projects:update" + classes = ("ajax-modal", "btn-edit") + + def get_link_url(self, project): + step = 'update_members' + base_url = reverse(self.url, args=[project.id]) + param = urlencode({"step": step}) + return "?".join([base_url, param]) class UsageLink(tables.LinkAction): diff --git a/horizon/dashboards/syspanel/projects/templates/projects/_create_user.html b/horizon/dashboards/syspanel/projects/templates/projects/_create_user.html new file mode 100644 index 000000000..bf2005f60 --- /dev/null +++ b/horizon/dashboards/syspanel/projects/templates/projects/_create_user.html @@ -0,0 +1,25 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block form_id %}create_user_form{% endblock %} +{% block form_action %}{% url horizon:syspanel:projects:create_user tenant_id %}{% endblock %} + +{% block modal-header %}{% blocktrans %}Create User for project '{{ tenant_name }}'.{% endblocktrans %}{% endblock %} + + +{% block modal-body %} +
+
+ {% include "horizon/common/_form_fields.html" %} +
+
+
+

{% trans "Description" %}:

+

{% trans "From here you can create a new user to add to this project." %}

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