Improved region switcher.
Adds the ability to live-switch regions, and bakes in support at a lower level. Cleans up login-related code. Makes the login view capable of being used as a modal dialog. Overall UX improvements for region support. Fixes a bug where having one region would still show the region switcher inappropriately. Fixed bug 929886. BACKWARDS INCOMPATIBLE CHANGE: If you were an early adopter of the region switcher, you will need to reverse the order of the settings tuples from the previous ("region name", "endpoint") order to the new ("endpoint", "region name") style. This change was done to better suit Django's "choices" syntax since the original ordering was arbitrary. Change-Id: I79db4ec1e608ee0f35916966c018d2a76b5ff662
This commit is contained in:
parent
95970b701c
commit
4d8a924862
@ -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"))
|
||||
|
@ -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
|
||||
|
@ -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<tenant_id>[^/]+)/$', 'switch_tenants',
|
||||
name='auth_switch'))
|
||||
|
@ -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" %}
|
||||
|
10
horizon/horizon/templates/horizon/auth/login.html
Normal file
10
horizon/horizon/templates/horizon/auth/login.html
Normal file
@ -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 %}
|
@ -1,11 +1,13 @@
|
||||
{% if region.support %}
|
||||
{% if regions.support %}
|
||||
<div id="region_switcher" class="dropdown switcher_bar" tabindex='1'>
|
||||
<a class="dropdown-toggle" data-toggle="dropdown" href="#region_switcher">
|
||||
{{ region.name }}
|
||||
{{ regions.current.name }}
|
||||
</a>
|
||||
<ul id="region_list" class="dropdown-menu">
|
||||
<li class='divider'></li>
|
||||
<li><a href="{% url horizon:auth_logout %}">Change Regions</a></li>
|
||||
{% for region in regions.available %}
|
||||
<li><a class="ajax-modal" href="{% url horizon:auth_login %}?region={{ region.endpoint }}">{{ region.name }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -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 @@ class AuthViewTests(test.BaseViewTests):
|
||||
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 AuthViewTests(test.BaseViewTests):
|
||||
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 AuthViewTests(test.BaseViewTests):
|
||||
|
||||
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 @@ class AuthViewTests(test.BaseViewTests):
|
||||
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 @@ class AuthViewTests(test.BaseViewTests):
|
||||
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 @@ class AuthViewTests(test.BaseViewTests):
|
||||
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}
|
||||
|
@ -101,8 +101,8 @@ SWIFT_PASS = 'testing'
|
||||
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"
|
||||
|
@ -27,6 +27,7 @@ from django.utils.translation import ugettext as _
|
||||
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_class = Login
|
||||
template_name = "horizon/auth/login.html"
|
||||
|
||||
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})
|
||||
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):
|
||||
|
@ -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())
|
||||
|
@ -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;
|
||||
|
@ -34,7 +34,7 @@
|
||||
{% block headerjs %}{% endblock %}
|
||||
{% block headercss %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<body id="{% block body_id %}{% endblock %}">
|
||||
{% block content %}
|
||||
<div id="container" class="fluid-container sidebar-left">
|
||||
{% block sidebar %}{% endblock %}
|
||||
|
@ -36,10 +36,10 @@ HORIZON_CONFIG = {
|
||||
'user_home': 'dashboard.views.user_home',
|
||||
}
|
||||
|
||||
# For multiple regions uncomment this configuration, and add (title, endpoint).
|
||||
# For multiple regions uncomment this configuration, and add (endpoint, title).
|
||||
# AVAILABLE_REGIONS = [
|
||||
# ('cluster1', 'http://cluster1.com:5000/v2.0'),
|
||||
# ('cluster2', 'http://cluster2.com:5000/v2.0'),
|
||||
# ('http://cluster1.example.com:5000/v2.0', 'cluster1'),
|
||||
# ('http://cluster2.example.com:5000/v2.0', 'cluster2'),
|
||||
# ]
|
||||
|
||||
# FIXME: This is only here so quantum still works. It must be refactored to
|
||||
|
Loading…
x
Reference in New Issue
Block a user