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
This commit is contained in:
Jannis Leidel 2011-12-15 16:12:46 +00:00
parent e7b5e22484
commit 5df31c0164
3 changed files with 122 additions and 82 deletions

View File

@ -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):

View File

@ -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"}

View File

@ -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)