diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py index 6fcf323216..23185a0ece 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 429967cba0..2bacc8e611 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 a0be92fc15..1975266f4f 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)