diff --git a/docs/source/conf.py b/docs/source/conf.py index f45c86b2f31..97faa55107d 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -11,7 +11,8 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys, os +import sys +import os BASE_DIR = os.path.dirname(os.path.abspath(__file__)) HORIZON_DIR = os.path.abspath(os.path.join(BASE_DIR, "..", "..", "horizon")) diff --git a/horizon/horizon/context_processors.py b/horizon/horizon/context_processors.py index c2c582aa43b..8a35fbeb4d4 100644 --- a/horizon/horizon/context_processors.py +++ b/horizon/horizon/context_processors.py @@ -75,10 +75,12 @@ def horizon(request): context['network_configured'] = getattr(settings, 'QUANTUM_ENABLED', None) # Region context/support - available_regions = getattr(settings, 'AVAILABLE_REGIONS', None) - regions = {'support': available_regions > 1, - 'endpoint': request.session.get('region_endpoint'), - 'name': request.session.get('region_name')} - context['region'] = regions + available_regions = getattr(settings, 'AVAILABLE_REGIONS', []) + regions = {'support': len(available_regions) > 1, + 'current': {'endpoint': request.session.get('region_endpoint'), + 'name': request.session.get('region_name')}, + 'available': [{'endpoint': region[0], 'name':region[1]} for + region in available_regions]} + context['regions'] = regions return context diff --git a/horizon/horizon/site_urls.py b/horizon/horizon/site_urls.py index 91b06541074..291c537417a 100644 --- a/horizon/horizon/site_urls.py +++ b/horizon/horizon/site_urls.py @@ -20,10 +20,12 @@ from django.conf.urls.defaults import patterns, url, include +from horizon.views.auth import LoginView + urlpatterns = patterns('horizon.views.auth', url(r'home/$', 'user_home', name='user_home'), - url(r'auth/login/$', 'login', name='auth_login'), + url(r'auth/login/$', LoginView.as_view(), name='auth_login'), url(r'auth/logout/$', 'logout', name='auth_logout'), url(r'auth/switch/(?P[^/]+)/$', 'switch_tenants', name='auth_switch')) diff --git a/horizon/horizon/templates/horizon/auth/_login.html b/horizon/horizon/templates/horizon/auth/_login.html index 0dafd6c0ff3..7ffe007de21 100644 --- a/horizon/horizon/templates/horizon/auth/_login.html +++ b/horizon/horizon/templates/horizon/auth/_login.html @@ -2,9 +2,9 @@ {% load i18n %} {% block modal-header %}Log In{% endblock %} -{% block modal_class %}modal{% endblock %} +{% block modal_class %}modal login{% endblock %} -{% block form-action %}{% url horizon:auth_login %}{% endblock %} +{% block form_action %}{% url horizon:auth_login %}{% endblock %} {% block modal-body %} {% include "horizon/_messages.html" %} diff --git a/horizon/horizon/templates/horizon/auth/login.html b/horizon/horizon/templates/horizon/auth/login.html new file mode 100644 index 00000000000..bb6ae668a13 --- /dev/null +++ b/horizon/horizon/templates/horizon/auth/login.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block title %}{% trans "Login" %}{% endblock %} + +{% block body_id %}splash{% endblock %} + +{% block content %} + {% include '_login.html' %} +{% endblock %} diff --git a/horizon/horizon/templates/horizon/common/_region_selector.html b/horizon/horizon/templates/horizon/common/_region_selector.html index e301511c4dd..240acf83e78 100644 --- a/horizon/horizon/templates/horizon/common/_region_selector.html +++ b/horizon/horizon/templates/horizon/common/_region_selector.html @@ -1,11 +1,13 @@ -{% if region.support %} +{% if regions.support %} {% endif %} diff --git a/horizon/horizon/tests/auth_tests.py b/horizon/horizon/tests/auth_tests.py index cbe4dab65ea..50325d34a65 100644 --- a/horizon/horizon/tests/auth_tests.py +++ b/horizon/horizon/tests/auth_tests.py @@ -21,6 +21,7 @@ from django import http from django.contrib import messages from django.core.urlresolvers import reverse +from keystoneclient.v2_0 import tenants as keystone_tenants from keystoneclient import exceptions as keystone_exceptions from mox import IsA @@ -37,25 +38,29 @@ def setUp(self): super(AuthViewTests, self).setUp() self.setActiveUser() self.PASSWORD = 'secret' + self.tenant = keystone_tenants.Tenant(keystone_tenants.TenantManager, + {'id': '6', + 'name': 'FAKENAME'}) + self.tenants = [self.tenant] def test_login_index(self): res = self.client.get(reverse('horizon:auth_login')) - self.assertTemplateUsed(res, 'splash.html') + self.assertTemplateUsed(res, 'horizon/auth/login.html') def test_login_user_logged_in(self): self.setActiveUser(self.TEST_TOKEN, self.TEST_USER, self.TEST_TENANT, False, self.TEST_SERVICE_CATALOG) + # Hitting the login URL directly should always give you a login page. res = self.client.get(reverse('horizon:auth_login')) - self.assertRedirectsNoFollow(res, DASH_INDEX_URL) + self.assertTemplateUsed(res, 'horizon/auth/login.html') def test_login_no_tenants(self): - NEW_TENANT_ID = '6' - NEW_TENANT_NAME = 'FAKENAME' + TOKEN_ID = 1 form_data = {'method': 'Login', - 'region': 'http://localhost:5000/v2.0,local', + 'region': 'http://localhost:5000/v2.0', 'password': self.PASSWORD, 'username': self.TEST_USER} @@ -70,10 +75,6 @@ class FakeToken(object): api.token_create(IsA(http.HttpRequest), "", self.TEST_USER, self.PASSWORD).AndReturn(aToken) - aTenant = self.mox.CreateMock(api.Token) - aTenant.id = NEW_TENANT_ID - aTenant.name = NEW_TENANT_NAME - self.mox.StubOutWithMock(api, 'tenant_list_for_token') api.tenant_list_for_token(IsA(http.HttpRequest), aToken.id).\ AndReturn([]) @@ -87,46 +88,39 @@ class FakeToken(object): res = self.client.post(reverse('horizon:auth_login'), form_data) - self.assertTemplateUsed(res, 'splash.html') + self.assertTemplateUsed(res, 'horizon/auth/login.html') def test_login(self): - NEW_TENANT_ID = '6' - NEW_TENANT_NAME = 'FAKENAME' - TOKEN_ID = 1 - form_data = {'method': 'Login', - 'region': 'http://localhost:5000/v2.0,local', - 'password': self.PASSWORD, - 'username': self.TEST_USER} + 'region': 'http://localhost:5000/v2.0', + 'password': self.PASSWORD, + 'username': self.TEST_USER} self.mox.StubOutWithMock(api, 'token_create') + self.mox.StubOutWithMock(api, 'tenant_list_for_token') + self.mox.StubOutWithMock(api, 'token_create_scoped') class FakeToken(object): - id = TOKEN_ID, + id = 1, user = {"id": "1", "roles": [{"id": "1", "name": "fake"}], "name": "user"} serviceCatalog = {} tenant = None + aToken = api.Token(FakeToken()) bToken = aToken + bToken.tenant = {'id': self.tenant.id, 'name': self.tenant.name} api.token_create(IsA(http.HttpRequest), "", self.TEST_USER, self.PASSWORD).AndReturn(aToken) - - aTenant = self.mox.CreateMock(api.Token) - aTenant.id = NEW_TENANT_ID - aTenant.name = NEW_TENANT_NAME - bToken.tenant = {'id': aTenant.id, 'name': aTenant.name} - - self.mox.StubOutWithMock(api, 'tenant_list_for_token') - api.tenant_list_for_token(IsA(http.HttpRequest), aToken.id).\ - AndReturn([aTenant]) - - self.mox.StubOutWithMock(api, 'token_create_scoped') - api.token_create_scoped(IsA(http.HttpRequest), aTenant.id, - aToken.id).AndReturn(bToken) + api.tenant_list_for_token(IsA(http.HttpRequest), + aToken.id).AndReturn(self.tenants) + api.token_create_scoped(IsA(http.HttpRequest), + self.tenant.id, + aToken.id).AndReturn(bToken) self.mox.ReplayAll() + res = self.client.post(reverse('horizon:auth_login'), form_data) self.assertRedirectsNoFollow(res, DASH_INDEX_URL) @@ -139,14 +133,14 @@ def test_login_invalid_credentials(self): self.mox.ReplayAll() form_data = {'method': 'Login', - 'region': 'http://localhost:5000/v2.0,local', + 'region': 'http://localhost:5000/v2.0', 'password': self.PASSWORD, 'username': self.TEST_USER} res = self.client.post(reverse('horizon:auth_login'), form_data, follow=True) - self.assertTemplateUsed(res, 'splash.html') + self.assertTemplateUsed(res, 'horizon/auth/login.html') def test_login_exception(self): self.mox.StubOutWithMock(api, 'token_create') @@ -159,12 +153,12 @@ def test_login_exception(self): self.mox.ReplayAll() form_data = {'method': 'Login', - 'region': 'http://localhost:5000/v2.0,local', + 'region': 'http://localhost:5000/v2.0', 'password': self.PASSWORD, 'username': self.TEST_USER} res = self.client.post(reverse('horizon:auth_login'), form_data) - self.assertTemplateUsed(res, 'splash.html') + self.assertTemplateUsed(res, 'horizon/auth/login.html') def test_switch_tenants_index(self): res = self.client.get(reverse('horizon:auth_switch', @@ -207,7 +201,7 @@ def test_switch_tenants(self): self.mox.ReplayAll() form_data = {'method': 'LoginWithTenant', - 'region': 'http://localhost:5000/v2.0,local', + 'region': 'http://localhost:5000/v2.0', 'password': self.PASSWORD, 'tenant': NEW_TENANT_ID, 'username': self.TEST_USER} diff --git a/horizon/horizon/tests/testsettings.py b/horizon/horizon/tests/testsettings.py index 1e3f9f91594..771ed3f1af7 100644 --- a/horizon/horizon/tests/testsettings.py +++ b/horizon/horizon/tests/testsettings.py @@ -101,8 +101,8 @@ SWIFT_AUTHURL = 'http://swift/swiftapi/v1.0' AVAILABLE_REGIONS = [ - ('local', 'http://localhost:5000/v2.0'), - ('remote', 'http://remote:5000/v2.0'), + ('http://localhost:5000/v2.0', 'local'), + ('http://remote:5000/v2.0', 'remote'), ] OPENSTACK_ADDRESS = "localhost" diff --git a/horizon/horizon/views/auth.py b/horizon/horizon/views/auth.py index 82bc40ac60d..a8695b13205 100644 --- a/horizon/horizon/views/auth.py +++ b/horizon/horizon/views/auth.py @@ -27,6 +27,7 @@ import horizon from horizon import api from horizon import exceptions +from horizon import forms from horizon import users from horizon.base import Horizon from horizon.views.auth_forms import Login, LoginWithTenant, _set_session_data @@ -40,21 +41,22 @@ def user_home(request): return shortcuts.redirect(horizon.get_user_home(request.user)) -def login(request): +class LoginView(forms.ModalFormView): """ Logs in a user and redirects them to the URL specified by :func:`horizon.get_user_home`. """ - if request.user.is_authenticated(): - user = users.User(users.get_user_from_request(request)) - return shortcuts.redirect(Horizon.get_user_home(user)) - - form, handled = Login.maybe_handle(request) - if handled: - return handled - - # FIXME(gabriel): we don't ship a template named splash.html - return shortcuts.render(request, 'splash.html', {'form': form}) + form_class = Login + template_name = "horizon/auth/login.html" + + def get_initial(self): + initial = super(LoginView, self).get_initial() + current_region = self.request.session.get('region_endpoint', None) + requested_region = self.request.GET.get('region', None) + regions = dict(getattr(settings, "AVAILABLE_REGIONS", [])) + if requested_region in regions and requested_region != current_region: + initial.update({'region': requested_region}) + return initial def switch_tenants(request, tenant_id): diff --git a/horizon/horizon/views/auth_forms.py b/horizon/horizon/views/auth_forms.py index cb52d660641..3a04718c221 100644 --- a/horizon/horizon/views/auth_forms.py +++ b/horizon/horizon/views/auth_forms.py @@ -50,16 +50,6 @@ def _set_session_data(request, token): request.session['roles'] = token.user['roles'] -def _regions_supported(): - if len(getattr(settings, 'AVAILABLE_REGIONS', [])) > 1: - return True - - -region_field = forms.ChoiceField(widget=forms.Select, - choices=[('%s,%s' % (region[1], region[0]), region[0]) - for region in getattr(settings, 'AVAILABLE_REGIONS', [])]) - - class Login(forms.SelfHandlingForm): """ Form used for logging in a user. @@ -69,17 +59,27 @@ class Login(forms.SelfHandlingForm): Subclass of :class:`~horizon.forms.SelfHandlingForm`. """ - if _regions_supported(): - region = region_field + region = forms.ChoiceField(label=_("Region")) username = forms.CharField(max_length="20", label=_("User Name")) password = forms.CharField(max_length="20", label=_("Password"), widget=forms.PasswordInput(render_value=False)) + def __init__(self, *args, **kwargs): + super(Login, self).__init__(*args, **kwargs) + # FIXME(gabriel): When we switch to region-only settings, we can + # remove this default region business. + default_region = (settings.OPENSTACK_KEYSTONE_URL, "Default Region") + regions = getattr(settings, 'AVAILABLE_REGIONS', [default_region]) + self.fields['region'].choices = regions + if len(regions) == 1: + self.fields['region'].initial = default_region[0] + self.fields['region'].widget = forms.widgets.HiddenInput() + def handle(self, request, data): - region = data.get('region', '').split(',') - if len(region) > 1: - request.session['region_endpoint'] = region[0] - request.session['region_name'] = region[1] + endpoint = data.get('region') + region_name = dict(self.fields['region'].choices)[endpoint] + request.session['region_endpoint'] = endpoint + request.session['region_name'] = region_name if data.get('tenant', None): try: @@ -163,8 +163,7 @@ class LoginWithTenant(Login): Exactly like :class:`.Login` but includes the tenant id as a field so that the process of choosing a default tenant is bypassed. """ - if _regions_supported(): - region = region_field + region = forms.ChoiceField(required=False) username = forms.CharField(max_length="20", widget=forms.TextInput(attrs={'readonly': 'readonly'})) tenant = forms.CharField(widget=forms.HiddenInput()) diff --git a/openstack-dashboard/dashboard/static/dashboard/css/style.css b/openstack-dashboard/dashboard/static/dashboard/css/style.css index dc63f9c94db..de64cf46c82 100644 --- a/openstack-dashboard/dashboard/static/dashboard/css/style.css +++ b/openstack-dashboard/dashboard/static/dashboard/css/style.css @@ -684,6 +684,9 @@ td.actions_column .row_actions .hide { .dropdown-menu li:hover { background: none; } +.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; diff --git a/openstack-dashboard/dashboard/templates/base.html b/openstack-dashboard/dashboard/templates/base.html index bf4e619962e..d6a7f48ae7d 100644 --- a/openstack-dashboard/dashboard/templates/base.html +++ b/openstack-dashboard/dashboard/templates/base.html @@ -34,7 +34,7 @@ {% block headerjs %}{% endblock %} {% block headercss %}{% endblock %} - + {% block content %}