Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed #34977: Make change password form reachable through button #17489

Merged
merged 1 commit into from Mar 28, 2024

Conversation

fsbraun
Copy link
Contributor

@fsbraun fsbraun commented Nov 19, 2023

https://code.djangoproject.com/ticket/34977

To improve usability and accessibility, this PR replaces the link to change a user's password within the help text of the password field of the change user form by a button.

image

Besides an improved user experience, I would expect this PR to also improve accessibility. But since I am anything but an expert on accessibility, I would like to ask the accessibility team to review and give hints on hot to improve accessibility.

@nessita
Copy link
Contributor

nessita commented Jan 5, 2024

I would go a different route to solve this issue, specially because I believe that the best practice for help texts is not to use markup at all (more so for easing translations).

So my proposal is to:

  • Re-phrase the help texts so they are complete sentences that make sense but without any actionable links nor buttons.
  • Add the action button next to the help text.

This would also have the (nice) side effect to separate "form concerns" (ie help texts) from UI elements that want to be displayed together in a template.

Initial proposal (to be discussed with the accessibility team):

diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py
index 5398af4dad..30d28cbe00 100644
--- a/django/contrib/auth/forms.py
+++ b/django/contrib/auth/forms.py
@@ -179,9 +179,8 @@ class UserChangeForm(forms.ModelForm):
     password = ReadOnlyPasswordHashField(
         label=_("Password"),
         help_text=_(
-            "<p>Raw passwords are not stored, so there is no way to see this "
-            "user’s password, but you can change the password here:</p>"
-            '<p><a class="button" role="button" href="{}">Change password</a></p>'
+            "Raw passwords are not stored, so there is no way to see this "
+            "user’s password."
         ),
     )
 
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 c73042b18f..f88d2e1ba4 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 @@
+{% load i18n %}
+
 <div{% include 'django/forms/widgets/attrs.html' %}>
 {% for entry in summary %}
 <strong>{{ entry.label }}</strong>{% if entry.value %}: <bdi>{{ entry.value }}</bdi>{% endif %}
 {% endfor %}
+<a class="button" role="button" href="../password/">{% trans "Change password" %}</a>
 </div>

Draft UI:
image

@nessita nessita removed their request for review January 5, 2024 23:04
@fsbraun
Copy link
Contributor Author

fsbraun commented Jan 8, 2024

Good idea! Once we're touching ReadOnlyPasswordHashField I'd also like to change the formatting that the button always will be on the right. Depending on viewport width, the button will otherwise break into the next line.

@thibaudcolas thibaudcolas self-requested a review January 12, 2024 08:06
@nessita
Copy link
Contributor

nessita commented Mar 7, 2024

@fsbraun Would you have time to update this PR by rebasing onto main and ensuring is ready for re-review? Thanks!

@django/accessibility Would you have any opinion on my proposal above?

/remind me to re-check the status of this PR in 3 weeks

Copy link

github-actions bot commented Mar 7, 2024

@nessita set a reminder for 3/28/2024

@fsbraun fsbraun force-pushed the ticket_34977 branch 3 times, most recently from 52c7f22 to 64844a9 Compare March 11, 2024 15:24
@fsbraun
Copy link
Contributor Author

fsbraun commented Mar 11, 2024

@nessita I've rebased and rewritten to reflect your first comment:

  • Help texts do not contain markup or buttons
  • Widget now contains the button for the link
  • In private exchange with @sabderemane we concluded that a role attribute for the button is not advisable from a accessibility viewpoint

@nessita
Copy link
Contributor

nessita commented Mar 11, 2024

@nessita I've rebased and rewritten to reflect your first comment:

* Help texts do not contain markup or buttons

* Widget now contains the button for the link

* In private exchange with @sabderemane we concluded that a `role` attribute for the button is not advisable from a accessibility viewpoint

Thank you @fsbraun ! Please remember to update the ticket flags accordingly.

Copy link
Contributor

@nessita nessita left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall looks good!

Added some comments, and also please note that this work needs documentation updates, specifically we should review if any of the existing docs mention the link, and also we need release notes in the 5.1.txt file.

Thank you @fsbraun !

django/contrib/admin/static/admin/css/widgets.css Outdated Show resolved Hide resolved
@@ -54,11 +54,17 @@ def get_context(self, name, value, attrs):
for key, value_ in hasher.safe_summary(value).items():
summary.append({"label": gettext(key), "value": value_})
context["summary"] = summary
context["usable_password"] = len(summary) > 1
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe that is cleaner to define using value.startswith(UNUSABLE_PASSWORD_PREFIX). So, something like:

        usable_password = not value.startswith(UNUSABLE_PASSWORD_PREFIX)
        if not value or not usable_password:
            summary.append({"label": gettext("No password set.")})
        else:
            # ...
        context["usable_password"] = usable_password

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On a second thought, since this is the Widget class, shall we instead add to the context the label and action of the button? My initial thinking is that approach would be cleaner and simpler in the template. What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nessita What's the right way to get the admin namespace from within the widget? I would need it to reverse the set password URL. The template uses a ../bla which indeed is a bit hacky. The reason might be that the admin namespace is not readily available.

Admin forms get the admin site passed to their __init__ function. But that pattern would require to change the signature of the UserChangeForm which I would like to avoid.

I have moved the button label from the template to the get_context method, however.

css = {
"all": ("admin/css/widgets.css",),
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this really needed? I have removed it and I still see the same formatting in the admin 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other widgets on the user change form load the widgets.css, so removing it here will indeed not change anything.

I would still like to keep it, since the widget needs it for margins. If we removed the css from the widget it will fail to display properly if other widgets on the change from do not load the widgets.css for whatever reason.

Copy link
Contributor Author

@fsbraun fsbraun Mar 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternative: Have your own auth/css/widgets.css? (Once might argue this is cleaner since it does not cross app boundaries.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the extra details. My advice then is as follows (I'll make a new commit with these):

  • Change the HTML to make use of semantic elements such as <p>
  • Remove the new CSS classes and custom CSS rule
  • Let users of this widget define their own CSS by including the widget in whichever CSS context is relevant for them.

@fsbraun
Copy link
Contributor Author

fsbraun commented Mar 27, 2024

@nessita Thank you for your comments!

I added a bullet point to the release notes. I searched the docs and did not find a reference to the link. Have I missed something?

I also implemented some of your comments where I did not see any roadblocks. I added comments to the others. Getting the URL of the change password action from within the widget seems tricky to me.

@fsbraun
Copy link
Contributor Author

fsbraun commented Mar 27, 2024

Building docs fails apparently since it cannot locate the class django.contrib.auth.forms.ReadOnlyPasswordHashWidget . I am not sure why. What am I missing?

Copy link
Contributor

@nessita nessita left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, thanks! I will make a few tweaks and merge. 🏆

django/contrib/auth/forms.py Outdated Show resolved Hide resolved
css = {
"all": ("admin/css/widgets.css",),
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the extra details. My advice then is as follows (I'll make a new commit with these):

  • Change the HTML to make use of semantic elements such as <p>
  • Remove the new CSS classes and custom CSS rule
  • Let users of this widget define their own CSS by including the widget in whichever CSS context is relevant for them.

…cing the reset password link with a button.

Co-authored-by: Natalia <124304+nessita@users.noreply.github.com>
@nessita nessita merged commit 944745a into django:main Mar 28, 2024
35 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
3 participants