Skip to content

Commit dcb8f00

Browse files
hramezanichicheng
authored andcommitted
Fixed #29379 -- Added autocomplete attribute to contrib.auth.forms fields.
Thank you to Nick Pope for review. Co-authored-by: CHI Cheng <[email protected]>
1 parent c498f08 commit dcb8f00

File tree

3 files changed

+73
-11
lines changed

3 files changed

+73
-11
lines changed

django/contrib/auth/forms.py

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -78,12 +78,12 @@ class UserCreationForm(forms.ModelForm):
7878
password1 = forms.CharField(
7979
label=_("Password"),
8080
strip=False,
81-
widget=forms.PasswordInput,
81+
widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}),
8282
help_text=password_validation.password_validators_help_text_html(),
8383
)
8484
password2 = forms.CharField(
8585
label=_("Password confirmation"),
86-
widget=forms.PasswordInput,
86+
widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}),
8787
strip=False,
8888
help_text=_("Enter the same password as before, for verification."),
8989
)
@@ -96,7 +96,10 @@ class Meta:
9696
def __init__(self, *args, **kwargs):
9797
super().__init__(*args, **kwargs)
9898
if self._meta.model.USERNAME_FIELD in self.fields:
99-
self.fields[self._meta.model.USERNAME_FIELD].widget.attrs.update({'autofocus': True})
99+
self.fields[self._meta.model.USERNAME_FIELD].widget.attrs.update({
100+
'autocomplete': 'username',
101+
'autofocus': True,
102+
})
100103

