Procházet zdrojové kódy

Fixed #29379 -- Added autocomplete attribute to contrib.auth.forms fields.

Thank you to Nick Pope for review.

Co-authored-by: CHI Cheng <cloudream@gmail.com>
Hasan Ramezani před 6 roky
rodič
revize
dcb8f00d06

+ 18 - 11
django/contrib/auth/forms.py

@@ -78,12 +78,12 @@ class UserCreationForm(forms.ModelForm):
     password1 = forms.CharField(
         label=_("Password"),
         strip=False,
-        widget=forms.PasswordInput,
+        widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}),
         help_text=password_validation.password_validators_help_text_html(),
     )
     password2 = forms.CharField(
         label=_("Password confirmation"),
-        widget=forms.PasswordInput,
+        widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}),
         strip=False,
         help_text=_("Enter the same password as before, for verification."),
     )
@@ -96,7 +96,10 @@ class UserCreationForm(forms.ModelForm):
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
         if self._meta.model.USERNAME_FIELD in self.fields:
-            self.fields[self._meta.model.USERNAME_FIELD].widget.attrs.update({'autofocus': True})
+            self.fields[self._meta.model.USERNAME_FIELD].widget.attrs.update({
+                'autocomplete': 'username',
+                'autofocus': True,
+            })
 
     def clean_password2(self):
         password1 = self.cleaned_data.get("password1")
