Repository: wagtail/wagtail

Issue:
Login template does not respect non_fields_errors display
When using a custom `WAGTAILADMIN_USER_LOGIN_FORM`, you can't set form-wide errors as they will always be displayed as `Your {{ username_field }} and password didn't match. Please try again.` from `"wagtailadmin/login.html"`

As the default LoginForm (`wagtail.admin.forms.auth.LoginForm`) subclasses `django.contrib.auth.forms.AuthenticationForm` which already has an `'invalid_login'` error message (that is usename_field-aware) we could just use that, but if we want this particular message we could just override this error message"


Discussion:

Resolving patch:
diff --git a/wagtail/admin/forms/auth.py b/wagtail/admin/forms/auth.py
--- a/wagtail/admin/forms/auth.py
+++ b/wagtail/admin/forms/auth.py
@@ -18,6 +18,13 @@ class LoginForm(AuthenticationForm):
 
     remember = forms.BooleanField(required=False)
 
+    error_messages = {
+        **AuthenticationForm.error_messages,
+        "invalid_login": gettext_lazy(
+            "Your %(username_field)s and password didn't match. Please try again."
+        ),
+    }
+
     def __init__(self, request=None, *args, **kwargs):
         super().__init__(request=request, *args, **kwargs)
         self.fields["username"].widget.attrs["placeholder"] = gettext_lazy(
@@ -31,6 +38,13 @@ def extra_fields(self):
             if field_name not in ["username", "password", "remember"]:
                 yield field_name, self[field_name]
 
+    def get_invalid_login_error(self):
+        return forms.ValidationError(
+            self.error_messages["invalid_login"],
+            code="invalid_login",
+            params={"username_field": self.username_field.verbose_name},
+        )
+
 
 class PasswordResetForm(DjangoPasswordResetForm):
     email = forms.EmailField(


Test patch:
diff --git a/wagtail/admin/tests/test_forms.py b/wagtail/admin/tests/test_forms.py
--- a/wagtail/admin/tests/test_forms.py
+++ b/wagtail/admin/tests/test_forms.py
@@ -7,6 +7,12 @@
 class CustomLoginForm(LoginForm):
     captcha = CharField(label="Captcha", help_text="should be in extra_fields()")
 
+    def clean(self):
+        cleaned_data = super().clean()
+        if not cleaned_data.get("captcha") == "solved":
+            self.add_error(None, "Captcha is invalid")
+        return cleaned_data
+
 
 class CustomPasswordResetForm(PasswordResetForm):
     captcha = CharField(label="Captcha", help_text="should be in extra_fields()")
diff --git a/wagtail/admin/tests/test_views.py b/wagtail/admin/tests/test_views.py
--- a/wagtail/admin/tests/test_views.py
+++ b/wagtail/admin/tests/test_views.py
@@ -1,8 +1,9 @@
+from django.contrib.auth import get_user_model
 from django.test import TestCase, override_settings
 from django.urls import reverse
 
 from wagtail.admin.forms.auth import PasswordResetForm
-from wagtail.admin.tests.test_forms import CustomPasswordResetForm
+from wagtail.admin.tests.test_forms import CustomLoginForm, CustomPasswordResetForm
 from wagtail.models import Page
 from wagtail.test.utils import WagtailTestUtils
 
@@ -103,6 +104,37 @@ def test_login_page_renders_extra_fields(self):
             html=True,
         )
 
+    @override_settings(
+        WAGTAILADMIN_USER_LOGIN_FORM="wagtail.admin.tests.test_forms.CustomLoginForm"
+    )
+    def test_login_page_renders_custom_form_non_field_errors(self):
+        response = self.client.post(
+            reverse("wagtailadmin_login"),
+            {
+                "username": "test@email.com",
+                "password": "password",
+                "captcha": "unsolved",
+            },
+        )
+        self.assertContains(response, "Captcha is invalid")
+
+    @override_settings(
+        WAGTAILADMIN_USER_LOGIN_FORM="wagtail.admin.tests.test_forms.CustomLoginForm"
+    )
+    def test_login_page_renders_custom_form_login_error(self):
+        response = self.client.post(
+            reverse("wagtailadmin_login"),
+            {
+                "username": "test@email.com",
+                "password": "bad-password",
+                "captcha": "solved",
+            },
+        )
+        msg = CustomLoginForm.error_messages["invalid_login"] % {
+            "username_field": get_user_model().USERNAME_FIELD
+        }
+        self.assertContains(response, msg, html=True)
+
     def test_session_expire_on_browser_close(self):
         self.client.post(
             reverse("wagtailadmin_login"),