101104
def clean_password2(self):
102105
password1 = self.cleaned_data.get("password1")
@@ -163,11 +166,11 @@ class AuthenticationForm(forms.Form):
163166
Base class for authenticating users. Extend this to get a form that accepts
164167
username/password logins.
165168
"""
166-
username = UsernameField(widget=forms.TextInput(attrs={'autofocus': True}))
169+
username = UsernameField(widget=forms.TextInput(attrs={'autocomplete': 'username', 'autofocus': True}))
167170
password = forms.CharField(
168171
label=_("Password"),
169172
strip=False,
170-
widget=forms.PasswordInput,
173+
widget=forms.PasswordInput(attrs={'autocomplete': 'current-password'}),
171174
)
172175

173176
error_messages = {
@@ -235,7 +238,11 @@ def get_invalid_login_error(self):
235238

236239

237240
class PasswordResetForm(forms.Form):
238-
email = forms.EmailField(label=_("Email"), max_length=254)
241+
email = forms.EmailField(
242+
label=_("Email"),
243+
max_length=254,
244+
widget=forms.EmailInput(attrs={'autocomplete': 'email'})
245+
)
239246

240247
def send_mail(self, subject_template_name, email_template_name,
241248
context, from_email, to_email, html_email_template_name=None):
@@ -311,14 +318,14 @@ class SetPasswordForm(forms.Form):
311318
}
312319
new_password1 = forms.CharField(
313320
label=_("New password"),
314-
widget=forms.PasswordInput,
321+
widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}),
315322
strip=False,
316323
help_text=password_validation.password_validators_help_text_html(),
317324
)
318325
new_password2 = forms.CharField(
319326
label=_("New password confirmation"),
320327
strip=False,
321-
widget=forms.PasswordInput,
328+
widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}),
322329
)
323330

324331
def __init__(self, user, *args, **kwargs):
@@ -357,7 +364,7 @@ class PasswordChangeForm(SetPasswordForm):
357364
old_password = forms.CharField(
358365
label=_("Old password"),
359366
strip=False,
360-
widget=forms.PasswordInput(attrs={'autofocus': True}),
367+
widget=forms.PasswordInput(attrs={'autocomplete': 'current-password', 'autofocus': True}),
361368
)
362369

363370
field_order = ['old_password', 'new_password1', 'new_password2']
@@ -385,13 +392,13 @@ class AdminPasswordChangeForm(forms.Form):
385392
required_css_class = 'required'
386393
password1 = forms.CharField(
387394
label=_("Password"),
388-
widget=forms.PasswordInput(attrs={'autofocus': True}),
395+
widget=forms.PasswordInput(attrs={'autocomplete': 'new-password', 'autofocus': True}),
389396
strip=False,
390397
help_text=password_validation.password_validators_help_text_html(),
391398
)
392399
password2 = forms.CharField(
393400
label=_("Password (again)"),
394-
widget=forms.PasswordInput,
401+
widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}),
395402
strip=False,
396403
help_text=_("Enter the same password as before, for verification."),
397404
)

docs/releases/3.0.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ Minor features
7676
to mirror the existing
7777
:meth:`~django.contrib.auth.models.User.get_group_permissions()` method.
7878

79+
* Added HTML ``autocomplete`` attribute to widgets of username, email, and
80+
password fields in :mod:`django.contrib.auth.forms` for better interaction
81+
with browser password managers.
82+
7983
:mod:`django.contrib.contenttypes`
8084
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
8185

tests/auth_tests/test_forms.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,17 @@ def test_username_field_autocapitalize_none(self):
265265
form = UserCreationForm()
266266
self.assertEqual(form.fields['username'].widget.attrs.get('autocapitalize'), 'none')
267267

268+
def test_html_autocomplete_attributes(self):
269+
form = UserCreationForm()
270+
tests = (
271+
('username', 'username'),
272+
('password1', 'new-password'),
273+
('password2', 'new-password'),
274+
)
275+
for field_name, autocomplete in tests:
276+
with self.subTest(field_name=field_name, autocomplete=autocomplete):
277+
self.assertEqual(form.fields[field_name].widget.attrs['autocomplete'], autocomplete)
278+
268279

269280
# To verify that the login form rejects inactive users, use an authentication
270281
# backend that allows them.
@@ -492,6 +503,16 @@ def test_get_invalid_login_error(self):
492503
self.assertEqual(error.code, 'invalid_login')
493504
self.assertEqual(error.params, {'username': 'username'})
494505

506+
def test_html_autocomplete_attributes(self):
507+
form = AuthenticationForm()
508+
tests = (
509+
('username', 'username'),
510+
('password', 'current-password'),
511+
)
512+
for field_name, autocomplete in tests:
513+
with self.subTest(field_name=field_name, autocomplete=autocomplete):
514+
self.assertEqual(form.fields[field_name].widget.attrs['autocomplete'], autocomplete)
515+
495516

496517
class SetPasswordFormTest(TestDataMixin, TestCase):
497518

@@ -572,6 +593,16 @@ def test_help_text_translation(self):
572593
for french_text in french_help_texts:
573594
self.assertIn(french_text, html)
574595

596+
def test_html_autocomplete_attributes(self):
597+
form = SetPasswordForm(self.u1)
598+
tests = (
599+
('new_password1', 'new-password'),
600+
('new_password2', 'new-password'),
601+
)
602+
for field_name, autocomplete in tests:
603+
with self.subTest(field_name=field_name, autocomplete=autocomplete):
604+
self.assertEqual(form.fields[field_name].widget.attrs['autocomplete'], autocomplete)
605+
575606

576607
class PasswordChangeFormTest(TestDataMixin, TestCase):
577608

@@ -633,6 +664,11 @@ def test_password_whitespace_not_stripped(self):
633664
self.assertEqual(form.cleaned_data['new_password1'], data['new_password1'])
634665
self.assertEqual(form.cleaned_data['new_password2'], data['new_password2'])
635666

667+
def test_html_autocomplete_attributes(self):
668+
user = User.objects.get(username='testclient')
669+
form = PasswordChangeForm(user)
670+
self.assertEqual(form.fields['old_password'].widget.attrs['autocomplete'], 'current-password')
671+
636672

637673
class UserChangeFormTest(TestDataMixin, TestCase):
638674

@@ -916,6 +952,10 @@ def test_custom_email_field(self):
916952
self.assertEqual(len(mail.outbox), 1)
917953
self.assertEqual(mail.outbox[0].to, [email])
918954

955+
def test_html_autocomplete_attributes(self):
956+
form = PasswordResetForm()
957+
self.assertEqual(form.fields['email'].widget.attrs['autocomplete'], 'email')
958+
919959

920960
class ReadOnlyPasswordHashTest(SimpleTestCase):
921961

@@ -997,3 +1037,14 @@ def test_one_password(self):
9971037
form2 = AdminPasswordChangeForm(user, {'password1': 'test', 'password2': ''})
9981038
self.assertEqual(form2.errors['password2'], required_error)
9991039
self.assertNotIn('password1', form2.errors)
1040+
1041+
def test_html_autocomplete_attributes(self):
1042+
user = User.objects.get(username='testclient')
1043+
form = AdminPasswordChangeForm(user)
1044+
tests = (
1045+
('password1', 'new-password'),
1046+
('password2', 'new-password'),
1047+
)
1048+
for field_name, autocomplete in tests:
1049+
with self.subTest(field_name=field_name, autocomplete=autocomplete):
1050+
self.assertEqual(form.fields[field_name].widget.attrs['autocomplete'], autocomplete)

0 commit comments

Comments
 (0)