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 "common/elements/password_rules.html" %}
+
+ {% include "admin/elements/forms/errors.html" with form=password_form %}