We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
2.0: AttributeError on editing user through admin
<!--
    NOTE: IF YOUR ISSUE DOES NOT FOLLOW THIS TEMPLATE, IT WILL BE CLOSED.

    This form is only for reporting reproducible bugs. If you need assistance
    with Nautobot installation, or if you have a general question, please start a
    discussion instead: https://github.com/nautobot/nautobot/discussions

    Please describe the environment in which you are running Nautobot. Be sure
    that you are running an unmodified instance of the latest stable release
    before submitting a bug report, and that any plugins have been disabled.
-->
### Environment
* Nautobot version (Docker tag too if applicable): `next`
* Python version:
* Database platform, version:
* Middleware(s):


### Steps to Reproduce
In the Nautobot admin, navigate to a specific user to edit it.

<!-- What did you expect to happen? -->
### Expected Behavior

User edit page to load

<!-- What happened instead? -->
### Observed Behavior

```
AttributeError at /admin/users/user/9fc58549-2485-4f88-a4e5-ec4986fe644e/change/

'QuerySet' object has no attribute 'nocache'
```

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `nautobot/users/admin.py`
Content:
```
1 from django import forms
2 from django.contrib import admin
3 from django.contrib.auth import get_user_model
4 from django.contrib.auth.admin import UserAdmin as UserAdmin_
5 from django.contrib.auth.models import Group
6 from django.contrib.contenttypes.models import ContentType
7 from django.core.exceptions import FieldError, ValidationError
8 from django.db import models
9 
10 from nautobot.core.admin import NautobotModelAdmin
11 from nautobot.extras.admin import order_content_types
12 from nautobot.users.models import AdminGroup, ObjectPermission, Token, User
13 
14 
15 #
16 # Inline models
17 #
18 
19 
20 class ObjectPermissionInline(admin.TabularInline):
21     exclude = None
22     extra = 3
23     readonly_fields = ["object_types", "actions", "constraints"]
24     verbose_name = "Permission"
25     verbose_name_plural = "Permissions"
26 
27     def get_queryset(self, request):
28         return super().get_queryset(request).prefetch_related("objectpermission__object_types").nocache()
29 
30     @staticmethod
31     def object_types(instance):
32         # Don't call .values_list() here because we want to reference the pre-fetched object_types
33         return ", ".join([ot.name for ot in instance.objectpermission.object_types.all()])
34 
35     @staticmethod
36     def actions(instance):
37         return ", ".join(instance.objectpermission.actions)
38 
39     @staticmethod
40     def constraints(instance):
41         return instance.objectpermission.constraints
42 
43 
44 class GroupObjectPermissionInline(ObjectPermissionInline):
45     model = AdminGroup.object_permissions.through
46 
47 
48 class UserObjectPermissionInline(ObjectPermissionInline):
49     model = get_user_model().object_permissions.through
50 
51 
52 #
53 # Users & groups
54 #
55 
56 # Unregister the built-in GroupAdmin class so that we can use our custom admin class below
57 admin.site.unregister(Group)
58 
59 
60 @admin.register(AdminGroup)
61 class GroupAdmin(NautobotModelAdmin):
62     fields = ("name",)
63     list_display = ("name", "user_count")
64     ordering = ("name",)
65     search_fields = ("name",)
66     inlines = [GroupObjectPermissionInline]
67 
68     @staticmethod
69     def user_count(obj):
70         return obj.user_set.count()
71 
72 
73 @admin.register(User)
74 class UserAdmin(UserAdmin_):
75     list_display = [
76         "username",
77         "email",
78         "first_name",
79         "last_name",
80         "is_superuser",
81         "is_staff",
82         "is_active",
83     ]
84     fieldsets = (
85         (
86             None,
87             {"fields": ("username", "password", "first_name", "last_name", "email")},
88         ),
89         ("Groups", {"fields": ("groups",)}),
90         (
91             "Status",
92             {
93                 "fields": ("is_active", "is_staff", "is_superuser"),
94             },
95         ),
96         ("Important dates", {"fields": ("last_login", "date_joined")}),
97         ("User Preferences", {"fields": ("config_data",)}),
98     )
99     filter_horizontal = ("groups",)
100     formfield_overrides = NautobotModelAdmin.formfield_overrides
101     readonly_fields = ("config_data",)
102 
103     def get_inlines(self, request, obj):
104         if obj is not None:
105             return (UserObjectPermissionInline,)
106         return ()
107 
108 
109 #
110 # REST API tokens
111 #
112 
113 
114 class TokenAdminForm(forms.ModelForm):
115     key = forms.CharField(
116         required=False,
117         help_text="If no key is provided, one will be generated automatically.",
118     )
119 
120     class Meta:
121         fields = ["user", "key", "write_enabled", "expires", "description"]
122         model = Token
123 
124 
125 @admin.register(Token)
126 class TokenAdmin(NautobotModelAdmin):
127     form = TokenAdminForm
128     list_display = ["key", "user", "created", "expires", "write_enabled", "description"]
129 
130 
131 #
132 # Permissions
133 #
134 
135 
136 class ObjectPermissionForm(forms.ModelForm):
137     can_view = forms.BooleanField(required=False)
138     can_add = forms.BooleanField(required=False)
139     can_change = forms.BooleanField(required=False)
140     can_delete = forms.BooleanField(required=False)
141 
142     class Meta:
143         model = ObjectPermission
144         exclude = []
145         help_texts = {
146             "actions": "Actions granted in addition to those listed above",
147             "constraints": "JSON expression of a queryset filter that will return only permitted objects. Leave null "
148             "to match all objects of this type. A list of multiple objects will result in a logical OR "
149             "operation.",
150         }
151         labels = {"actions": "Additional actions"}
152         widgets = {"constraints": forms.Textarea(attrs={"class": "vLargeTextField"})}
153 
154     def __init__(self, *args, **kwargs):
155         super().__init__(*args, **kwargs)
156 
157         # Make the actions field optional since the admin form uses it only for non-CRUD actions
158         self.fields["actions"].required = False
159 
160         # Format ContentType choices
161         order_content_types(self.fields["object_types"])
162         self.fields["object_types"].choices.insert(0, ("", "---------"))
163 
164         # Order group and user fields
165         self.fields["groups"].queryset = self.fields["groups"].queryset.order_by("name")
166         self.fields["users"].queryset = self.fields["users"].queryset.order_by("username")
167 
168         # Check the appropriate checkboxes when editing an existing ObjectPermission
169         if self.instance.present_in_database:
170             for action in ["view", "add", "change", "delete"]:
171                 if action in self.instance.actions:
172                     self.fields[f"can_{action}"].initial = True
173                     self.instance.actions.remove(action)
174 
175     def clean(self):
176         super().clean()
177 
178         object_types = self.cleaned_data.get("object_types")
179         constraints = self.cleaned_data.get("constraints")
180 
181         # Append any of the selected CRUD checkboxes to the actions list
182         if not self.cleaned_data.get("actions"):
183             self.cleaned_data["actions"] = []
184         for action in ["view", "add", "change", "delete"]:
185             if self.cleaned_data[f"can_{action}"] and action not in self.cleaned_data["actions"]:
186                 self.cleaned_data["actions"].append(action)
187 
188         # At least one action must be specified
189         if not self.cleaned_data["actions"]:
190             raise ValidationError("At least one action must be selected.")
191 
192         # Validate the specified model constraints by attempting to execute a query. We don't care whether the query
193         # returns anything; we just want to make sure the specified constraints are valid.
194         if object_types and constraints:
195             # Normalize the constraints to a list of dicts
196             if not isinstance(constraints, list):
197                 constraints = [constraints]
198             for ct in object_types:
199                 model = ct.model_class()
200                 try:
201                     model.objects.filter(*[models.Q(**c) for c in constraints]).exists()
202                 except FieldError as e:
203                     raise ValidationError({"constraints": f"Invalid filter for {model}: {e}"})
204 
205 
206 class ActionListFilter(admin.SimpleListFilter):
207     title = "action"
208     parameter_name = "action"
209 
210     def lookups(self, request, model_admin):
211         options = set()
212         for action_list in ObjectPermission.objects.values_list("actions", flat=True).distinct():
213             options.update(action_list)
214         return [(action, action) for action in sorted(options)]
215 
216     def queryset(self, request, queryset):
217         if self.value():
218             return queryset.filter(actions=[self.value()])
219         return None
220 
221 
222 class ObjectTypeListFilter(admin.SimpleListFilter):
223     title = "object type"
224     parameter_name = "object_type"
225 
226     def lookups(self, request, model_admin):
227         object_types = ObjectPermission.objects.values_list("object_types__pk", flat=True).distinct()
228         content_types = ContentType.objects.filter(pk__in=object_types).order_by("app_label", "model")
229         return [(ct.pk, ct) for ct in content_types]
230 
231     def queryset(self, request, queryset):
232         if self.value():
233             return queryset.filter(object_types=self.value())
234         return None
235 
236 
237 @admin.register(ObjectPermission)
238 class ObjectPermissionAdmin(NautobotModelAdmin):
239     actions = ("enable", "disable")
240     fieldsets = (
241         (None, {"fields": ("name", "description", "enabled")}),
242         (
243             "Actions",
244             {
245                 "fields": (
246                     ("can_view", "can_add", "can_change", "can_delete"),
247                     "actions",
248                 )
249             },
250         ),
251         ("Objects", {"fields": ("object_types",)}),
252         ("Assignment", {"fields": ("groups", "users")}),
253         ("Constraints", {"fields": ("constraints",), "classes": ("monospace",)}),
254     )
255     filter_horizontal = ("object_types", "groups", "users")
256     form = ObjectPermissionForm
257     list_display = [
258         "name",
259         "enabled",
260         "list_models",
261         "list_users",
262         "list_groups",
263         "actions",
264         "constraints",
265         "description",
266     ]
267     list_filter = ["enabled", ActionListFilter, ObjectTypeListFilter, "groups", "users"]
268     search_fields = ["actions", "constraints", "description", "name"]
269 
270     def get_queryset(self, request):
271         return super().get_queryset(request).prefetch_related("object_types", "users", "groups")
272 
273     def list_models(self, obj):
274         return ", ".join([f"{ct}" for ct in obj.object_types.all()])
275 
276     list_models.short_description = "Models"
277 
278     def list_users(self, obj):
279         return ", ".join([u.username for u in obj.users.all()])
280 
281     list_users.short_description = "Users"
282 
283     def list_groups(self, obj):
284         return ", ".join([g.name for g in obj.groups.all()])
285 
286     list_groups.short_description = "Groups"
287 
288     #
289     # Admin actions
290     #
291 
292     def enable(self, request, queryset):
293         updated = queryset.update(enabled=True)
294         self.message_user(request, f"Enabled {updated} permissions")
295 
296     def disable(self, request, queryset):
297         updated = queryset.update(enabled=False)
298         self.message_user(request, f"Disabled {updated} permissions")
299
```
--- 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/nautobot/users/admin.py b/nautobot/users/admin.py
--- a/nautobot/users/admin.py
+++ b/nautobot/users/admin.py
@@ -25,7 +25,7 @@
     verbose_name_plural = "Permissions"
 
     def get_queryset(self, request):
