From 0ebf14aad8bf3427a79fff4be12c55dab43c2fdc Mon Sep 17 00:00:00 2001 From: Andy Byers Date: Tue, 19 May 2026 10:54:28 +0100 Subject: [PATCH 1/3] fix: #4642 persistent inline errors on password update Wrap edit-profile password change in PasswordChangeForm so validation errors (current-password mismatch, confirmation mismatch, policy failures) render inline via the form errors partial and the page stays put on failure instead of redirecting with disappearing toasts. Adds the password_rules partial so requirements are visible upfront. --- src/core/forms/forms.py | 54 +++++++++++++++++++ src/core/views.py | 34 ++++-------- .../admin/core/accounts/edit_profile.html | 29 +++------- 3 files changed, 71 insertions(+), 46 deletions(-) diff --git a/src/core/forms/forms.py b/src/core/forms/forms.py index 3ea467b4ae..defabf7705 100755 --- a/src/core/forms/forms.py +++ b/src/core/forms/forms.py @@ -246,6 +246,60 @@ def save(self, commit=True): return user +class PasswordChangeForm(forms.Form): + """ + A form for changing the password of an already-authenticated user. + + Validates the current password, confirms the two new-password fields + match, and runs the press password policy check so that all failures + are surfaced as inline form errors rather than disappearing toast + messages. + """ + + old_password = forms.CharField( + label=_("Current password"), + widget=forms.PasswordInput, + ) + new_password_one = forms.CharField( + label=_("New password"), + widget=forms.PasswordInput, + ) + new_password_two = forms.CharField( + label=_("Confirm new password"), + widget=forms.PasswordInput, + ) + + def __init__(self, *args, user=None, request=None, **kwargs): + super().__init__(*args, **kwargs) + self.user = user + self.request = request + + def clean_old_password(self): + value = self.cleaned_data.get("old_password") + if self.user and not self.user.check_password(value): + raise ValidationError(_("Current password is incorrect.")) + return value + + def clean(self): + cleaned = super().clean() + new_one = cleaned.get("new_password_one") + new_two = cleaned.get("new_password_two") + + if new_one and new_two and new_one != new_two: + self.add_error("new_password_two", _("Passwords do not match.")) + + if new_one and self.user and self.request: + problems = self.user.password_policy_check(self.request, new_one) + for problem in problems: + self.add_error("new_password_one", _("Password not updated: ") + str(problem)) + + return cleaned + + def save(self): + self.user.set_password(self.cleaned_data["new_password_one"]) + self.user.save() + + class EditAccountForm(forms.ModelForm): """ A form for modifying profile details of an account, such as diff --git a/src/core/views.py b/src/core/views.py index 24706c8c8d..92e219e1f5 100755 --- a/src/core/views.py +++ b/src/core/views.py @@ -546,6 +546,7 @@ def edit_profile(request): """ user = request.user form = forms.EditAccountForm(instance=user) + password_form = forms.PasswordChangeForm(user=user, request=request) send_reader_notifications = False next_url = request.GET.get("next", "") @@ -584,33 +585,15 @@ def edit_profile(request): ) elif "change_password" in request.POST: - old_password = request.POST.get("current_password") - new_pass_one = request.POST.get("new_password_one") - new_pass_two = request.POST.get("new_password_two") - - if old_password and request.user.check_password(old_password): - if new_pass_one == new_pass_two: - problems = request.user.password_policy_check(request, new_pass_one) - if not problems: - request.user.set_password(new_pass_one) - request.user.save() - messages.add_message( - request, messages.SUCCESS, _("Password updated.") - ) - else: - [ - messages.add_message(request, messages.INFO, problem) - for problem in problems - ] - else: - messages.add_message( - request, messages.WARNING, _("Passwords do not match") - ) - - else: + password_form = forms.PasswordChangeForm( + request.POST, user=request.user, request=request + ) + if password_form.is_valid(): + password_form.save() messages.add_message( - request, messages.WARNING, _("Old password is not correct.") + request, messages.SUCCESS, _("Password updated.") ) + return redirect(reverse("core_edit_profile")) elif "subscribe" in request.POST and send_reader_notifications: request.user.add_account_role( @@ -663,6 +646,7 @@ def edit_profile(request): template = "admin/core/accounts/edit_profile.html" context = { "form": form, + "password_form": password_form, "staff_group_membership_form": staff_group_membership_form, "user_to_edit": user, "send_reader_notifications": send_reader_notifications, diff --git a/src/templates/admin/core/accounts/edit_profile.html b/src/templates/admin/core/accounts/edit_profile.html index 61711894cd..74c8323491 100644 --- a/src/templates/admin/core/accounts/edit_profile.html +++ b/src/templates/admin/core/accounts/edit_profile.html @@ -83,30 +83,17 @@

{% trans "Update Password" %}

You can update your password by entering your existing password plus your new password. {% endblocktrans %}

+ {% trans "Password Requirements" %} + + {% include "admin/elements/forms/errors.html" with form=password_form %}
{% csrf_token %}
-
- - -
-
- - -
-
- - -
+ {% include "admin/elements/forms/field.html" with field=password_form.old_password %} + {% include "admin/elements/forms/field.html" with field=password_form.new_password_one %} + {% include "admin/elements/forms/field.html" with field=password_form.new_password_two %}