From 6489eae6cf7ba587cf84cba334dd66b0d6f0a986 Mon Sep 17 00:00:00 2001
From: Matt Westcott <matt@west.co.tt>
Date: Fri, 24 Jan 2025 01:49:26 +0000
Subject: [PATCH] Prevent database error when calling permission_order.register
 on app ready

Fixes #12742

Previously, `permission_order.register` performed a database lookup for the content type. This is invalid if called from an app `ready` method as the documentation suggests, because this may run before the database has been initialised. Instead, `register` now queues up the arguments it receives, and the content type lookup is constructed lazily on first call to `get_content_type_order_lookup` (which happens when the group edit view is requested).
---
 wagtail/test/snippets/apps.py                  | 10 ++++++++++
 wagtail/users/permission_order.py              | 18 ++++++++++++++++--
 .../users/templatetags/wagtailusers_tags.py    |  5 +++--
 3 files changed, 29 insertions(+), 4 deletions(-)

diff --git a/wagtail/test/snippets/apps.py b/wagtail/test/snippets/apps.py
index 13e6f1bc5f..2e21040227 100644
--- a/wagtail/test/snippets/apps.py
+++ b/wagtail/test/snippets/apps.py
@@ -7,3 +7,13 @@ class WagtailSnippetsTestsAppConfig(AppConfig):
     name = "wagtail.test.snippets"
     label = "snippetstests"
     verbose_name = _("Wagtail snippets tests")
+
+    def ready(self):
+        # Test registration of permission order within the group permissions view,
+        # as per https://docs.wagtail.org/en/stable/extending/customizing_group_views.html#customizing-the-group-editor-permissions-ordering
+        # Invoking `register` from `ready` confirms that it does not perform any database queries -
+        # if it did, it would fail (on a standard test run without --keepdb at least) because the
+        # test database hasn't been migrated yet.
+        from wagtail.users.permission_order import register
+
+        register("snippetstests.fancysnippet", order=999)
diff --git a/wagtail/users/permission_order.py b/wagtail/users/permission_order.py
index 2314deff3f..fb4fac507b 100644
--- a/wagtail/users/permission_order.py
+++ b/wagtail/users/permission_order.py
@@ -2,6 +2,7 @@ from django.contrib.contenttypes.models import ContentType
 
 from wagtail.coreutils import resolve_model_string
 
+content_types_to_register = []
 CONTENT_TYPE_ORDER = {}
 
 
@@ -13,5 +14,18 @@ def register(model, **kwargs):
     """
     order = kwargs.pop("order", None)
     if order is not None:
-        content_type = ContentType.objects.get_for_model(resolve_model_string(model))
-        CONTENT_TYPE_ORDER[content_type.id] = order
+        # We typically call this at application startup, when the database may not be ready,
+        # and so we can't look up the content type yet. Instead we will queue up the
+        # (model, order) pair to be processed when the lookup is requested.
+        content_types_to_register.append((model, order))
+
+
+def get_content_type_order_lookup():
+    if content_types_to_register:
+        for model, order in content_types_to_register:
+            content_type = ContentType.objects.get_for_model(
+                resolve_model_string(model)
+            )
+            CONTENT_TYPE_ORDER[content_type.id] = order
+        content_types_to_register.clear()
+    return CONTENT_TYPE_ORDER
diff --git a/wagtail/users/templatetags/wagtailusers_tags.py b/wagtail/users/templatetags/wagtailusers_tags.py
index 3c2ca0e2df..7d39f38221 100644
--- a/wagtail/users/templatetags/wagtailusers_tags.py
+++ b/wagtail/users/templatetags/wagtailusers_tags.py
@@ -11,7 +11,7 @@ from django.utils.translation import gettext_noop
 from wagtail import hooks
 from wagtail.admin.models import Admin
 from wagtail.coreutils import accepts_kwarg
-from wagtail.users.permission_order import CONTENT_TYPE_ORDER
+from wagtail.users.permission_order import get_content_type_order_lookup
 from wagtail.utils.deprecation import RemovedInWagtail70Warning
 
 register = template.Library()
@@ -96,9 +96,10 @@ def format_permissions(permission_bound_field):
     # get a distinct and ordered list of the content types that these permissions relate to.
     # relies on Permission model default ordering, dict.fromkeys() retaining that order
     # from the queryset, and the stability of sorted().
+    content_type_order = get_content_type_order_lookup()
     content_type_ids = sorted(
         dict.fromkeys(permissions.values_list("content_type_id", flat=True)),
-        key=lambda ct: CONTENT_TYPE_ORDER.get(ct, float("inf")),
+        key=lambda ct: content_type_order.get(ct, float("inf")),
     )
 
     # iterate over permission_bound_field to build a lookup of individual renderable