@@ -163,11 +166,11 @@ class AuthenticationForm(forms.Form):
     Base class for authenticating users. Extend this to get a form that accepts
     username/password logins.
     """
-    username = UsernameField(widget=forms.TextInput(attrs={'autofocus': True}))
+    username = UsernameField(widget=forms.TextInput(attrs={'autocomplete': 'username', 'autofocus': True}))
     password = forms.CharField(
         label=_("Password"),
         strip=False,
-        widget=forms.PasswordInput,
+        widget=forms.PasswordInput(attrs={'autocomplete': 'current-password'}),
     )
 
     error_messages = {
@@ -235,7 +238,11 @@ class AuthenticationForm(forms.Form):
 
 
 class PasswordResetForm(forms.Form):
-    email = forms.EmailField(label=_("Email"), max_length=254)
+    email = forms.EmailField(
+        label=_("Email"),
+        max_length=254,
+        widget=forms.EmailInput(attrs={'autocomplete': 'email'})
+    )
 
     def send_mail(self, subject_template_name, email_template_name,
                   context, from_email, to_email, html_email_template_name=None):
@@ -311,14 +318,14 @@ class SetPasswordForm(forms.Form):
     }
     new_password1 = forms.CharField(
         label=_("New password"),
-        widget=forms.PasswordInput,
+        widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}),
         strip=False,
         help_text=password_validation.password_validators_help_text_html(),
     )
     new_password2 = forms.CharField(
         label=_("New password confirmation"),
         strip=False,
-        widget=forms.PasswordInput,
+        widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}),
     )
 
     def __init__(self, user, *args, **kwargs):
@@ -357,7 +364,7 @@ class PasswordChangeForm(SetPasswordForm):
     old_password = forms.CharField(
         label=_("Old password"),
         strip=False,
-        widget=forms.PasswordInput(attrs={'autofocus': True}),
+        widget=forms.PasswordInput(attrs={'autocomplete': 'current-password', 'autofocus': True}),
     )
 
     field_order = ['old_password', 'new_password1', 'new_password2']
@@ -385,13 +392,13 @@ class AdminPasswordChangeForm(forms.Form):
     required_css_class = 'required'
     password1 = forms.CharField(
         label=_("Password"),
-        widget=forms.PasswordInput(attrs={'autofocus': True}),
+        widget=forms.PasswordInput(attrs={'autocomplete': 'new-password', 'autofocus': True}),
         strip=False,
         help_text=password_validation.password_validators_help_text_html(),
     )
     password2 = forms.CharField(
         label=_("Password (again)"),
-        widget=forms.PasswordInput,
+        widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}),
         strip=False,
         help_text=_("Enter the same password as before, for verification."),
     )

+ 4 - 0
docs/releases/3.0.txt

@@ -76,6 +76,10 @@ Minor features
   to mirror the existing
   :meth:`~django.contrib.auth.models.User.get_group_permissions()` method.
 
+* Added HTML ``autocomplete`` attribute to widgets of username, email, and
+  password fields in :mod:`django.contrib.auth.forms` for better interaction
+  with browser password managers.
+
 :mod:`django.contrib.contenttypes`
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 

+ 51 - 0
tests/auth_tests/test_forms.py

@@ -265,6 +265,17 @@ class UserCreationFormTest(TestDataMixin, TestCase):
         form = UserCreationForm()
         self.assertEqual(form.fields['username'].widget.attrs.get('autocapitalize'), 'none')
 
+    def test_html_autocomplete_attributes(self):
+        form = UserCreationForm()
+        tests = (
+            ('username', 'username'),
+            ('password1', 'new-password'),
+            ('password2', 'new-password'),
+        )
+        for field_name, autocomplete in tests:
+            with self.subTest(field_name=field_name, autocomplete=autocomplete):
+                self.assertEqual(form.fields[field_name].widget.attrs['autocomplete'], autocomplete)
+
 
 # To verify that the login form rejects inactive users, use an authentication
 # backend that allows them.
@@ -492,6 +503,16 @@ class AuthenticationFormTest(TestDataMixin, TestCase):
         self.assertEqual(error.code, 'invalid_login')
         self.assertEqual(error.params, {'username': 'username'})
 
+    def test_html_autocomplete_attributes(self):
+        form = AuthenticationForm()
+        tests = (
+            ('username', 'username'),
+            ('password', 'current-password'),
+        )
+        for field_name, autocomplete in tests:
+            with self.subTest(field_name=field_name, autocomplete=autocomplete):
+                self.assertEqual(form.fields[field_name].widget.attrs['autocomplete'], autocomplete)
+
 
 class SetPasswordFormTest(TestDataMixin, TestCase):
 
@@ -572,6 +593,16 @@ class SetPasswordFormTest(TestDataMixin, TestCase):
             for french_text in french_help_texts:
                 self.assertIn(french_text, html)
 
+    def test_html_autocomplete_attributes(self):
+        form = SetPasswordForm(self.u1)
+        tests = (
+            ('new_password1', 'new-password'),
+            ('new_password2', 'new-password'),
+        )
+        for field_name, autocomplete in tests:
+            with self.subTest(field_name=field_name, autocomplete=autocomplete):
+                self.assertEqual(form.fields[field_name].widget.attrs['autocomplete'], autocomplete)
+
 
 class PasswordChangeFormTest(TestDataMixin, TestCase):
 
@@ -633,6 +664,11 @@ class PasswordChangeFormTest(TestDataMixin, TestCase):
         self.assertEqual(form.cleaned_data['new_password1'], data['new_password1'])
         self.assertEqual(form.cleaned_data['new_password2'], data['new_password2'])
 
+    def test_html_autocomplete_attributes(self):
+        user = User.objects.get(username='testclient')
+        form = PasswordChangeForm(user)
+        self.assertEqual(form.fields['old_password'].widget.attrs['autocomplete'], 'current-password')
+
 
 class UserChangeFormTest(TestDataMixin, TestCase):
 
@@ -916,6 +952,10 @@ class PasswordResetFormTest(TestDataMixin, TestCase):
         self.assertEqual(len(mail.outbox), 1)
         self.assertEqual(mail.outbox[0].to, [email])
 
+    def test_html_autocomplete_attributes(self):
+        form = PasswordResetForm()
+        self.assertEqual(form.fields['email'].widget.attrs['autocomplete'], 'email')
+
 
 class ReadOnlyPasswordHashTest(SimpleTestCase):
 
@@ -997,3 +1037,14 @@ class AdminPasswordChangeFormTest(TestDataMixin, TestCase):
         form2 = AdminPasswordChangeForm(user, {'password1': 'test', 'password2': ''})
         self.assertEqual(form2.errors['password2'], required_error)
         self.assertNotIn('password1', form2.errors)
+
+    def test_html_autocomplete_attributes(self):
+        user = User.objects.get(username='testclient')
+        form = AdminPasswordChangeForm(user)
+        tests = (
+            ('password1', 'new-password'),
+            ('password2', 'new-password'),
+        )
+        for field_name, autocomplete in tests:
+            with self.subTest(field_name=field_name, autocomplete=autocomplete):
+                self.assertEqual(form.fields[field_name].widget.attrs['autocomplete'], autocomplete)