Skip to content

Commit

Permalink
Improved region switcher.
Browse files Browse the repository at this point in the history
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
  • Loading branch information
gabrielhurley committed Feb 10, 2012
1 parent 95970b7 commit 4d8a924
Show file tree
Hide file tree
Showing 13 changed files with 98 additions and 83 deletions.
3 changes: 2 additions & 1 deletion docs/source/conf.py
Expand Up @@ -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"))
Expand Down
12 changes: 7 additions & 5 deletions horizon/horizon/context_processors.py
Expand Up @@ -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
4 changes: 3 additions & 1 deletion horizon/horizon/site_urls.py
Expand Up @@ -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'))
Expand Down
4 changes: 2 additions & 2 deletions horizon/horizon/templates/horizon/auth/_login.html
Expand Up @@ -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" %}
Expand Down
10 changes: 10 additions & 0 deletions 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 %}
@@ -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 %}
66 changes: 30 additions & 36 deletions horizon/horizon/tests/auth_tests.py
Expand Up @@ -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

Expand All @@ -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}

Expand All @@ -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([])
Expand All @@ -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)

Expand All @@ -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')
Expand All @@ -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',
Expand Down Expand Up @@ -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}
Expand Down
4 changes: 2 additions & 2 deletions horizon/horizon/tests/testsettings.py
Expand Up @@ -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"
Expand Down
24 changes: 13 additions & 11 deletions horizon/horizon/views/auth.py
Expand Up @@ -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
Expand All @@ -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):
Expand Down
35 changes: 17 additions & 18 deletions horizon/horizon/views/auth_forms.py
Expand Up @@ -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.
Expand All @@ -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:
Expand Down Expand Up @@ -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())
3 changes: 3 additions & 0 deletions openstack-dashboard/dashboard/static/dashboard/css/style.css
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion openstack-dashboard/dashboard/templates/base.html
Expand Up @@ -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 %}
Expand Down
6 changes: 3 additions & 3 deletions openstack-dashboard/local/local_settings.py.example
Expand Up @@ -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
Expand Down

0 comments on commit 4d8a924

Please sign in to comment.