Display a message on the login page

In some cases, particularly when having to log the user out after
performing some action (e.g. password change), we want to display a
friendly message on the login screen to explain to the user why they
have been redirected to the login page.

This adds a function to do so, and uses it in a couple of places:
 - When updating one's own password using the Settings panel
 - Session time out
 - HTTP 401

Change-Id: Ie53c5552159304e1f1304ac6211b3accfd9aa623
Implements: blueprint messages-on-login-page
This commit is contained in:
Julie Pichon 2013-08-28 15:30:44 +01:00
parent a6f39bc74a
commit 85f4c8b473
6 changed files with 50 additions and 13 deletions

View File

@ -34,9 +34,10 @@ from django.http import HttpResponseRedirect # noqa
from django import shortcuts from django import shortcuts
from django.utils.encoding import iri_to_uri # noqa from django.utils.encoding import iri_to_uri # noqa
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ # noqa
from horizon import exceptions from horizon import exceptions
from horizon.utils import functions as utils
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -62,7 +63,10 @@ class HorizonMiddleware(object):
timestamp = datetime.datetime.now() timestamp = datetime.datetime.now()
if last_activity and (timestamp - last_activity).seconds > timeout: if last_activity and (timestamp - last_activity).seconds > timeout:
request.session.pop('last_activity') request.session.pop('last_activity')
return HttpResponseRedirect(settings.LOGOUT_URL) response = HttpResponseRedirect(settings.LOGOUT_URL)
reason = _("Session timed out.")
utils.add_logout_reason(request, response, reason)
return response
request.session['last_activity'] = timestamp request.session['last_activity'] = timestamp
request.horizon = {'dashboard': None, request.horizon = {'dashboard': None,
@ -86,12 +90,13 @@ class HorizonMiddleware(object):
response = redirect_to_login(next_url, login_url=login_url, response = redirect_to_login(next_url, login_url=login_url,
redirect_field_name=field_name) redirect_field_name=field_name)
# TODO(gabriel): Find a way to display an appropriate message to
# the user *on* the login form...
if request.is_ajax(): if request.is_ajax():
response_401 = http.HttpResponse(status=401) response_401 = http.HttpResponse(status=401)
response_401['X-Horizon-Location'] = response['location'] response_401['X-Horizon-Location'] = response['location']
return response_401 return response_401
else:
utils.add_logout_reason(request, response, _("Unauthorized."))
return response return response
# If an internal "NotFound" error gets this far, return a real 404. # If an internal "NotFound" error gets this far, return a real 404.

View File

@ -18,6 +18,11 @@
<a href="{% url 'horizon:user_home' %}">{% trans "home page" %}</a></p> <a href="{% url 'horizon:user_home' %}">{% trans "home page" %}</a></p>
</span> </span>
</div> </div>
{% endif %}
{% if request.COOKIES.logout_reason %}
<div class="control-group clearfix error">
<span class="help-inline"><p>{{ request.COOKIES.logout_reason }}</p></span>
</div>
{% endif %} {% endif %}
{% if next %}<input type="hidden" name="{{ redirect_field_name }}" value="{{ next }}" />{% endif %} {% if next %}<input type="hidden" name="{{ redirect_field_name }}" value="{{ next }}" />{% endif %}
{% include "horizon/common/_form_fields.html" %} {% include "horizon/common/_form_fields.html" %}

View File

@ -60,14 +60,14 @@ class RequestFactoryWithMessages(RequestFactory):
def get(self, *args, **kwargs): def get(self, *args, **kwargs):
req = super(RequestFactoryWithMessages, self).get(*args, **kwargs) req = super(RequestFactoryWithMessages, self).get(*args, **kwargs)
req.user = User() req.user = User()
req.session = [] req.session = {}
req._messages = default_storage(req) req._messages = default_storage(req)
return req return req
def post(self, *args, **kwargs): def post(self, *args, **kwargs):
req = super(RequestFactoryWithMessages, self).post(*args, **kwargs) req = super(RequestFactoryWithMessages, self).post(*args, **kwargs)
req.user = User() req.user = User()
req.session = [] req.session = {}
req._messages = default_storage(req) req._messages = default_storage(req)
return req return req

View File

@ -2,6 +2,7 @@ import math
from django.utils.encoding import force_unicode # noqa from django.utils.encoding import force_unicode # noqa
from django.utils.functional import lazy # noqa from django.utils.functional import lazy # noqa
from django.utils import translation
def _lazy_join(separator, strings): def _lazy_join(separator, strings):
@ -15,3 +16,11 @@ def bytes_to_gigabytes(bytes):
# Converts the number of bytes to the next highest number of Gigabytes # Converts the number of bytes to the next highest number of Gigabytes
# For example 5000000 (5 Meg) would return '1' # For example 5000000 (5 Meg) would return '1'
return int(math.ceil(float(bytes) / 1024 ** 3)) return int(math.ceil(float(bytes) / 1024 ** 3))
def add_logout_reason(request, response, reason):
# Store the translated string in the cookie
lang = translation.get_language_from_request(request)
with translation.override(lang):
reason = unicode(reason).encode('utf-8')
response.set_cookie('logout_reason', reason, max_age=30)

View File

@ -14,13 +14,16 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from django.conf import settings # noqa
from django.forms import ValidationError # noqa from django.forms import ValidationError # noqa
from django import http
from django.utils.translation import ugettext_lazy as _ # noqa from django.utils.translation import ugettext_lazy as _ # noqa
from django.views.decorators.debug import sensitive_variables # noqa from django.views.decorators.debug import sensitive_variables # noqa
from horizon import exceptions from horizon import exceptions
from horizon import forms from horizon import forms
from horizon import messages from horizon import messages
from horizon.utils import functions as utils
from horizon.utils import validators from horizon.utils import validators
from openstack_dashboard import api from openstack_dashboard import api
@ -56,7 +59,10 @@ class PasswordForm(forms.SelfHandlingForm):
api.keystone.user_update_own_password(request, api.keystone.user_update_own_password(request,
data['current_password'], data['current_password'],
data['new_password']) data['new_password'])
messages.success(request, _('Password changed.')) response = http.HttpResponseRedirect(settings.LOGOUT_URL)
msg = _("Password changed. Please log in again to continue.")
utils.add_logout_reason(request, response, msg)
return response
except Exception: except Exception:
exceptions.handle(request, exceptions.handle(request,
_('Unable to change password.')) _('Unable to change password.'))

View File

@ -31,27 +31,39 @@ class ChangePasswordTests(test.TestCase):
@test.create_stubs({api.keystone: ('user_update_own_password', )}) @test.create_stubs({api.keystone: ('user_update_own_password', )})
def test_change_password(self): def test_change_password(self):
api.keystone.user_update_own_password(IsA(http.HttpRequest), api.keystone.user_update_own_password(IsA(http.HttpRequest),
'oldpwd', 'oldpwd',
'normalpwd',).AndReturn(None) 'normalpwd',).AndReturn(None)
self.mox.ReplayAll() self.mox.ReplayAll()
formData = {'method': 'PasswordForm', formData = {'method': 'PasswordForm',
'current_password': 'oldpwd', 'current_password': 'oldpwd',
'new_password': 'normalpwd', 'new_password': 'normalpwd',
'confirm_password': 'normalpwd'} 'confirm_password': 'normalpwd'}
res = self.client.post(INDEX_URL, formData) res = self.client.post(INDEX_URL, formData)
self.assertNoFormErrors(res) self.assertNoFormErrors(res)
def test_change_validation_passwords_not_matching(self): def test_change_validation_passwords_not_matching(self):
formData = {'method': 'PasswordForm', formData = {'method': 'PasswordForm',
'current_password': 'currpasswd', 'current_password': 'currpasswd',
'new_password': 'testpassword', 'new_password': 'testpassword',
'confirm_password': 'doesnotmatch'} 'confirm_password': 'doesnotmatch'}
res = self.client.post(INDEX_URL, formData) res = self.client.post(INDEX_URL, formData)
self.assertFormError(res, "form", None, ['Passwords do not match.']) self.assertFormError(res, "form", None, ['Passwords do not match.'])
@test.create_stubs({api.keystone: ('user_update_own_password', )})
def test_change_password_shows_message_on_login_page(self):
api.keystone.user_update_own_password(IsA(http.HttpRequest),
'oldpwd',
'normalpwd').AndReturn(None)
self.mox.ReplayAll()
formData = {'method': 'PasswordForm',
'current_password': 'oldpwd',
'new_password': 'normalpwd',
'confirm_password': 'normalpwd'}
res = self.client.post(INDEX_URL, formData, follow=True)
info_msg = "Password changed. Please log in again to continue."
self.assertContains(res, info_msg)