From 5df31c0164e9477a3ebb6b1bbde8604e06fbefd4 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Thu, 15 Dec 2011 16:12:46 +0000 Subject: [PATCH] Fixed #17194 -- Made sure the auth form tests work if a language other than English is activated by moving the error message translation strings into class level dictionaries. Many thanks to Claude Paroz, rabio and Bas Peschier for their initial work on this. git-svn-id: http://code.djangoproject.com/svn/django/trunk@17204 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/contrib/auth/forms.py | 58 ++++++++++++++++---- django/contrib/auth/tests/forms.py | 58 +++++++++++--------- django/contrib/auth/tests/views.py | 88 +++++++++++++++--------------- 3 files changed, 122 insertions(+), 82 deletions(-) diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py index 6fcf3232166..23185a0ece3 100644 --- a/django/contrib/auth/forms.py +++ b/django/contrib/auth/forms.py @@ -15,6 +15,7 @@ UNMASKED_DIGITS_TO_SHOW = 6 mask_password = lambda p: "%s%s" % (p[:UNMASKED_DIGITS_TO_SHOW], "*" * max(len(p) - UNMASKED_DIGITS_TO_SHOW, 0)) + class ReadOnlyPasswordHashWidget(forms.Widget): def render(self, name, value, attrs): if not value: @@ -39,6 +40,7 @@ class ReadOnlyPasswordHashWidget(forms.Widget): "masked": masked, }) + class ReadOnlyPasswordHashField(forms.Field): widget = ReadOnlyPasswordHashWidget @@ -46,10 +48,15 @@ class ReadOnlyPasswordHashField(forms.Field): kwargs.setdefault("required", False) super(ReadOnlyPasswordHashField, self).__init__(*args, **kwargs) + class UserCreationForm(forms.ModelForm): """ A form that creates a user, with no privileges, from the given username and password. """ + error_messages = { + 'duplicate_username': _("A user with that username already exists."), + 'password_mismatch': _("The two password fields didn't match."), + } username = forms.RegexField(label=_("Username"), max_length=30, regex=r'^[\w.@+-]+$', help_text = _("Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only."), error_messages = {'invalid': _("This value may contain only letters, numbers and @/./+/-/_ characters.")}) @@ -67,13 +74,13 @@ class UserCreationForm(forms.ModelForm): User.objects.get(username=username) except User.DoesNotExist: return username - raise forms.ValidationError(_("A user with that username already exists.")) + raise forms.ValidationError(self.error_messages['duplicate_username']) def clean_password2(self): password1 = self.cleaned_data.get("password1", "") password2 = self.cleaned_data["password2"] if password1 != password2: - raise forms.ValidationError(_("The two password fields didn't match.")) + raise forms.ValidationError(self.error_messages['password_mismatch']) return password2 def save(self, commit=True): @@ -83,6 +90,7 @@ class UserCreationForm(forms.ModelForm): user.save() return user + class UserChangeForm(forms.ModelForm): username = forms.RegexField(label=_("Username"), max_length=30, regex=r'^[\w.@+-]+$', help_text = _("Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only."), @@ -101,6 +109,7 @@ class UserChangeForm(forms.ModelForm): if f is not None: f.queryset = f.queryset.select_related('content_type') + class AuthenticationForm(forms.Form): """ Base class for authenticating users. Extend this to get a form that accepts @@ -109,6 +118,14 @@ class AuthenticationForm(forms.Form): username = forms.CharField(label=_("Username"), max_length=30) password = forms.CharField(label=_("Password"), widget=forms.PasswordInput) + error_messages = { + 'invalid_login': _("Please enter a correct username and password. " + "Note that both fields are case-sensitive."), + 'no_cookies': _("Your Web browser doesn't appear to have cookies " + "enabled. Cookies are required for logging in."), + 'inactive': _("This account is inactive."), + } + def __init__(self, request=None, *args, **kwargs): """ If request is passed in, the form will validate that cookies are @@ -127,17 +144,15 @@ class AuthenticationForm(forms.Form): if username and password: self.user_cache = authenticate(username=username, password=password) if self.user_cache is None: - raise forms.ValidationError(_("Please enter a correct username and password. Note that both fields are case-sensitive.")) + raise forms.ValidationError(self.error_messages['invalid_login']) elif not self.user_cache.is_active: - raise forms.ValidationError(_("This account is inactive.")) + raise forms.ValidationError(self.error_messages['inactive']) self.check_for_test_cookie() return self.cleaned_data def check_for_test_cookie(self): if self.request and not self.request.session.test_cookie_worked(): - raise forms.ValidationError( - _("Your Web browser doesn't appear to have cookies enabled. " - "Cookies are required for logging in.")) + raise forms.ValidationError(self.error_messages['no_cookies']) def get_user_id(self): if self.user_cache: @@ -147,7 +162,14 @@ class AuthenticationForm(forms.Form): def get_user(self): return self.user_cache + class PasswordResetForm(forms.Form): + error_messages = { + 'unknown': _("That e-mail address doesn't have an associated " + "user account. Are you sure you've registered?"), + 'unusable': _("The user account associated with this e-mail " + "address cannot reset the password."), + } email = forms.EmailField(label=_("E-mail"), max_length=75) def clean_email(self): @@ -159,9 +181,9 @@ class PasswordResetForm(forms.Form): email__iexact=email, is_active=True) if not len(self.users_cache): - raise forms.ValidationError(_("That e-mail address doesn't have an associated user account. Are you sure you've registered?")) + raise forms.ValidationError(self.error_messages['unknown']) if any((user.password == UNUSABLE_PASSWORD) for user in self.users_cache): - raise forms.ValidationError(_("The user account associated with this e-mail address cannot reset the password.")) + raise forms.ValidationError(self.error_messages['unusable']) return email def save(self, domain_override=None, @@ -195,11 +217,15 @@ class PasswordResetForm(forms.Form): email = loader.render_to_string(email_template_name, c) send_mail(subject, email, from_email, [user.email]) + class SetPasswordForm(forms.Form): """ A form that lets a user change set his/her password without entering the old password """ + error_messages = { + 'password_mismatch': _("The two password fields didn't match."), + } new_password1 = forms.CharField(label=_("New password"), widget=forms.PasswordInput) new_password2 = forms.CharField(label=_("New password confirmation"), widget=forms.PasswordInput) @@ -212,7 +238,7 @@ class SetPasswordForm(forms.Form): password2 = self.cleaned_data.get('new_password2') if password1 and password2: if password1 != password2: - raise forms.ValidationError(_("The two password fields didn't match.")) + raise forms.ValidationError(self.error_messages['password_mismatch']) return password2 def save(self, commit=True): @@ -221,11 +247,15 @@ class SetPasswordForm(forms.Form): self.user.save() return self.user + class PasswordChangeForm(SetPasswordForm): """ A form that lets a user change his/her password by entering their old password. """ + error_messages = dict(SetPasswordForm.error_messages, **{ + 'password_incorrect': _("Your old password was entered incorrectly. Please enter it again."), + }) old_password = forms.CharField(label=_("Old password"), widget=forms.PasswordInput) def clean_old_password(self): @@ -234,14 +264,18 @@ class PasswordChangeForm(SetPasswordForm): """ old_password = self.cleaned_data["old_password"] if not self.user.check_password(old_password): - raise forms.ValidationError(_("Your old password was entered incorrectly. Please enter it again.")) + raise forms.ValidationError(self.error_messages['password_incorrect']) return old_password PasswordChangeForm.base_fields.keyOrder = ['old_password', 'new_password1', 'new_password2'] + class AdminPasswordChangeForm(forms.Form): """ A form used to change the password of a user in the admin interface. """ + error_messages = { + 'password_mismatch': _("The two password fields didn't match."), + } password1 = forms.CharField(label=_("Password"), widget=forms.PasswordInput) password2 = forms.CharField(label=_("Password (again)"), widget=forms.PasswordInput) @@ -254,7 +288,7 @@ class AdminPasswordChangeForm(forms.Form): password2 = self.cleaned_data.get('password2') if password1 and password2: if password1 != password2: - raise forms.ValidationError(_("The two password fields didn't match.")) + raise forms.ValidationError(self.error_messages['password_mismatch']) return password2 def save(self, commit=True): diff --git a/django/contrib/auth/tests/forms.py b/django/contrib/auth/tests/forms.py index 429967cba00..2bacc8e6119 100644 --- a/django/contrib/auth/tests/forms.py +++ b/django/contrib/auth/tests/forms.py @@ -1,9 +1,12 @@ from __future__ import with_statement import os from django.core import mail +from django.forms.fields import Field, EmailField from django.contrib.auth.models import User from django.contrib.auth.forms import UserCreationForm, AuthenticationForm, PasswordChangeForm, SetPasswordForm, UserChangeForm, PasswordResetForm from django.test import TestCase +from django.utils.encoding import force_unicode +from django.utils import translation class UserCreationFormTest(TestCase): @@ -19,7 +22,7 @@ class UserCreationFormTest(TestCase): form = UserCreationForm(data) self.assertFalse(form.is_valid()) self.assertEqual(form["username"].errors, - [u'A user with that username already exists.']) + [force_unicode(form.error_messages['duplicate_username'])]) def test_invalid_data(self): data = { @@ -30,8 +33,7 @@ class UserCreationFormTest(TestCase): form = UserCreationForm(data) self.assertFalse(form.is_valid()) self.assertEqual(form["username"].errors, - [u'This value may contain only letters, numbers and @/./+/-/_ characters.']) - + [force_unicode(form.fields['username'].error_messages['invalid'])]) def test_password_verification(self): # The verification password is incorrect. @@ -43,25 +45,21 @@ class UserCreationFormTest(TestCase): form = UserCreationForm(data) self.assertFalse(form.is_valid()) self.assertEqual(form["password2"].errors, - [u"The two password fields didn't match."]) - + [force_unicode(form.error_messages['password_mismatch'])]) def test_both_passwords(self): # One (or both) passwords weren't given data = {'username': 'jsmith'} form = UserCreationForm(data) + required_error = [force_unicode(Field.default_error_messages['required'])] self.assertFalse(form.is_valid()) - self.assertEqual(form['password1'].errors, - [u'This field is required.']) - self.assertEqual(form['password2'].errors, - [u'This field is required.']) - + self.assertEqual(form['password1'].errors, required_error) + self.assertEqual(form['password2'].errors, required_error) data['password2'] = 'test123' form = UserCreationForm(data) self.assertFalse(form.is_valid()) - self.assertEqual(form['password1'].errors, - [u'This field is required.']) + self.assertEqual(form['password1'].errors, required_error) def test_success(self): # The success case. @@ -91,7 +89,7 @@ class AuthenticationFormTest(TestCase): form = AuthenticationForm(None, data) self.assertFalse(form.is_valid()) self.assertEqual(form.non_field_errors(), - [u'Please enter a correct username and password. Note that both fields are case-sensitive.']) + [force_unicode(form.error_messages['invalid_login'])]) def test_inactive_user(self): # The user is inactive. @@ -102,8 +100,20 @@ class AuthenticationFormTest(TestCase): form = AuthenticationForm(None, data) self.assertFalse(form.is_valid()) self.assertEqual(form.non_field_errors(), - [u'This account is inactive.']) + [force_unicode(form.error_messages['inactive'])]) + def test_inactive_user_i18n(self): + with self.settings(USE_I18N=True): + with translation.override('pt-br', deactivate=True): + # The user is inactive. + data = { + 'username': 'inactive', + 'password': 'password', + } + form = AuthenticationForm(None, data) + self.assertFalse(form.is_valid()) + self.assertEqual(form.non_field_errors(), + [force_unicode(form.error_messages['inactive'])]) def test_success(self): # The success case @@ -130,7 +140,7 @@ class SetPasswordFormTest(TestCase): form = SetPasswordForm(user, data) self.assertFalse(form.is_valid()) self.assertEqual(form["new_password2"].errors, - [u"The two password fields didn't match."]) + [force_unicode(form.error_messages['password_mismatch'])]) def test_success(self): user = User.objects.get(username='testclient') @@ -156,8 +166,7 @@ class PasswordChangeFormTest(TestCase): form = PasswordChangeForm(user, data) self.assertFalse(form.is_valid()) self.assertEqual(form["old_password"].errors, - [u'Your old password was entered incorrectly. Please enter it again.']) - + [force_unicode(form.error_messages['password_incorrect'])]) def test_password_verification(self): # The two new passwords do not match. @@ -170,8 +179,7 @@ class PasswordChangeFormTest(TestCase): form = PasswordChangeForm(user, data) self.assertFalse(form.is_valid()) self.assertEqual(form["new_password2"].errors, - [u"The two password fields didn't match."]) - + [force_unicode(form.error_messages['password_mismatch'])]) def test_success(self): # The success case. @@ -190,6 +198,7 @@ class PasswordChangeFormTest(TestCase): self.assertEqual(PasswordChangeForm(user, {}).fields.keys(), ['old_password', 'new_password1', 'new_password2']) + class UserChangeFormTest(TestCase): fixtures = ['authtestdata.json'] @@ -200,7 +209,7 @@ class UserChangeFormTest(TestCase): form = UserChangeForm(data, instance=user) self.assertFalse(form.is_valid()) self.assertEqual(form['username'].errors, - [u'This value may contain only letters, numbers and @/./+/-/_ characters.']) + [force_unicode(form.fields['username'].error_messages['invalid'])]) def test_bug_14242(self): # A regression test, introduce by adding an optimization for the @@ -232,19 +241,19 @@ class PasswordResetFormTest(TestCase): return (user, username, email) def test_invalid_email(self): - data = {'email':'not valid'} + data = {'email': 'not valid'} form = PasswordResetForm(data) self.assertFalse(form.is_valid()) self.assertEqual(form['email'].errors, - [u'Enter a valid e-mail address.']) + [force_unicode(EmailField.default_error_messages['invalid'])]) def test_nonexistant_email(self): # Test nonexistant email address - data = {'email':'foo@bar.com'} + data = {'email': 'foo@bar.com'} form = PasswordResetForm(data) self.assertFalse(form.is_valid()) self.assertEqual(form.errors, - {'email': [u"That e-mail address doesn't have an associated user account. Are you sure you've registered?"]}) + {'email': [force_unicode(form.error_messages['unknown'])]}) def test_cleaned_data(self): # Regression test @@ -284,7 +293,6 @@ class PasswordResetFormTest(TestCase): form = PasswordResetForm({'email': email}) self.assertFalse(form.is_valid()) - def test_unusable_password(self): user = User.objects.create_user('testuser', 'test@example.com', 'test') data = {"email": "test@example.com"} diff --git a/django/contrib/auth/tests/views.py b/django/contrib/auth/tests/views.py index a0be92fc15a..1975266f4fd 100644 --- a/django/contrib/auth/tests/views.py +++ b/django/contrib/auth/tests/views.py @@ -4,15 +4,18 @@ import re import urllib from django.conf import settings -from django.contrib.auth import SESSION_KEY, REDIRECT_FIELD_NAME -from django.contrib.auth.forms import AuthenticationForm from django.contrib.sites.models import Site, RequestSite from django.contrib.auth.models import User -from django.core.urlresolvers import NoReverseMatch -from django.test import TestCase from django.core import mail -from django.core.urlresolvers import reverse +from django.core.urlresolvers import reverse, NoReverseMatch from django.http import QueryDict +from django.utils.encoding import force_unicode +from django.utils.html import escape +from django.test import TestCase + +from django.contrib.auth import SESSION_KEY, REDIRECT_FIELD_NAME +from django.contrib.auth.forms import (AuthenticationForm, PasswordChangeForm, + SetPasswordForm, PasswordResetForm) class AuthViewsTestCase(TestCase): @@ -40,13 +43,15 @@ class AuthViewsTestCase(TestCase): def login(self, password='password'): response = self.client.post('/login/', { 'username': 'testclient', - 'password': password - } - ) + 'password': password, + }) self.assertEqual(response.status_code, 302) self.assertTrue(response['Location'].endswith(settings.LOGIN_REDIRECT_URL)) self.assertTrue(SESSION_KEY in self.client.session) + def assertContainsEscaped(self, response, text, **kwargs): + return self.assertContains(response, escape(force_unicode(text)), **kwargs) + class AuthViewNamedURLTests(AuthViewsTestCase): urls = 'django.contrib.auth.urls' @@ -80,7 +85,7 @@ class PasswordResetTest(AuthViewsTestCase): response = self.client.get('/password_reset/') self.assertEqual(response.status_code, 200) response = self.client.post('/password_reset/', {'email': 'not_a_real_email@email.com'}) - self.assertContains(response, "That e-mail address doesn't have an associated user account") + self.assertContainsEscaped(response, PasswordResetForm.error_messages['unknown']) self.assertEqual(len(mail.outbox), 0) def test_email_found(self): @@ -121,7 +126,7 @@ class PasswordResetTest(AuthViewsTestCase): url, path = self._test_confirm_start() # Let's munge the token in the path, but keep the same length, # in case the URLconf will reject a different length. - path = path[:-5] + ("0"*4) + path[-1] + path = path[:-5] + ("0" * 4) + path[-1] response = self.client.get(path) self.assertEqual(response.status_code, 200) @@ -143,10 +148,12 @@ class PasswordResetTest(AuthViewsTestCase): # Same as test_confirm_invalid, but trying # to do a POST instead. url, path = self._test_confirm_start() - path = path[:-5] + ("0"*4) + path[-1] + path = path[:-5] + ("0" * 4) + path[-1] - response = self.client.post(path, {'new_password1': 'anewpassword', - 'new_password2':' anewpassword'}) + self.client.post(path, { + 'new_password1': 'anewpassword', + 'new_password2': ' anewpassword', + }) # Check the password has not been changed u = User.objects.get(email='staffmember@example.com') self.assertTrue(not u.check_password("anewpassword")) @@ -169,20 +176,20 @@ class PasswordResetTest(AuthViewsTestCase): def test_confirm_different_passwords(self): url, path = self._test_confirm_start() response = self.client.post(path, {'new_password1': 'anewpassword', - 'new_password2':' x'}) + 'new_password2': 'x'}) self.assertEqual(response.status_code, 200) - self.assertTrue("The two password fields didn't match" in response.content) + self.assertContainsEscaped(response, SetPasswordForm.error_messages['password_mismatch']) + class ChangePasswordTest(AuthViewsTestCase): def fail_login(self, password='password'): response = self.client.post('/login/', { 'username': 'testclient', - 'password': password - } - ) + 'password': password, + }) self.assertEqual(response.status_code, 200) - self.assertTrue("Please enter a correct username and password. Note that both fields are case-sensitive." in response.content) + self.assertContainsEscaped(response, AuthenticationForm.error_messages['invalid_login']) def logout(self): response = self.client.get('/logout/') @@ -193,10 +200,9 @@ class ChangePasswordTest(AuthViewsTestCase): 'old_password': 'donuts', 'new_password1': 'password1', 'new_password2': 'password1', - } - ) + }) self.assertEqual(response.status_code, 200) - self.assertTrue("Your old password was entered incorrectly. Please enter it again." in response.content) + self.assertContainsEscaped(response, PasswordChangeForm.error_messages['password_incorrect']) def test_password_change_fails_with_mismatched_passwords(self): self.login() @@ -204,10 +210,9 @@ class ChangePasswordTest(AuthViewsTestCase): 'old_password': 'password', 'new_password1': 'password1', 'new_password2': 'donuts', - } - ) + }) self.assertEqual(response.status_code, 200) - self.assertTrue("The two password fields didn't match." in response.content) + self.assertContainsEscaped(response, SetPasswordForm.error_messages['password_mismatch']) def test_password_change_succeeds(self): self.login() @@ -215,8 +220,7 @@ class ChangePasswordTest(AuthViewsTestCase): 'old_password': 'password', 'new_password1': 'password1', 'new_password2': 'password1', - } - ) + }) self.assertEqual(response.status_code, 302) self.assertTrue(response['Location'].endswith('/password_change/done/')) self.fail_login() @@ -228,8 +232,7 @@ class ChangePasswordTest(AuthViewsTestCase): 'old_password': 'password', 'new_password1': 'password1', 'new_password2': 'password1', - } - ) + }) self.assertEqual(response.status_code, 302) self.assertTrue(response['Location'].endswith('/password_change/done/')) @@ -266,13 +269,12 @@ class LoginTest(AuthViewsTestCase): nasty_url = '%(url)s?%(next)s=%(bad_url)s' % { 'url': login_url, 'next': REDIRECT_FIELD_NAME, - 'bad_url': urllib.quote(bad_url) + 'bad_url': urllib.quote(bad_url), } response = self.client.post(nasty_url, { 'username': 'testclient', 'password': password, - } - ) + }) self.assertEqual(response.status_code, 302) self.assertFalse(bad_url in response['Location'], "%s should be blocked" % bad_url) @@ -284,18 +286,16 @@ class LoginTest(AuthViewsTestCase): 'view/?param=//example.com', 'https:///', '//testserver/', - '/url%20with%20spaces/', # see ticket #12534 - ): + '/url%20with%20spaces/'): # see ticket #12534 safe_url = '%(url)s?%(next)s=%(good_url)s' % { 'url': login_url, 'next': REDIRECT_FIELD_NAME, - 'good_url': urllib.quote(good_url) + 'good_url': urllib.quote(good_url), } response = self.client.post(safe_url, { 'username': 'testclient', 'password': password, - } - ) + }) self.assertEqual(response.status_code, 302) self.assertTrue(good_url in response['Location'], "%s should be allowed" % good_url) @@ -322,8 +322,8 @@ class LoginURLSettings(AuthViewsTestCase): login_required_url = self.get_login_required_url(login_url) querystring = QueryDict('', mutable=True) querystring['next'] = '/login_required/' - self.assertEqual(login_required_url, - 'http://testserver%s?%s' % (login_url, querystring.urlencode('/'))) + self.assertEqual(login_required_url, 'http://testserver%s?%s' % + (login_url, querystring.urlencode('/'))) def test_remote_login_url(self): login_url = 'http://remote.example.com/login' @@ -422,12 +422,11 @@ class LogoutTest(AuthViewsTestCase): for bad_url in ('http://example.com', 'https://example.com', 'ftp://exampel.com', - '//example.com' - ): + '//example.com'): nasty_url = '%(url)s?%(next)s=%(bad_url)s' % { 'url': logout_url, 'next': REDIRECT_FIELD_NAME, - 'bad_url': urllib.quote(bad_url) + 'bad_url': urllib.quote(bad_url), } self.login() response = self.client.get(nasty_url) @@ -443,12 +442,11 @@ class LogoutTest(AuthViewsTestCase): 'view/?param=//example.com', 'https:///', '//testserver/', - '/url%20with%20spaces/', # see ticket #12534 - ): + '/url%20with%20spaces/'): # see ticket #12534 safe_url = '%(url)s?%(next)s=%(good_url)s' % { 'url': logout_url, 'next': REDIRECT_FIELD_NAME, - 'good_url': urllib.quote(good_url) + 'good_url': urllib.quote(good_url), } self.login() response = self.client.get(safe_url)