We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Inconsistent use of model verbose_name in permissions UI
### Issue Summary

The object permissions UI doesn't show a model's [`verbose_name`](https://docs.djangoproject.com/en/4.2/ref/models/options/#verbose-name) if it differs from the model's name when it was created.

### Steps to Reproduce

Start a new project with `wagtail start myproject` and create a simple model like the following:

```py
from django.db import models

from wagtail.snippets.models import register_snippet

@register_snippet
class MyModel(models.Model):
    text = models.TextField()
```

In the group edit view, permissions render like this; note "My model" and "Can view":

![image](https://github.com/wagtail/wagtail/assets/654645/afca283f-99f6-4752-a926-6aed44365832)

Now, give the model a different `verbose_name`:

```py
@register_snippet
class MyModel(models.Model):
    text = models.TextField()

    class Meta:
        verbose_name = "Something else"
```

Now, the permissions render in a surprising way; note "Something else" and "Can view my model":

![image](https://github.com/wagtail/wagtail/assets/654645/8735d7b4-a5dc-479c-af6d-ac62aba6d6c0)

As a user, I would expect to see "Can view" the way all other objects are typically listed.

This behavior seems to come from this line that defines the permission "name":

https://github.com/wagtail/wagtail/blob/a8bc03dd8aad9d15fd4d8e84ace1bc89cbe96053/wagtail/users/templatetags/wagtailusers_tags.py#L101-L103

This code assumes that a model's content type's name (`perm.content_type.name`) will exactly match the name on the permission object (`perm.name`). This is true as long as the model's verbose name hasn't been changed, but, [per the docs](https://docs.djangoproject.com/en/4.2/ref/contrib/contenttypes/#django.contrib.contenttypes.models.ContentType.name), `content_type.name` "is taken from the `verbose_name` attribute of the model". So if a model's verbose name is changed, that change will show up when you look at its content type.

The issue is that when a model is first created, its automatically-generated permissions are given names that contain the model's  name at that time. For example, the above `MyModel` class generated permissions like this:

```
110|28|add_mymodel|Can add my model
111|28|change_mymodel|Can change my model
112|28|delete_mymodel|Can delete my model
113|28|view_mymodel|Can view my model
```

In the above example, the code assumes that since the content type reports its name as `Something else`, the permission's name will be `Can view something else`, but this isn't a valid assumption. The permission's name is still what it was when it was created, `Can view my model`. 

To fix this, the code could lookup the model objects corresponding to the permission content types and use those to do the substitution, maybe something like this:

```diff
diff --git a/wagtail/users/templatetags/wagtailusers_tags.py b/wagtail/users/templatetags/wagtailusers_tags.py
index c188425ad0..3da81fe0dd 100644
--- a/wagtail/users/templatetags/wagtailusers_tags.py
+++ b/wagtail/users/templatetags/wagtailusers_tags.py
@@ -95,11 +95,15 @@ def format_permissions(permission_bound_field):
                 }
             else:
                 extra_perms_exist["custom"] = True
+                perm_model_class = perm.content_type.model_class()
                 custom_perms.append(
                     {
                         "perm": perm,
                         "name": re.sub(
-                            f"{perm.content_type.name}$", "", perm.name, flags=re.I
+                            f"{perm_model_class._meta.model_name}$",  "", perm.name, flags=re.I
                         ).strip(),
                         "selected": checkbox.data["selected"],
                     }
```

### Technical details

-   I have confirmed that this issue can be reproduced as described on a fresh Wagtail project: yes
-   Python version: 3.11.1
-   Django version: 4.0.10
-   Wagtail version: 5.2a0

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `wagtail/users/templatetags/wagtailusers_tags.py`
Content:
```
1 import itertools
2 from collections import defaultdict
3 
4 from django import template
5 
6 from wagtail import hooks
7 from wagtail.users.permission_order import CONTENT_TYPE_ORDER
8 
9 register = template.Library()
10 
11 
12 @register.inclusion_tag("wagtailusers/groups/includes/formatted_permissions.html")
13 def format_permissions(permission_bound_field):
14     """
15     Given a bound field with a queryset of Permission objects - which must be using
16     the CheckboxSelectMultiple widget - construct a list of dictionaries for 'objects':
17 
18     'objects': [
19         {
20             'object': name_of_some_content_object,
21             'add': checkbox,
22             'change': checkbox,
23             'delete': checkbox,
24             'publish': checkbox,  # only if the model extends DraftStateMixin
25             'custom': list_of_checkboxes_for_custom_permissions
26         },
27     ]
28 
29     and a list of other permissions:
30 
31     'others': [
32         (any_non_add_change_delete_permission, checkbox),
33     ]
34 
35     (where 'checkbox' is an object with a tag() method that renders the checkbox as HTML;
36     this is a BoundWidget on Django >=1.11)
37 
38     - and returns a table template formatted with this list.
39 
40     """
41     permissions = permission_bound_field.field._queryset
42     # get a distinct and ordered list of the content types that these permissions relate to.
43     # relies on Permission model default ordering, dict.fromkeys() retaining that order
44     # from the queryset, and the stability of sorted().
45     content_type_ids = sorted(
46         dict.fromkeys(permissions.values_list("content_type_id", flat=True)),
47         key=lambda ct: CONTENT_TYPE_ORDER.get(ct, float("inf")),
48     )
49 
50     # iterate over permission_bound_field to build a lookup of individual renderable
51     # checkbox objects
52     # checkbox.data['value'] gives a ModelChoiceIteratorValue
53     checkboxes_by_id = {
54         int(checkbox.data["value"].value): checkbox
55         for checkbox in permission_bound_field
56     }
57 
58     object_perms = []
59     other_perms = []
60 
61     # Permissions that are known by Wagtail, to be shown under their own columns.
62     # Other permissions will be shown under the "custom permissions" column.
63     main_permission_names = ["add", "change", "delete", "publish", "lock", "unlock"]
64 
65     # Only show the columns for these permissions if any of the model has them.
66     extra_perms_exist = {
67         "publish": False,
68         "lock": False,
69         "unlock": False,
70         "custom": False,
71     }
72     # Batch the permission query for all content types, then group by content type
73     # (instead of querying permissions for each content type separately)
74     content_perms_by_ct_id = defaultdict(list)
75     permissions = permissions.filter(content_type_id__in=content_type_ids)
76     for permission in permissions:
77         content_perms_by_ct_id[permission.content_type_id].append(permission)
78 
79     # Iterate using the sorted content_type_ids
80     for ct_id in content_type_ids:
81         content_perms = content_perms_by_ct_id[ct_id]
82         content_perms_dict = {}
83         custom_perms = []
84 
85         if content_perms[0].content_type.name == "admin":
86             perm = content_perms[0]
87             other_perms.append((perm, checkboxes_by_id[perm.id]))
88             continue
89 
90         for perm in content_perms:
91             content_perms_dict["object"] = perm.content_type.name
92             checkbox = checkboxes_by_id[perm.id]
93             # identify the main categories of permission, and assign to
94             # the relevant dict key, else bung in the 'custom_perms' list
95             permission_action = perm.codename.split("_", maxsplit=1)
96             permission_action = permission_action[permission_action[0].lower() == "can"]
97             permission_action = permission_action.rsplit(maxsplit=1)[0]
98 
99             if permission_action in main_permission_names:
100                 if permission_action in extra_perms_exist:
101                     extra_perms_exist[permission_action] = True
102                 content_perms_dict[permission_action] = {
103                     "perm": perm,
104                     "checkbox": checkbox,
105                 }
106             else:
107                 extra_perms_exist["custom"] = True
108                 custom_perms.append(
109                     {
110                         "perm": perm,
111                         "name": f"Can {permission_action}",
112                         "selected": checkbox.data["selected"],
113                     }
114                 )
115 
116         content_perms_dict["custom"] = custom_perms
117         object_perms.append(content_perms_dict)
118     return {
119         "object_perms": object_perms,
120         "other_perms": other_perms,
121         "extra_perms_exist": extra_perms_exist,
122     }
123 
124 
125 @register.inclusion_tag("wagtailadmin/shared/buttons.html", takes_context=True)
126 def user_listing_buttons(context, user):
127     button_hooks = hooks.get_hooks("register_user_listing_buttons")
128     buttons = sorted(
129         itertools.chain.from_iterable(hook(context, user) for hook in button_hooks)
130     )
131     return {"user": user, "buttons": buttons}
132
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/wagtail/users/templatetags/wagtailusers_tags.py b/wagtail/users/templatetags/wagtailusers_tags.py
--- a/wagtail/users/templatetags/wagtailusers_tags.py
+++ b/wagtail/users/templatetags/wagtailusers_tags.py
@@ -2,6 +2,9 @@
 from collections import defaultdict
 
 from django import template
+from django.contrib.auth import get_permission_codename
+from django.contrib.auth.models import Permission
+from django.utils.text import camel_case_to_spaces
 
 from wagtail import hooks
 from wagtail.users.permission_order import CONTENT_TYPE_ORDER
@@ -9,6 +12,45 @@
 register = template.Library()
 
 
+def normalize_permission_label(permission: Permission):
+    """
+    Strip model name from the end of the label, e.g. "Can deliver pizza" for a
+    Pizza model becomes "Can deliver". For permissions in the model's
+    Meta.default_permissions with default labels, also replace underscores
+    with spaces.
+
+    This is used to display custom model permissions in the admin.
+
+    See https://github.com/wagtail/wagtail/issues/10982.
+    """
+    label = permission.name
+    content_type = permission.content_type
+    model = content_type.model_class()
+    verbose_name = default_verbose_name = content_type.name
+
+    if model:
+        default_verbose_name = camel_case_to_spaces(model._meta.object_name)
+
+        # If it's in default_permissions and the label matches Django's default
+        # label, remove the model name from the end of the label. Also replace
+        # underscores with spaces, as Django uses the action internal name as-is
+        # for the permission label, which means it tends to be in snake_case.
+        for action in model._meta.default_permissions:
+            default_codename = get_permission_codename(action, model._meta)
+            is_default = permission.codename == default_codename
+            if is_default and permission.name.startswith(f"Can {action}"):
+                return f"Can {action.replace('_', ' ')}"
+
+    # For all other cases (including custom permissions), try to remove the
+    # verbose name from the end of the label. This only works if the label
+    # matches the current verbose name or Django's default verbose name.
+    for name in (default_verbose_name, verbose_name):
+        if label.lower().endswith(name.lower()):
+            return label[: -len(name)].strip()
+
+    return label
+
+
 @register.inclusion_tag("wagtailusers/groups/includes/formatted_permissions.html")
 def format_permissions(permission_bound_field):
     """
@@ -92,10 +134,7 @@
             checkbox = checkboxes_by_id[perm.id]
             # identify the main categories of permission, and assign to
             # the relevant dict key, else bung in the 'custom_perms' list
-            permission_action = perm.codename.split("_", maxsplit=1)
-            permission_action = permission_action[permission_action[0].lower() == "can"]
-            permission_action = permission_action.rsplit(maxsplit=1)[0]
-
+            permission_action = perm.codename.split("_")[0]
             if permission_action in main_permission_names:
                 if permission_action in extra_perms_exist:
                     extra_perms_exist[permission_action] = True
@@ -105,10 +144,12 @@
                 }
             else:
                 extra_perms_exist["custom"] = True
+                perm_name = normalize_permission_label(perm)
+
                 custom_perms.append(
                     {
                         "perm": perm,
-                        "name": f"Can {permission_action}",
+                        "name": perm_name,
                         "selected": checkbox.data["selected"],
                     }
                 )

{"golden_diff": "diff --git a/wagtail/users/templatetags/wagtailusers_tags.py b/wagtail/users/templatetags/wagtailusers_tags.py\n--- a/wagtail/users/templatetags/wagtailusers_tags.py\n+++ b/wagtail/users/templatetags/wagtailusers_tags.py\n@@ -2,6 +2,9 @@\n from collections import defaultdict\n \n from django import template\n+from django.contrib.auth import get_permission_codename\n+from django.contrib.auth.models import Permission\n+from django.utils.text import camel_case_to_spaces\n \n from wagtail import hooks\n from wagtail.users.permission_order import CONTENT_TYPE_ORDER\n@@ -9,6 +12,45 @@\n register = template.Library()\n \n \n+def normalize_permission_label(permission: Permission):\n+    \"\"\"\n+    Strip model name from the end of the label, e.g. \"Can deliver pizza\" for a\n+    Pizza model becomes \"Can deliver\". For permissions in the model's\n+    Meta.default_permissions with default labels, also replace underscores\n+    with spaces.\n+\n+    This is used to display custom model permissions in the admin.\n+\n+    See https://github.com/wagtail/wagtail/issues/10982.\n+    \"\"\"\n+    label = permission.name\n+    content_type = permission.content_type\n+    model = content_type.model_class()\n+    verbose_name = default_verbose_name = content_type.name\n+\n+    if model:\n+        default_verbose_name = camel_case_to_spaces(model._meta.object_name)\n+\n+        # If it's in default_permissions and the label matches Django's default\n+        # label, remove the model name from the end of the label. Also replace\n+        # underscores with spaces, as Django uses the action internal name as-is\n+        # for the permission label, which means it tends to be in snake_case.\n+        for action in model._meta.default_permissions:\n+            default_codename = get_permission_codename(action, model._meta)\n+            is_default = permission.codename == default_codename\n+            if is_default and permission.name.startswith(f\"Can {action}\"):\n+                return f\"Can {action.replace('_', ' ')}\"\n+\n+    # For all other cases (including custom permissions), try to remove the\n+    # verbose name from the end of the label. This only works if the label\n+    # matches the current verbose name or Django's default verbose name.\n+    for name in (default_verbose_name, verbose_name):\n+        if label.lower().endswith(name.lower()):\n+            return label[: -len(name)].strip()\n+\n+    return label\n+\n+\n @register.inclusion_tag(\"wagtailusers/groups/includes/formatted_permissions.html\")\n def format_permissions(permission_bound_field):\n     \"\"\"\n@@ -92,10 +134,7 @@\n             checkbox = checkboxes_by_id[perm.id]\n             # identify the main categories of permission, and assign to\n             # the relevant dict key, else bung in the 'custom_perms' list\n-            permission_action = perm.codename.split(\"_\", maxsplit=1)\n-            permission_action = permission_action[permission_action[0].lower() == \"can\"]\n-            permission_action = permission_action.rsplit(maxsplit=1)[0]\n-\n+            permission_action = perm.codename.split(\"_\")[0]\n             if permission_action in main_permission_names:\n                 if permission_action in extra_perms_exist:\n                     extra_perms_exist[permission_action] = True\n@@ -105,10 +144,12 @@\n                 }\n             else:\n                 extra_perms_exist[\"custom\"] = True\n+                perm_name = normalize_permission_label(perm)\n+\n                 custom_perms.append(\n                     {\n                         \"perm\": perm,\n-                        \"name\": f\"Can {permission_action}\",\n+                        \"name\": perm_name,\n                         \"selected\": checkbox.data[\"selected\"],\n                     }\n                 )\n", "issue": "Inconsistent use of model verbose_name in permissions UI\n### Issue Summary\r\n\r\nThe object permissions UI doesn't show a model's [`verbose_name`](https://docs.djangoproject.com/en/4.2/ref/models/options/#verbose-name) if it differs from the model's name when it was created.\r\n\r\n### Steps to Reproduce\r\n\r\nStart a new project with `wagtail start myproject` and create a simple model like the following:\r\n\r\n```py\r\nfrom django.db import models\r\n\r\nfrom wagtail.snippets.models import register_snippet\r\n\r\n@register_snippet\r\nclass MyModel(models.Model):\r\n    text = models.TextField()\r\n```\r\n\r\nIn the group edit view, permissions render like this; note \"My model\" and \"Can view\":\r\n\r\n![image](https://github.com/wagtail/wagtail/assets/654645/afca283f-99f6-4752-a926-6aed44365832)\r\n\r\nNow, give the model a different `verbose_name`:\r\n\r\n```py\r\n@register_snippet\r\nclass MyModel(models.Model):\r\n    text = models.TextField()\r\n\r\n    class Meta:\r\n        verbose_name = \"Something else\"\r\n```\r\n\r\nNow, the permissions render in a surprising way; note \"Something else\" and \"Can view my model\":\r\n\r\n![image](https://github.com/wagtail/wagtail/assets/654645/8735d7b4-a5dc-479c-af6d-ac62aba6d6c0)\r\n\r\nAs a user, I would expect to see \"Can view\" the way all other objects are typically listed.\r\n\r\nThis behavior seems to come from this line that defines the permission \"name\":\r\n\r\nhttps://github.com/wagtail/wagtail/blob/a8bc03dd8aad9d15fd4d8e84ace1bc89cbe96053/wagtail/users/templatetags/wagtailusers_tags.py#L101-L103\r\n\r\nThis code assumes that a model's content type's name (`perm.content_type.name`) will exactly match the name on the permission object (`perm.name`). This is true as long as the model's verbose name hasn't been changed, but, [per the docs](https://docs.djangoproject.com/en/4.2/ref/contrib/contenttypes/#django.contrib.contenttypes.models.ContentType.name), `content_type.name` \"is taken from the `verbose_name` attribute of the model\". So if a model's verbose name is changed, that change will show up when you look at its content type.\r\n\r\nThe issue is that when a model is first created, its automatically-generated permissions are given names that contain the model's  name at that time. For example, the above `MyModel` class generated permissions like this:\r\n\r\n```\r\n110|28|add_mymodel|Can add my model\r\n111|28|change_mymodel|Can change my model\r\n112|28|delete_mymodel|Can delete my model\r\n113|28|view_mymodel|Can view my model\r\n```\r\n\r\nIn the above example, the code assumes that since the content type reports its name as `Something else`, the permission's name will be `Can view something else`, but this isn't a valid assumption. The permission's name is still what it was when it was created, `Can view my model`. \r\n\r\nTo fix this, the code could lookup the model objects corresponding to the permission content types and use those to do the substitution, maybe something like this:\r\n\r\n```diff\r\ndiff --git a/wagtail/users/templatetags/wagtailusers_tags.py b/wagtail/users/templatetags/wagtailusers_tags.py\r\nindex c188425ad0..3da81fe0dd 100644\r\n--- a/wagtail/users/templatetags/wagtailusers_tags.py\r\n+++ b/wagtail/users/templatetags/wagtailusers_tags.py\r\n@@ -95,11 +95,15 @@ def format_permissions(permission_bound_field):\r\n                 }\r\n             else:\r\n                 extra_perms_exist[\"custom\"] = True\r\n+                perm_model_class = perm.content_type.model_class()\r\n                 custom_perms.append(\r\n                     {\r\n                         \"perm\": perm,\r\n                         \"name\": re.sub(\r\n-                            f\"{perm.content_type.name}$\", \"\", perm.name, flags=re.I\r\n+                            f\"{perm_model_class._meta.model_name}$\",  \"\", perm.name, flags=re.I\r\n                         ).strip(),\r\n                         \"selected\": checkbox.data[\"selected\"],\r\n                     }\r\n```\r\n\r\n### Technical details\r\n\r\n-   I have confirmed that this issue can be reproduced as described on a fresh Wagtail project: yes\r\n-   Python version: 3.11.1\r\n-   Django version: 4.0.10\r\n-   Wagtail version: 5.2a0\n", "before_files": [{"content": "import itertools\nfrom collections import defaultdict\n\nfrom django import template\n\nfrom wagtail import hooks\nfrom wagtail.users.permission_order import CONTENT_TYPE_ORDER\n\nregister = template.Library()\n\n\n@register.inclusion_tag(\"wagtailusers/groups/includes/formatted_permissions.html\")\ndef format_permissions(permission_bound_field):\n    \"\"\"\n    Given a bound field with a queryset of Permission objects - which must be using\n    the CheckboxSelectMultiple widget - construct a list of dictionaries for 'objects':\n\n    'objects': [\n        {\n            'object': name_of_some_content_object,\n            'add': checkbox,\n            'change': checkbox,\n            'delete': checkbox,\n            'publish': checkbox,  # only if the model extends DraftStateMixin\n            'custom': list_of_checkboxes_for_custom_permissions\n        },\n    ]\n\n    and a list of other permissions:\n\n    'others': [\n        (any_non_add_change_delete_permission, checkbox),\n    ]\n\n    (where 'checkbox' is an object with a tag() method that renders the checkbox as HTML;\n    this is a BoundWidget on Django >=1.11)\n\n    - and returns a table template formatted with this list.\n\n    \"\"\"\n    permissions = permission_bound_field.field._queryset\n    # get a distinct and ordered list of the content types that these permissions relate to.\n    # relies on Permission model default ordering, dict.fromkeys() retaining that order\n    # from the queryset, and the stability of sorted().\n    content_type_ids = sorted(\n        dict.fromkeys(permissions.values_list(\"content_type_id\", flat=True)),\n        key=lambda ct: CONTENT_TYPE_ORDER.get(ct, float(\"inf\")),\n    )\n\n    # iterate over permission_bound_field to build a lookup of individual renderable\n    # checkbox objects\n    # checkbox.data['value'] gives a ModelChoiceIteratorValue\n    checkboxes_by_id = {\n        int(checkbox.data[\"value\"].value): checkbox\n        for checkbox in permission_bound_field\n    }\n\n    object_perms = []\n    other_perms = []\n\n    # Permissions that are known by Wagtail, to be shown under their own columns.\n    # Other permissions will be shown under the \"custom permissions\" column.\n    main_permission_names = [\"add\", \"change\", \"delete\", \"publish\", \"lock\", \"unlock\"]\n\n    # Only show the columns for these permissions if any of the model has them.\n    extra_perms_exist = {\n        \"publish\": False,\n        \"lock\": False,\n        \"unlock\": False,\n        \"custom\": False,\n    }\n    # Batch the permission query for all content types, then group by content type\n    # (instead of querying permissions for each content type separately)\n    content_perms_by_ct_id = defaultdict(list)\n    permissions = permissions.filter(content_type_id__in=content_type_ids)\n    for permission in permissions:\n        content_perms_by_ct_id[permission.content_type_id].append(permission)\n\n    # Iterate using the sorted content_type_ids\n    for ct_id in content_type_ids:\n        content_perms = content_perms_by_ct_id[ct_id]\n        content_perms_dict = {}\n        custom_perms = []\n\n        if content_perms[0].content_type.name == \"admin\":\n            perm = content_perms[0]\n            other_perms.append((perm, checkboxes_by_id[perm.id]))\n            continue\n\n        for perm in content_perms:\n            content_perms_dict[\"object\"] = perm.content_type.name\n            checkbox = checkboxes_by_id[perm.id]\n            # identify the main categories of permission, and assign to\n            # the relevant dict key, else bung in the 'custom_perms' list\n            permission_action = perm.codename.split(\"_\", maxsplit=1)\n            permission_action = permission_action[permission_action[0].lower() == \"can\"]\n            permission_action = permission_action.rsplit(maxsplit=1)[0]\n\n            if permission_action in main_permission_names:\n                if permission_action in extra_perms_exist:\n                    extra_perms_exist[permission_action] = True\n                content_perms_dict[permission_action] = {\n                    \"perm\": perm,\n                    \"checkbox\": checkbox,\n                }\n            else:\n                extra_perms_exist[\"custom\"] = True\n                custom_perms.append(\n                    {\n                        \"perm\": perm,\n                        \"name\": f\"Can {permission_action}\",\n                        \"selected\": checkbox.data[\"selected\"],\n                    }\n                )\n\n        content_perms_dict[\"custom\"] = custom_perms\n        object_perms.append(content_perms_dict)\n    return {\n        \"object_perms\": object_perms,\n        \"other_perms\": other_perms,\n        \"extra_perms_exist\": extra_perms_exist,\n    }\n\n\n@register.inclusion_tag(\"wagtailadmin/shared/buttons.html\", takes_context=True)\ndef user_listing_buttons(context, user):\n    button_hooks = hooks.get_hooks(\"register_user_listing_buttons\")\n    buttons = sorted(\n        itertools.chain.from_iterable(hook(context, user) for hook in button_hooks)\n    )\n    return {\"user\": user, \"buttons\": buttons}\n", "path": "wagtail/users/templatetags/wagtailusers_tags.py"}], "after_files": [{"content": "import itertools\nfrom collections import defaultdict\n\nfrom django import template\nfrom django.contrib.auth import get_permission_codename\nfrom django.contrib.auth.models import Permission\nfrom django.utils.text import camel_case_to_spaces\n\nfrom wagtail import hooks\nfrom wagtail.users.permission_order import CONTENT_TYPE_ORDER\n\nregister = template.Library()\n\n\ndef normalize_permission_label(permission: Permission):\n    \"\"\"\n    Strip model name from the end of the label, e.g. \"Can deliver pizza\" for a\n    Pizza model becomes \"Can deliver\". For permissions in the model's\n    Meta.default_permissions with default labels, also replace underscores\n    with spaces.\n\n    This is used to display custom model permissions in the admin.\n\n    See https://github.com/wagtail/wagtail/issues/10982.\n    \"\"\"\n    label = permission.name\n    content_type = permission.content_type\n    model = content_type.model_class()\n    verbose_name = default_verbose_name = content_type.name\n\n    if model:\n        default_verbose_name = camel_case_to_spaces(model._meta.object_name)\n\n        # If it's in default_permissions and the label matches Django's default\n        # label, remove the model name from the end of the label. Also replace\n        # underscores with spaces, as Django uses the action internal name as-is\n        # for the permission label, which means it tends to be in snake_case.\n        for action in model._meta.default_permissions:\n            default_codename = get_permission_codename(action, model._meta)\n            is_default = permission.codename == default_codename\n            if is_default and permission.name.startswith(f\"Can {action}\"):\n                return f\"Can {action.replace('_', ' ')}\"\n\n    # For all other cases (including custom permissions), try to remove the\n    # verbose name from the end of the label. This only works if the label\n    # matches the current verbose name or Django's default verbose name.\n    for name in (default_verbose_name, verbose_name):\n        if label.lower().endswith(name.lower()):\n            return label[: -len(name)].strip()\n\n    return label\n\n\n@register.inclusion_tag(\"wagtailusers/groups/includes/formatted_permissions.html\")\ndef format_permissions(permission_bound_field):\n    \"\"\"\n    Given a bound field with a queryset of Permission objects - which must be using\n    the CheckboxSelectMultiple widget - construct a list of dictionaries for 'objects':\n\n    'objects': [\n        {\n            'object': name_of_some_content_object,\n            'add': checkbox,\n            'change': checkbox,\n            'delete': checkbox,\n            'publish': checkbox,  # only if the model extends DraftStateMixin\n            'custom': list_of_checkboxes_for_custom_permissions\n        },\n    ]\n\n    and a list of other permissions:\n\n    'others': [\n        (any_non_add_change_delete_permission, checkbox),\n    ]\n\n    (where 'checkbox' is an object with a tag() method that renders the checkbox as HTML;\n    this is a BoundWidget on Django >=1.11)\n\n    - and returns a table template formatted with this list.\n\n    \"\"\"\n    permissions = permission_bound_field.field._queryset\n    # get a distinct and ordered list of the content types that these permissions relate to.\n    # relies on Permission model default ordering, dict.fromkeys() retaining that order\n    # from the queryset, and the stability of sorted().\n    content_type_ids = sorted(\n        dict.fromkeys(permissions.values_list(\"content_type_id\", flat=True)),\n        key=lambda ct: CONTENT_TYPE_ORDER.get(ct, float(\"inf\")),\n    )\n\n    # iterate over permission_bound_field to build a lookup of individual renderable\n    # checkbox objects\n    # checkbox.data['value'] gives a ModelChoiceIteratorValue\n    checkboxes_by_id = {\n        int(checkbox.data[\"value\"].value): checkbox\n        for checkbox in permission_bound_field\n    }\n\n    object_perms = []\n    other_perms = []\n\n    # Permissions that are known by Wagtail, to be shown under their own columns.\n    # Other permissions will be shown under the \"custom permissions\" column.\n    main_permission_names = [\"add\", \"change\", \"delete\", \"publish\", \"lock\", \"unlock\"]\n\n    # Only show the columns for these permissions if any of the model has them.\n    extra_perms_exist = {\n        \"publish\": False,\n        \"lock\": False,\n        \"unlock\": False,\n        \"custom\": False,\n    }\n    # Batch the permission query for all content types, then group by content type\n    # (instead of querying permissions for each content type separately)\n    content_perms_by_ct_id = defaultdict(list)\n    permissions = permissions.filter(content_type_id__in=content_type_ids)\n    for permission in permissions:\n        content_perms_by_ct_id[permission.content_type_id].append(permission)\n\n    # Iterate using the sorted content_type_ids\n    for ct_id in content_type_ids:\n        content_perms = content_perms_by_ct_id[ct_id]\n        content_perms_dict = {}\n        custom_perms = []\n\n        if content_perms[0].content_type.name == \"admin\":\n            perm = content_perms[0]\n            other_perms.append((perm, checkboxes_by_id[perm.id]))\n            continue\n\n        for perm in content_perms:\n            content_perms_dict[\"object\"] = perm.content_type.name\n            checkbox = checkboxes_by_id[perm.id]\n            # identify the main categories of permission, and assign to\n            # the relevant dict key, else bung in the 'custom_perms' list\n            permission_action = perm.codename.split(\"_\")[0]\n            if permission_action in main_permission_names:\n                if permission_action in extra_perms_exist:\n                    extra_perms_exist[permission_action] = True\n                content_perms_dict[permission_action] = {\n                    \"perm\": perm,\n                    \"checkbox\": checkbox,\n                }\n            else:\n                extra_perms_exist[\"custom\"] = True\n                perm_name = normalize_permission_label(perm)\n\n                custom_perms.append(\n                    {\n                        \"perm\": perm,\n                        \"name\": perm_name,\n                        \"selected\": checkbox.data[\"selected\"],\n                    }\n                )\n\n        content_perms_dict[\"custom\"] = custom_perms\n        object_perms.append(content_perms_dict)\n    return {\n        \"object_perms\": object_perms,\n        \"other_perms\": other_perms,\n        \"extra_perms_exist\": extra_perms_exist,\n    }\n\n\n@register.inclusion_tag(\"wagtailadmin/shared/buttons.html\", takes_context=True)\ndef user_listing_buttons(context, user):\n    button_hooks = hooks.get_hooks(\"register_user_listing_buttons\")\n    buttons = sorted(\n        itertools.chain.from_iterable(hook(context, user) for hook in button_hooks)\n    )\n    return {\"user\": user, \"buttons\": buttons}\n", "path": "wagtail/users/templatetags/wagtailusers_tags.py"}]}