-        return super().get_queryset(request).prefetch_related("objectpermission__object_types").nocache()
+        return super().get_queryset(request).prefetch_related("objectpermission__object_types")
 
     @staticmethod
     def object_types(instance):

{"golden_diff": "diff --git a/nautobot/users/admin.py b/nautobot/users/admin.py\n--- a/nautobot/users/admin.py\n+++ b/nautobot/users/admin.py\n@@ -25,7 +25,7 @@\n     verbose_name_plural = \"Permissions\"\n \n     def get_queryset(self, request):\n-        return super().get_queryset(request).prefetch_related(\"objectpermission__object_types\").nocache()\n+        return super().get_queryset(request).prefetch_related(\"objectpermission__object_types\")\n \n     @staticmethod\n     def object_types(instance):\n", "issue": "2.0: AttributeError on editing user through admin\n<!--\r\n    NOTE: IF YOUR ISSUE DOES NOT FOLLOW THIS TEMPLATE, IT WILL BE CLOSED.\r\n\r\n    This form is only for reporting reproducible bugs. If you need assistance\r\n    with Nautobot installation, or if you have a general question, please start a\r\n    discussion instead: https://github.com/nautobot/nautobot/discussions\r\n\r\n    Please describe the environment in which you are running Nautobot. Be sure\r\n    that you are running an unmodified instance of the latest stable release\r\n    before submitting a bug report, and that any plugins have been disabled.\r\n-->\r\n### Environment\r\n* Nautobot version (Docker tag too if applicable): `next`\r\n* Python version:\r\n* Database platform, version:\r\n* Middleware(s):\r\n\r\n\r\n### Steps to Reproduce\r\nIn the Nautobot admin, navigate to a specific user to edit it.\r\n\r\n<!-- What did you expect to happen? -->\r\n### Expected Behavior\r\n\r\nUser edit page to load\r\n\r\n<!-- What happened instead? -->\r\n### Observed Behavior\r\n\r\n```\r\nAttributeError at /admin/users/user/9fc58549-2485-4f88-a4e5-ec4986fe644e/change/\r\n\r\n'QuerySet' object has no attribute 'nocache'\r\n```\n", "before_files": [{"content": "from django import forms\nfrom django.contrib import admin\nfrom django.contrib.auth import get_user_model\nfrom django.contrib.auth.admin import UserAdmin as UserAdmin_\nfrom django.contrib.auth.models import Group\nfrom django.contrib.contenttypes.models import ContentType\nfrom django.core.exceptions import FieldError, ValidationError\nfrom django.db import models\n\nfrom nautobot.core.admin import NautobotModelAdmin\nfrom nautobot.extras.admin import order_content_types\nfrom nautobot.users.models import AdminGroup, ObjectPermission, Token, User\n\n\n#\n# Inline models\n#\n\n\nclass ObjectPermissionInline(admin.TabularInline):\n    exclude = None\n    extra = 3\n    readonly_fields = [\"object_types\", \"actions\", \"constraints\"]\n    verbose_name = \"Permission\"\n    verbose_name_plural = \"Permissions\"\n\n    def get_queryset(self, request):\n        return super().get_queryset(request).prefetch_related(\"objectpermission__object_types\").nocache()\n\n    @staticmethod\n    def object_types(instance):\n        # Don't call .values_list() here because we want to reference the pre-fetched object_types\n        return \", \".join([ot.name for ot in instance.objectpermission.object_types.all()])\n\n    @staticmethod\n    def actions(instance):\n        return \", \".join(instance.objectpermission.actions)\n\n    @staticmethod\n    def constraints(instance):\n        return instance.objectpermission.constraints\n\n\nclass GroupObjectPermissionInline(ObjectPermissionInline):\n    model = AdminGroup.object_permissions.through\n\n\nclass UserObjectPermissionInline(ObjectPermissionInline):\n    model = get_user_model().object_permissions.through\n\n\n#\n# Users & groups\n#\n\n# Unregister the built-in GroupAdmin class so that we can use our custom admin class below\nadmin.site.unregister(Group)\n\n\n@admin.register(AdminGroup)\nclass GroupAdmin(NautobotModelAdmin):\n    fields = (\"name\",)\n    list_display = (\"name\", \"user_count\")\n    ordering = (\"name\",)\n    search_fields = (\"name\",)\n    inlines = [GroupObjectPermissionInline]\n\n    @staticmethod\n    def user_count(obj):\n        return obj.user_set.count()\n\n\n@admin.register(User)\nclass UserAdmin(UserAdmin_):\n    list_display = [\n        \"username\",\n        \"email\",\n        \"first_name\",\n        \"last_name\",\n        \"is_superuser\",\n        \"is_staff\",\n        \"is_active\",\n    ]\n    fieldsets = (\n        (\n            None,\n            {\"fields\": (\"username\", \"password\", \"first_name\", \"last_name\", \"email\")},\n        ),\n        (\"Groups\", {\"fields\": (\"groups\",)}),\n        (\n            \"Status\",\n            {\n                \"fields\": (\"is_active\", \"is_staff\", \"is_superuser\"),\n            },\n        ),\n        (\"Important dates\", {\"fields\": (\"last_login\", \"date_joined\")}),\n        (\"User Preferences\", {\"fields\": (\"config_data\",)}),\n    )\n    filter_horizontal = (\"groups\",)\n    formfield_overrides = NautobotModelAdmin.formfield_overrides\n    readonly_fields = (\"config_data\",)\n\n    def get_inlines(self, request, obj):\n        if obj is not None:\n            return (UserObjectPermissionInline,)\n        return ()\n\n\n#\n# REST API tokens\n#\n\n\nclass TokenAdminForm(forms.ModelForm):\n    key = forms.CharField(\n        required=False,\n        help_text=\"If no key is provided, one will be generated automatically.\",\n    )\n\n    class Meta:\n        fields = [\"user\", \"key\", \"write_enabled\", \"expires\", \"description\"]\n        model = Token\n\n\n@admin.register(Token)\nclass TokenAdmin(NautobotModelAdmin):\n    form = TokenAdminForm\n    list_display = [\"key\", \"user\", \"created\", \"expires\", \"write_enabled\", \"description\"]\n\n\n#\n# Permissions\n#\n\n\nclass ObjectPermissionForm(forms.ModelForm):\n    can_view = forms.BooleanField(required=False)\n    can_add = forms.BooleanField(required=False)\n    can_change = forms.BooleanField(required=False)\n    can_delete = forms.BooleanField(required=False)\n\n    class Meta:\n        model = ObjectPermission\n        exclude = []\n        help_texts = {\n            \"actions\": \"Actions granted in addition to those listed above\",\n            \"constraints\": \"JSON expression of a queryset filter that will return only permitted objects. Leave null \"\n            \"to match all objects of this type. A list of multiple objects will result in a logical OR \"\n            \"operation.\",\n        }\n        labels = {\"actions\": \"Additional actions\"}\n        widgets = {\"constraints\": forms.Textarea(attrs={\"class\": \"vLargeTextField\"})}\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n\n        # Make the actions field optional since the admin form uses it only for non-CRUD actions\n        self.fields[\"actions\"].required = False\n\n        # Format ContentType choices\n        order_content_types(self.fields[\"object_types\"])\n        self.fields[\"object_types\"].choices.insert(0, (\"\", \"---------\"))\n\n        # Order group and user fields\n        self.fields[\"groups\"].queryset = self.fields[\"groups\"].queryset.order_by(\"name\")\n        self.fields[\"users\"].queryset = self.fields[\"users\"].queryset.order_by(\"username\")\n\n        # Check the appropriate checkboxes when editing an existing ObjectPermission\n        if self.instance.present_in_database:\n            for action in [\"view\", \"add\", \"change\", \"delete\"]:\n                if action in self.instance.actions:\n                    self.fields[f\"can_{action}\"].initial = True\n                    self.instance.actions.remove(action)\n\n    def clean(self):\n        super().clean()\n\n        object_types = self.cleaned_data.get(\"object_types\")\n        constraints = self.cleaned_data.get(\"constraints\")\n\n        # Append any of the selected CRUD checkboxes to the actions list\n        if not self.cleaned_data.get(\"actions\"):\n            self.cleaned_data[\"actions\"] = []\n        for action in [\"view\", \"add\", \"change\", \"delete\"]:\n            if self.cleaned_data[f\"can_{action}\"] and action not in self.cleaned_data[\"actions\"]:\n                self.cleaned_data[\"actions\"].append(action)\n\n        # At least one action must be specified\n        if not self.cleaned_data[\"actions\"]:\n            raise ValidationError(\"At least one action must be selected.\")\n\n        # Validate the specified model constraints by attempting to execute a query. We don't care whether the query\n        # returns anything; we just want to make sure the specified constraints are valid.\n        if object_types and constraints:\n            # Normalize the constraints to a list of dicts\n            if not isinstance(constraints, list):\n                constraints = [constraints]\n            for ct in object_types:\n                model = ct.model_class()\n                try:\n                    model.objects.filter(*[models.Q(**c) for c in constraints]).exists()\n                except FieldError as e:\n                    raise ValidationError({\"constraints\": f\"Invalid filter for {model}: {e}\"})\n\n\nclass ActionListFilter(admin.SimpleListFilter):\n    title = \"action\"\n    parameter_name = \"action\"\n\n    def lookups(self, request, model_admin):\n        options = set()\n        for action_list in ObjectPermission.objects.values_list(\"actions\", flat=True).distinct():\n            options.update(action_list)\n        return [(action, action) for action in sorted(options)]\n\n    def queryset(self, request, queryset):\n        if self.value():\n            return queryset.filter(actions=[self.value()])\n        return None\n\n\nclass ObjectTypeListFilter(admin.SimpleListFilter):\n    title = \"object type\"\n    parameter_name = \"object_type\"\n\n    def lookups(self, request, model_admin):\n        object_types = ObjectPermission.objects.values_list(\"object_types__pk\", flat=True).distinct()\n        content_types = ContentType.objects.filter(pk__in=object_types).order_by(\"app_label\", \"model\")\n        return [(ct.pk, ct) for ct in content_types]\n\n    def queryset(self, request, queryset):\n        if self.value():\n            return queryset.filter(object_types=self.value())\n        return None\n\n\n@admin.register(ObjectPermission)\nclass ObjectPermissionAdmin(NautobotModelAdmin):\n    actions = (\"enable\", \"disable\")\n    fieldsets = (\n        (None, {\"fields\": (\"name\", \"description\", \"enabled\")}),\n        (\n            \"Actions\",\n            {\n                \"fields\": (\n                    (\"can_view\", \"can_add\", \"can_change\", \"can_delete\"),\n                    \"actions\",\n                )\n            },\n        ),\n        (\"Objects\", {\"fields\": (\"object_types\",)}),\n        (\"Assignment\", {\"fields\": (\"groups\", \"users\")}),\n        (\"Constraints\", {\"fields\": (\"constraints\",), \"classes\": (\"monospace\",)}),\n    )\n    filter_horizontal = (\"object_types\", \"groups\", \"users\")\n    form = ObjectPermissionForm\n    list_display = [\n        \"name\",\n        \"enabled\",\n        \"list_models\",\n        \"list_users\",\n        \"list_groups\",\n        \"actions\",\n        \"constraints\",\n        \"description\",\n    ]\n    list_filter = [\"enabled\", ActionListFilter, ObjectTypeListFilter, \"groups\", \"users\"]\n    search_fields = [\"actions\", \"constraints\", \"description\", \"name\"]\n\n    def get_queryset(self, request):\n        return super().get_queryset(request).prefetch_related(\"object_types\", \"users\", \"groups\")\n\n    def list_models(self, obj):\n        return \", \".join([f\"{ct}\" for ct in obj.object_types.all()])\n\n    list_models.short_description = \"Models\"\n\n    def list_users(self, obj):\n        return \", \".join([u.username for u in obj.users.all()])\n\n    list_users.short_description = \"Users\"\n\n    def list_groups(self, obj):\n        return \", \".join([g.name for g in obj.groups.all()])\n\n    list_groups.short_description = \"Groups\"\n\n    #\n    # Admin actions\n    #\n\n    def enable(self, request, queryset):\n        updated = queryset.update(enabled=True)\n        self.message_user(request, f\"Enabled {updated} permissions\")\n\n    def disable(self, request, queryset):\n        updated = queryset.update(enabled=False)\n        self.message_user(request, f\"Disabled {updated} permissions\")\n", "path": "nautobot/users/admin.py"}], "after_files": [{"content": "from django import forms\nfrom django.contrib import admin\nfrom django.contrib.auth import get_user_model\nfrom django.contrib.auth.admin import UserAdmin as UserAdmin_\nfrom django.contrib.auth.models import Group\nfrom django.contrib.contenttypes.models import ContentType\nfrom django.core.exceptions import FieldError, ValidationError\nfrom django.db import models\n\nfrom nautobot.core.admin import NautobotModelAdmin\nfrom nautobot.extras.admin import order_content_types\nfrom nautobot.users.models import AdminGroup, ObjectPermission, Token, User\n\n\n#\n# Inline models\n#\n\n\nclass ObjectPermissionInline(admin.TabularInline):\n    exclude = None\n    extra = 3\n    readonly_fields = [\"object_types\", \"actions\", \"constraints\"]\n    verbose_name = \"Permission\"\n    verbose_name_plural = \"Permissions\"\n\n    def get_queryset(self, request):\n        return super().get_queryset(request).prefetch_related(\"objectpermission__object_types\")\n\n    @staticmethod\n    def object_types(instance):\n        # Don't call .values_list() here because we want to reference the pre-fetched object_types\n        return \", \".join([ot.name for ot in instance.objectpermission.object_types.all()])\n\n    @staticmethod\n    def actions(instance):\n        return \", \".join(instance.objectpermission.actions)\n\n    @staticmethod\n    def constraints(instance):\n        return instance.objectpermission.constraints\n\n\nclass GroupObjectPermissionInline(ObjectPermissionInline):\n    model = AdminGroup.object_permissions.through\n\n\nclass UserObjectPermissionInline(ObjectPermissionInline):\n    model = get_user_model().object_permissions.through\n\n\n#\n# Users & groups\n#\n\n# Unregister the built-in GroupAdmin class so that we can use our custom admin class below\nadmin.site.unregister(Group)\n\n\n@admin.register(AdminGroup)\nclass GroupAdmin(NautobotModelAdmin):\n    fields = (\"name\",)\n    list_display = (\"name\", \"user_count\")\n    ordering = (\"name\",)\n    search_fields = (\"name\",)\n    inlines = [GroupObjectPermissionInline]\n\n    @staticmethod\n    def user_count(obj):\n        return obj.user_set.count()\n\n\n@admin.register(User)\nclass UserAdmin(UserAdmin_):\n    list_display = [\n        \"username\",\n        \"email\",\n        \"first_name\",\n        \"last_name\",\n        \"is_superuser\",\n        \"is_staff\",\n        \"is_active\",\n    ]\n    fieldsets = (\n        (\n            None,\n            {\"fields\": (\"username\", \"password\", \"first_name\", \"last_name\", \"email\")},\n        ),\n        (\"Groups\", {\"fields\": (\"groups\",)}),\n        (\n            \"Status\",\n            {\n                \"fields\": (\"is_active\", \"is_staff\", \"is_superuser\"),\n            },\n        ),\n        (\"Important dates\", {\"fields\": (\"last_login\", \"date_joined\")}),\n        (\"User Preferences\", {\"fields\": (\"config_data\",)}),\n    )\n    filter_horizontal = (\"groups\",)\n    formfield_overrides = NautobotModelAdmin.formfield_overrides\n    readonly_fields = (\"config_data\",)\n\n    def get_inlines(self, request, obj):\n        if obj is not None:\n            return (UserObjectPermissionInline,)\n        return ()\n\n\n#\n# REST API tokens\n#\n\n\nclass TokenAdminForm(forms.ModelForm):\n    key = forms.CharField(\n        required=False,\n        help_text=\"If no key is provided, one will be generated automatically.\",\n    )\n\n    class Meta:\n        fields = [\"user\", \"key\", \"write_enabled\", \"expires\", \"description\"]\n        model = Token\n\n\n@admin.register(Token)\nclass TokenAdmin(NautobotModelAdmin):\n    form = TokenAdminForm\n    list_display = [\"key\", \"user\", \"created\", \"expires\", \"write_enabled\", \"description\"]\n\n\n#\n# Permissions\n#\n\n\nclass ObjectPermissionForm(forms.ModelForm):\n    can_view = forms.BooleanField(required=False)\n    can_add = forms.BooleanField(required=False)\n    can_change = forms.BooleanField(required=False)\n    can_delete = forms.BooleanField(required=False)\n\n    class Meta:\n        model = ObjectPermission\n        exclude = []\n        help_texts = {\n            \"actions\": \"Actions granted in addition to those listed above\",\n            \"constraints\": \"JSON expression of a queryset filter that will return only permitted objects. Leave null \"\n            \"to match all objects of this type. A list of multiple objects will result in a logical OR \"\n            \"operation.\",\n        }\n        labels = {\"actions\": \"Additional actions\"}\n        widgets = {\"constraints\": forms.Textarea(attrs={\"class\": \"vLargeTextField\"})}\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n\n        # Make the actions field optional since the admin form uses it only for non-CRUD actions\n        self.fields[\"actions\"].required = False\n\n        # Format ContentType choices\n        order_content_types(self.fields[\"object_types\"])\n        self.fields[\"object_types\"].choices.insert(0, (\"\", \"---------\"))\n\n        # Order group and user fields\n        self.fields[\"groups\"].queryset = self.fields[\"groups\"].queryset.order_by(\"name\")\n        self.fields[\"users\"].queryset = self.fields[\"users\"].queryset.order_by(\"username\")\n\n        # Check the appropriate checkboxes when editing an existing ObjectPermission\n        if self.instance.present_in_database:\n            for action in [\"view\", \"add\", \"change\", \"delete\"]:\n                if action in self.instance.actions:\n                    self.fields[f\"can_{action}\"].initial = True\n                    self.instance.actions.remove(action)\n\n    def clean(self):\n        super().clean()\n\n        object_types = self.cleaned_data.get(\"object_types\")\n        constraints = self.cleaned_data.get(\"constraints\")\n\n        # Append any of the selected CRUD checkboxes to the actions list\n        if not self.cleaned_data.get(\"actions\"):\n            self.cleaned_data[\"actions\"] = []\n        for action in [\"view\", \"add\", \"change\", \"delete\"]:\n            if self.cleaned_data[f\"can_{action}\"] and action not in self.cleaned_data[\"actions\"]:\n                self.cleaned_data[\"actions\"].append(action)\n\n        # At least one action must be specified\n        if not self.cleaned_data[\"actions\"]:\n            raise ValidationError(\"At least one action must be selected.\")\n\n        # Validate the specified model constraints by attempting to execute a query. We don't care whether the query\n        # returns anything; we just want to make sure the specified constraints are valid.\n        if object_types and constraints:\n            # Normalize the constraints to a list of dicts\n            if not isinstance(constraints, list):\n                constraints = [constraints]\n            for ct in object_types:\n                model = ct.model_class()\n                try:\n                    model.objects.filter(*[models.Q(**c) for c in constraints]).exists()\n                except FieldError as e:\n                    raise ValidationError({\"constraints\": f\"Invalid filter for {model}: {e}\"})\n\n\nclass ActionListFilter(admin.SimpleListFilter):\n    title = \"action\"\n    parameter_name = \"action\"\n\n    def lookups(self, request, model_admin):\n        options = set()\n        for action_list in ObjectPermission.objects.values_list(\"actions\", flat=True).distinct():\n            options.update(action_list)\n        return [(action, action) for action in sorted(options)]\n\n    def queryset(self, request, queryset):\n        if self.value():\n            return queryset.filter(actions=[self.value()])\n        return None\n\n\nclass ObjectTypeListFilter(admin.SimpleListFilter):\n    title = \"object type\"\n    parameter_name = \"object_type\"\n\n    def lookups(self, request, model_admin):\n        object_types = ObjectPermission.objects.values_list(\"object_types__pk\", flat=True).distinct()\n        content_types = ContentType.objects.filter(pk__in=object_types).order_by(\"app_label\", \"model\")\n        return [(ct.pk, ct) for ct in content_types]\n\n    def queryset(self, request, queryset):\n        if self.value():\n            return queryset.filter(object_types=self.value())\n        return None\n\n\n@admin.register(ObjectPermission)\nclass ObjectPermissionAdmin(NautobotModelAdmin):\n    actions = (\"enable\", \"disable\")\n    fieldsets = (\n        (None, {\"fields\": (\"name\", \"description\", \"enabled\")}),\n        (\n            \"Actions\",\n            {\n                \"fields\": (\n                    (\"can_view\", \"can_add\", \"can_change\", \"can_delete\"),\n                    \"actions\",\n                )\n            },\n        ),\n        (\"Objects\", {\"fields\": (\"object_types\",)}),\n        (\"Assignment\", {\"fields\": (\"groups\", \"users\")}),\n        (\"Constraints\", {\"fields\": (\"constraints\",), \"classes\": (\"monospace\",)}),\n    )\n    filter_horizontal = (\"object_types\", \"groups\", \"users\")\n    form = ObjectPermissionForm\n    list_display = [\n        \"name\",\n        \"enabled\",\n        \"list_models\",\n        \"list_users\",\n        \"list_groups\",\n        \"actions\",\n        \"constraints\",\n        \"description\",\n    ]\n    list_filter = [\"enabled\", ActionListFilter, ObjectTypeListFilter, \"groups\", \"users\"]\n    search_fields = [\"actions\", \"constraints\", \"description\", \"name\"]\n\n    def get_queryset(self, request):\n        return super().get_queryset(request).prefetch_related(\"object_types\", \"users\", \"groups\")\n\n    def list_models(self, obj):\n        return \", \".join([f\"{ct}\" for ct in obj.object_types.all()])\n\n    list_models.short_description = \"Models\"\n\n    def list_users(self, obj):\n        return \", \".join([u.username for u in obj.users.all()])\n\n    list_users.short_description = \"Users\"\n\n    def list_groups(self, obj):\n        return \", \".join([g.name for g in obj.groups.all()])\n\n    list_groups.short_description = \"Groups\"\n\n    #\n    # Admin actions\n    #\n\n    def enable(self, request, queryset):\n        updated = queryset.update(enabled=True)\n        self.message_user(request, f\"Enabled {updated} permissions\")\n\n    def disable(self, request, queryset):\n        updated = queryset.update(enabled=False)\n        self.message_user(request, f\"Disabled {updated} permissions\")\n", "path": "nautobot/users/admin.py"}]}