diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py index 5f71f032581..ab46caa12ec 100644 --- a/django/contrib/auth/forms.py +++ b/django/contrib/auth/forms.py @@ -36,10 +36,9 @@ class ReadOnlyPasswordHashWidget(forms.Widget): def get_context(self, name, value, attrs): context = super().get_context(name, value, attrs) + usable_password = value and not value.startswith(UNUSABLE_PASSWORD_PREFIX) summary = [] - if not value or value.startswith(UNUSABLE_PASSWORD_PREFIX): - summary.append({"label": gettext("No password set.")}) - else: + if usable_password: try: hasher = identify_hasher(value) except ValueError: @@ -53,7 +52,12 @@ class ReadOnlyPasswordHashWidget(forms.Widget): else: for key, value_ in hasher.safe_summary(value).items(): summary.append({"label": gettext(key), "value": value_}) + else: + summary.append({"label": gettext("No password set.")}) context["summary"] = summary + context["button_label"] = ( + _("Reset password") if usable_password else _("Set password") + ) return context def id_for_label(self, id_): @@ -253,9 +257,8 @@ class UserChangeForm(forms.ModelForm): password = ReadOnlyPasswordHashField( label=_("Password"), help_text=_( - "Raw passwords are not stored, so there is no way to see this " - "user’s password, but you can change or unset the password using " - 'this form.' + "Raw passwords are not stored, so there is no way to see " + "the user’s password." ), ) @@ -271,11 +274,8 @@ class UserChangeForm(forms.ModelForm): if self.instance and not self.instance.has_usable_password(): password.help_text = _( "Enable password-based authentication for this user by setting a " - 'password using this form.' + "password." ) - password.help_text = password.help_text.format( - f"../../{self.instance.pk}/password/" - ) user_permissions = self.fields.get("user_permissions") if user_permissions: user_permissions.queryset = user_permissions.queryset.select_related( diff --git a/django/contrib/auth/templates/auth/widgets/read_only_password_hash.html b/django/contrib/auth/templates/auth/widgets/read_only_password_hash.html index c73042b18f9..e95fa3e9dab 100644 --- a/django/contrib/auth/templates/auth/widgets/read_only_password_hash.html +++ b/django/contrib/auth/templates/auth/widgets/read_only_password_hash.html @@ -1,5 +1,8 @@ -{% for entry in summary %} -{{ entry.label }}{% if entry.value %}: {{ entry.value }}{% endif %} -{% endfor %} +

+ {% for entry in summary %} + {{ entry.label }}{% if entry.value %}: {{ entry.value }}{% endif %} + {% endfor %} +

+

{{ button_label }}

diff --git a/docs/releases/5.1.txt b/docs/releases/5.1.txt index fbb05d75487..a07f8942a5a 100644 --- a/docs/releases/5.1.txt +++ b/docs/releases/5.1.txt @@ -57,6 +57,12 @@ Minor features :func:`~.django.contrib.auth.decorators.user_passes_test` decorators now support wrapping asynchronous view functions. +* ``ReadOnlyPasswordHashWidget`` now includes a button to reset the user's + password, which replaces the link previously embedded in the + ``ReadOnlyPasswordHashField``'s help text, improving the overall + accessibility of the + :class:`~django.contrib.auth.forms.UserChangeForm`. + :mod:`django.contrib.contenttypes` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/auth_tests/test_forms.py b/tests/auth_tests/test_forms.py index 373a9819559..b44f1edb242 100644 --- a/tests/auth_tests/test_forms.py +++ b/tests/auth_tests/test_forms.py @@ -1023,34 +1023,42 @@ class UserChangeFormTest(TestDataMixin, TestCase): self.assertEqual(form.initial["password"], form["password"].value()) @override_settings(ROOT_URLCONF="auth_tests.urls_admin") - def test_link_to_password_reset_in_helptext_via_to_field(self): + def test_link_to_password_reset_in_user_change_form(self): cases = [ ( "testclient", - 'you can change or unset the password using ', + "Raw passwords are not stored, so there is no way to see " + "the user’s password.", + "Reset password", ), ( "unusable_password", - "Enable password-based authentication for this user by setting " - 'a password using this form.', + "Enable password-based authentication for this user by setting a " + "password.", + "Set password", ), ] - for username, expected_help_text in cases: + password_reset_link = r'([^<]*)' + for username, expected_help_text, expected_button_label in cases: with self.subTest(username=username): user = User.objects.get(username=username) form = UserChangeForm(data={}, instance=user) password_help_text = form.fields["password"].help_text - matches = re.search(expected_help_text, password_help_text) + self.assertEqual(password_help_text, expected_help_text) + matches = re.search(password_reset_link, form.as_p()) + self.assertIsNotNone(matches) + self.assertEqual(len(matches.groups()), 2) url_prefix = f"admin:{user._meta.app_label}_{user._meta.model_name}" # URL to UserChangeForm in admin via to_field (instead of pk). - user_change_url = reverse(f"{url_prefix}_change", args=(user.username,)) + user_change_url = reverse(f"{url_prefix}_change", args=(user.pk,)) joined_url = urllib.parse.urljoin(user_change_url, matches.group(1)) pw_change_url = reverse( f"{url_prefix}_password_change", args=(user.pk,) ) self.assertEqual(joined_url, pw_change_url) + self.assertEqual(matches.group(2), expected_button_label) def test_custom_form(self): class CustomUserChangeForm(UserChangeForm): @@ -1345,11 +1353,14 @@ class ReadOnlyPasswordHashTest(SimpleTestCase): self.assertHTMLEqual( widget.render("name", value, {"id": "id_password"}), '
' + "

" " algorithm: pbkdf2_sha256" " iterations: 100000" " salt: a6Pucb******" " hash: " " WmCkn9**************************************" + "

" + '

Reset password

' "
", ) diff --git a/tests/auth_tests/test_views.py b/tests/auth_tests/test_views.py index d6bf6fbf528..53e33785b07 100644 --- a/tests/auth_tests/test_views.py +++ b/tests/auth_tests/test_views.py @@ -1442,7 +1442,7 @@ class ChangelistTests(MessagesTestMixin, AuthViewsTestCase): response = self.client.get(user_change_url) # Test the link inside password field help_text. rel_link = re.search( - r'change or unset the password using this form', + r'Reset password', response.content.decode(), )[1] self.assertEqual(urljoin(user_change_url, rel_link), password_change_url) @@ -1538,7 +1538,7 @@ class ChangelistTests(MessagesTestMixin, AuthViewsTestCase): response = self.client.get(user_change_url) # Test the link inside password field help_text. rel_link = re.search( - r'by setting a password using this form', + r'Set password', response.content.decode(), )[1] self.assertEqual(urljoin(user_change_url, rel_link), password_change_url)