We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Poor performances on create/edit tenant field with high number of tenants 
### Deployment Type

Self-hosted

### NetBox Version

v3.6.6

### Python Version

3.11

### Steps to Reproduce

* Create around 85k tenant in a single tenant group
* Create around 150k prefixes linked with differents tenants
* Create some sites, linked with differents tenants
* Then edit one of the prefix, and click on tenant selector.

### Expected Behavior

Maybe one second or less should be a good request timer
Or maybe the searchable select should be async to allow user to enter characters while the first request is running in background.

### Observed Behavior

With a postgres 16 database, and 16GB of ram and 4 cpus, It will take 3.5 seconds to respond and give the user the opportunity to enter some search characters.

As far i can go through the debug process, i can see theses kinds of SQL request with long running timer ( >3s):
```sql
SELECT 
  DISTINCT "tenancy_tenant"."id", 
  "tenancy_tenant"."created", 
  "tenancy_tenant"."last_updated", 
  "tenancy_tenant"."custom_field_data", 
  "tenancy_tenant"."description", 
  "tenancy_tenant"."comments", 
  "tenancy_tenant"."name", 
  "tenancy_tenant"."slug", 
  "tenancy_tenant"."group_id", 
  COALESCE(
    (
      SELECT 
        COUNT(*) AS "c" 
      FROM 
        "circuits_circuit" U0 
      WHERE 
        U0."tenant_id" = ("tenancy_tenant"."id") 
      GROUP BY 
        U0."tenant_id"
    ), 
    0
  ) AS "circuit_count", 
  COALESCE(
    (
      SELECT 
        COUNT(*) AS "c" 
      FROM 
        "dcim_device" U0 
      WHERE 
        U0."tenant_id" = ("tenancy_tenant"."id") 
      GROUP BY 
        U0."tenant_id"
    ), 
    0
  ) AS "device_count", 
  COALESCE(
    (
      SELECT 
        COUNT(*) AS "c" 
      FROM 
        "ipam_ipaddress" U0 
      WHERE 
        U0."tenant_id" = ("tenancy_tenant"."id") 
      GROUP BY 
        U0."tenant_id"
    ), 
    0
  ) AS "ipaddress_count", 
  COALESCE(
    (
      SELECT 
        COUNT(*) AS "c" 
      FROM 
        "ipam_prefix" U0 
      WHERE 
        U0."tenant_id" = ("tenancy_tenant"."id") 
      GROUP BY 
        U0."tenant_id"
    ), 
    0
  ) AS "prefix_count", 
  COALESCE(
    (
      SELECT 
        COUNT(*) AS "c" 
      FROM 
        "dcim_rack" U0 
      WHERE 
        U0."tenant_id" = ("tenancy_tenant"."id") 
      GROUP BY 
        U0."tenant_id"
    ), 
    0
  ) AS "rack_count", 
  COALESCE(
    (
      SELECT 
        COUNT(*) AS "c" 
      FROM 
        "dcim_site" U0 
      WHERE 
        U0."tenant_id" = ("tenancy_tenant"."id") 
      GROUP BY 
        U0."tenant_id"
    ), 
    0
  ) AS "site_count", 
  COALESCE(
    (
      SELECT 
        COUNT(*) AS "c" 
      FROM 
        "virtualization_virtualmachine" U0 
      WHERE 
        U0."tenant_id" = ("tenancy_tenant"."id") 
      GROUP BY 
        U0."tenant_id"
    ), 
    0
  ) AS "virtualmachine_count", 
  COALESCE(
    (
      SELECT 
        COUNT(*) AS "c" 
      FROM 
        "ipam_vlan" U0 
      WHERE 
        U0."tenant_id" = ("tenancy_tenant"."id") 
      GROUP BY 
        U0."tenant_id"
    ), 
    0
  ) AS "vlan_count", 
  COALESCE(
    (
      SELECT 
        COUNT(*) AS "c" 
      FROM 
        "ipam_vrf" U0 
      WHERE 
        U0."tenant_id" = ("tenancy_tenant"."id") 
      GROUP BY 
        U0."tenant_id"
    ), 
    0
  ) AS "vrf_count", 
  COALESCE(
    (
      SELECT 
        COUNT(*) AS "c" 
      FROM 
        "virtualization_cluster" U0 
      WHERE 
        U0."tenant_id" = ("tenancy_tenant"."id") 
      GROUP BY 
        U0."tenant_id"
    ), 
    0
  ) AS "cluster_count" 
FROM 
  "tenancy_tenant" 
WHERE 
  "tenancy_tenant"."group_id" IN (
    SELECT 
      U0."id" 
    FROM 
      "tenancy_tenantgroup" U0 
    WHERE 
      U0."id" = 4
  ) 
ORDER BY 
  "tenancy_tenant"."name" ASC 
LIMIT 
  50
```

Theses request, for each kind of related objects making a count of them. So with 85k tenant, each count is executed 85k times.
From my point of view, to display a dynamic selector with customer display field, it's not necessary to count every objects.

To be more specific on the pgsql server, the request is executed on RAM buffered datas. No hit on system SSD. The cache usage is around 30% and hit objects.

We already have a discussion about it on slack: https://netdev-community.slack.com/archives/C01P0FRSXRV/p1702476390699559

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `netbox/netbox/api/viewsets/mixins.py`
Content:
```
1 import logging
2 
3 from django.contrib.contenttypes.models import ContentType
4 from django.core.exceptions import ObjectDoesNotExist
5 from django.db import transaction
6 from django.http import Http404
7 from rest_framework import status
8 from rest_framework.response import Response
9 
10 from extras.models import ExportTemplate
11 from netbox.api.exceptions import SerializerNotFound
12 from netbox.api.serializers import BulkOperationSerializer
13 from netbox.constants import NESTED_SERIALIZER_PREFIX
14 from utilities.api import get_serializer_for_model
15 
16 __all__ = (
17     'BriefModeMixin',
18     'BulkDestroyModelMixin',
19     'BulkUpdateModelMixin',
20     'CustomFieldsMixin',
21     'ExportTemplatesMixin',
22     'ObjectValidationMixin',
23     'SequentialBulkCreatesMixin',
24 )
25 
26 
27 class BriefModeMixin:
28     """
29     Enables brief mode support, so that the client can invoke a model's nested serializer by passing e.g.
30         GET /api/dcim/sites/?brief=True
31     """
32     brief = False
33     brief_prefetch_fields = []
34 
35     def initialize_request(self, request, *args, **kwargs):
36         # Annotate whether brief mode is active
37         self.brief = request.method == 'GET' and request.GET.get('brief')
38 
39         return super().initialize_request(request, *args, **kwargs)
40 
41     def get_serializer_class(self):
42         logger = logging.getLogger(f'netbox.api.views.{self.__class__.__name__}')
43 
44         # If using 'brief' mode, find and return the nested serializer for this model, if one exists
45         if self.brief:
46             logger.debug("Request is for 'brief' format; initializing nested serializer")
47             try:
48                 return get_serializer_for_model(self.queryset.model, prefix=NESTED_SERIALIZER_PREFIX)
49             except SerializerNotFound:
50                 logger.debug(
51                     f"Nested serializer for {self.queryset.model} not found! Using serializer {self.serializer_class}"
52                 )
53 
54         return self.serializer_class
55 
56     def get_queryset(self):
57         qs = super().get_queryset()
58 
59         # If using brief mode, clear all prefetches from the queryset and append only brief_prefetch_fields (if any)
60         if self.brief:
61             return qs.prefetch_related(None).prefetch_related(*self.brief_prefetch_fields)
62 
63         return qs
64 
65 
66 class CustomFieldsMixin:
67     """
68     For models which support custom fields, populate the `custom_fields` context.
69     """
70     def get_serializer_context(self):
71         context = super().get_serializer_context()
72 
73         if hasattr(self.queryset.model, 'custom_fields'):
74             content_type = ContentType.objects.get_for_model(self.queryset.model)
75             context.update({
76                 'custom_fields': content_type.custom_fields.all(),
77             })
78 
79         return context
80 
81 
82 class ExportTemplatesMixin:
83     """
84     Enable ExportTemplate support for list views.
85     """
86     def list(self, request, *args, **kwargs):
87         if 'export' in request.GET:
88             content_type = ContentType.objects.get_for_model(self.get_serializer_class().Meta.model)
89             et = ExportTemplate.objects.filter(content_types=content_type, name=request.GET['export']).first()
90             if et is None:
91                 raise Http404
92             queryset = self.filter_queryset(self.get_queryset())
93             return et.render_to_response(queryset)
94 
95         return super().list(request, *args, **kwargs)
96 
97 
98 class SequentialBulkCreatesMixin:
99     """
100     Perform bulk creation of new objects sequentially, rather than all at once. This ensures that any validation
101     which depends on the evaluation of existing objects (such as checking for free space within a rack) functions
102     appropriately.
103     """
104     @transaction.atomic
105     def create(self, request, *args, **kwargs):
106         if not isinstance(request.data, list):
107             # Creating a single object
108             return super().create(request, *args, **kwargs)
109 
110         return_data = []
111         for data in request.data:
112             serializer = self.get_serializer(data=data)
113             serializer.is_valid(raise_exception=True)
114             self.perform_create(serializer)
115             return_data.append(serializer.data)
116 
117         headers = self.get_success_headers(serializer.data)
118 
119         return Response(return_data, status=status.HTTP_201_CREATED, headers=headers)
120 
121 
122 class BulkUpdateModelMixin:
123     """
124     Support bulk modification of objects using the list endpoint for a model. Accepts a PATCH action with a list of one
125     or more JSON objects, each specifying the numeric ID of an object to be updated as well as the attributes to be set.
126     For example:
127 
128     PATCH /api/dcim/sites/
129     [
130         {
131             "id": 123,
132             "name": "New name"
133         },
134         {
135             "id": 456,
136             "status": "planned"
137         }
138     ]
139     """
140     def bulk_update(self, request, *args, **kwargs):
141         partial = kwargs.pop('partial', False)
142         serializer = BulkOperationSerializer(data=request.data, many=True)
143         serializer.is_valid(raise_exception=True)
144         qs = self.get_queryset().filter(
145             pk__in=[o['id'] for o in serializer.data]
146         )
147 
148         # Map update data by object ID
149         update_data = {
150             obj.pop('id'): obj for obj in request.data
151         }
152 
153         data = self.perform_bulk_update(qs, update_data, partial=partial)
154 
155         return Response(data, status=status.HTTP_200_OK)
156 
157     def perform_bulk_update(self, objects, update_data, partial):
158         with transaction.atomic():
159             data_list = []
160             for obj in objects:
161                 data = update_data.get(obj.id)
162                 if hasattr(obj, 'snapshot'):
163                     obj.snapshot()
164                 serializer = self.get_serializer(obj, data=data, partial=partial)
165                 serializer.is_valid(raise_exception=True)
166                 self.perform_update(serializer)
167                 data_list.append(serializer.data)
168 
169             return data_list
170 
171     def bulk_partial_update(self, request, *args, **kwargs):
172         kwargs['partial'] = True
173         return self.bulk_update(request, *args, **kwargs)
174 
175 
176 class BulkDestroyModelMixin:
177     """
178     Support bulk deletion of objects using the list endpoint for a model. Accepts a DELETE action with a list of one
179     or more JSON objects, each specifying the numeric ID of an object to be deleted. For example:
180 
181     DELETE /api/dcim/sites/
182     [
183         {"id": 123},
184         {"id": 456}
185     ]
186     """
187     def bulk_destroy(self, request, *args, **kwargs):
188         serializer = BulkOperationSerializer(data=request.data, many=True)
189         serializer.is_valid(raise_exception=True)
190         qs = self.get_queryset().filter(
191             pk__in=[o['id'] for o in serializer.data]
192         )
193 
194         self.perform_bulk_destroy(qs)
195 
196         return Response(status=status.HTTP_204_NO_CONTENT)
197 
198     def perform_bulk_destroy(self, objects):
199         with transaction.atomic():
200             for obj in objects:
201                 if hasattr(obj, 'snapshot'):
202                     obj.snapshot()
203                 self.perform_destroy(obj)
204 
205 
206 class ObjectValidationMixin:
207 
208     def _validate_objects(self, instance):
209         """
210         Check that the provided instance or list of instances are matched by the current queryset. This confirms that
211         any newly created or modified objects abide by the attributes granted by any applicable ObjectPermissions.
212         """
213         if type(instance) is list:
214             # Check that all instances are still included in the view's queryset
215             conforming_count = self.queryset.filter(pk__in=[obj.pk for obj in instance]).count()
216             if conforming_count != len(instance):
217                 raise ObjectDoesNotExist
218         elif not self.queryset.filter(pk=instance.pk).exists():
219             raise ObjectDoesNotExist
220
```
--- 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/netbox/netbox/api/viewsets/mixins.py b/netbox/netbox/api/viewsets/mixins.py
--- a/netbox/netbox/api/viewsets/mixins.py
+++ b/netbox/netbox/api/viewsets/mixins.py
@@ -56,8 +56,15 @@
     def get_queryset(self):
         qs = super().get_queryset()
 
-        # If using brief mode, clear all prefetches from the queryset and append only brief_prefetch_fields (if any)
         if self.brief:
+            serializer_class = self.get_serializer_class()
+
+            # Clear any annotations for fields not present on the nested serializer
+            for annotation in list(qs.query.annotations.keys()):
+                if annotation not in serializer_class().fields:
+                    qs.query.annotations.pop(annotation)
+
+            # Clear any prefetches from the queryset and append only brief_prefetch_fields (if any)
             return qs.prefetch_related(None).prefetch_related(*self.brief_prefetch_fields)
 
         return qs

{"golden_diff": "diff --git a/netbox/netbox/api/viewsets/mixins.py b/netbox/netbox/api/viewsets/mixins.py\n--- a/netbox/netbox/api/viewsets/mixins.py\n+++ b/netbox/netbox/api/viewsets/mixins.py\n@@ -56,8 +56,15 @@\n     def get_queryset(self):\n         qs = super().get_queryset()\n \n-        # If using brief mode, clear all prefetches from the queryset and append only brief_prefetch_fields (if any)\n         if self.brief:\n+            serializer_class = self.get_serializer_class()\n+\n+            # Clear any annotations for fields not present on the nested serializer\n+            for annotation in list(qs.query.annotations.keys()):\n+                if annotation not in serializer_class().fields:\n+                    qs.query.annotations.pop(annotation)\n+\n+            # Clear any prefetches from the queryset and append only brief_prefetch_fields (if any)\n             return qs.prefetch_related(None).prefetch_related(*self.brief_prefetch_fields)\n \n         return qs\n", "issue": "Poor performances on create/edit tenant field with high number of tenants \n### Deployment Type\r\n\r\nSelf-hosted\r\n\r\n### NetBox Version\r\n\r\nv3.6.6\r\n\r\n### Python Version\r\n\r\n3.11\r\n\r\n### Steps to Reproduce\r\n\r\n* Create around 85k tenant in a single tenant group\r\n* Create around 150k prefixes linked with differents tenants\r\n* Create some sites, linked with differents tenants\r\n* Then edit one of the prefix, and click on tenant selector.\r\n\r\n### Expected Behavior\r\n\r\nMaybe one second or less should be a good request timer\r\nOr maybe the searchable select should be async to allow user to enter characters while the first request is running in background.\r\n\r\n### Observed Behavior\r\n\r\nWith a postgres 16 database, and 16GB of ram and 4 cpus, It will take 3.5 seconds to respond and give the user the opportunity to enter some search characters.\r\n\r\nAs far i can go through the debug process, i can see theses kinds of SQL request with long running timer ( >3s):\r\n```sql\r\nSELECT \r\n  DISTINCT \"tenancy_tenant\".\"id\", \r\n  \"tenancy_tenant\".\"created\", \r\n  \"tenancy_tenant\".\"last_updated\", \r\n  \"tenancy_tenant\".\"custom_field_data\", \r\n  \"tenancy_tenant\".\"description\", \r\n  \"tenancy_tenant\".\"comments\", \r\n  \"tenancy_tenant\".\"name\", \r\n  \"tenancy_tenant\".\"slug\", \r\n  \"tenancy_tenant\".\"group_id\", \r\n  COALESCE(\r\n    (\r\n      SELECT \r\n        COUNT(*) AS \"c\" \r\n      FROM \r\n        \"circuits_circuit\" U0 \r\n      WHERE \r\n        U0.\"tenant_id\" = (\"tenancy_tenant\".\"id\") \r\n      GROUP BY \r\n        U0.\"tenant_id\"\r\n    ), \r\n    0\r\n  ) AS \"circuit_count\", \r\n  COALESCE(\r\n    (\r\n      SELECT \r\n        COUNT(*) AS \"c\" \r\n      FROM \r\n        \"dcim_device\" U0 \r\n      WHERE \r\n        U0.\"tenant_id\" = (\"tenancy_tenant\".\"id\") \r\n      GROUP BY \r\n        U0.\"tenant_id\"\r\n    ), \r\n    0\r\n  ) AS \"device_count\", \r\n  COALESCE(\r\n    (\r\n      SELECT \r\n        COUNT(*) AS \"c\" \r\n      FROM \r\n        \"ipam_ipaddress\" U0 \r\n      WHERE \r\n        U0.\"tenant_id\" = (\"tenancy_tenant\".\"id\") \r\n      GROUP BY \r\n        U0.\"tenant_id\"\r\n    ), \r\n    0\r\n  ) AS \"ipaddress_count\", \r\n  COALESCE(\r\n    (\r\n      SELECT \r\n        COUNT(*) AS \"c\" \r\n      FROM \r\n        \"ipam_prefix\" U0 \r\n      WHERE \r\n        U0.\"tenant_id\" = (\"tenancy_tenant\".\"id\") \r\n      GROUP BY \r\n        U0.\"tenant_id\"\r\n    ), \r\n    0\r\n  ) AS \"prefix_count\", \r\n  COALESCE(\r\n    (\r\n      SELECT \r\n        COUNT(*) AS \"c\" \r\n      FROM \r\n        \"dcim_rack\" U0 \r\n      WHERE \r\n        U0.\"tenant_id\" = (\"tenancy_tenant\".\"id\") \r\n      GROUP BY \r\n        U0.\"tenant_id\"\r\n    ), \r\n    0\r\n  ) AS \"rack_count\", \r\n  COALESCE(\r\n    (\r\n      SELECT \r\n        COUNT(*) AS \"c\" \r\n      FROM \r\n        \"dcim_site\" U0 \r\n      WHERE \r\n        U0.\"tenant_id\" = (\"tenancy_tenant\".\"id\") \r\n      GROUP BY \r\n        U0.\"tenant_id\"\r\n    ), \r\n    0\r\n  ) AS \"site_count\", \r\n  COALESCE(\r\n    (\r\n      SELECT \r\n        COUNT(*) AS \"c\" \r\n      FROM \r\n        \"virtualization_virtualmachine\" U0 \r\n      WHERE \r\n        U0.\"tenant_id\" = (\"tenancy_tenant\".\"id\") \r\n      GROUP BY \r\n        U0.\"tenant_id\"\r\n    ), \r\n    0\r\n  ) AS \"virtualmachine_count\", \r\n  COALESCE(\r\n    (\r\n      SELECT \r\n        COUNT(*) AS \"c\" \r\n      FROM \r\n        \"ipam_vlan\" U0 \r\n      WHERE \r\n        U0.\"tenant_id\" = (\"tenancy_tenant\".\"id\") \r\n      GROUP BY \r\n        U0.\"tenant_id\"\r\n    ), \r\n    0\r\n  ) AS \"vlan_count\", \r\n  COALESCE(\r\n    (\r\n      SELECT \r\n        COUNT(*) AS \"c\" \r\n      FROM \r\n        \"ipam_vrf\" U0 \r\n      WHERE \r\n        U0.\"tenant_id\" = (\"tenancy_tenant\".\"id\") \r\n      GROUP BY \r\n        U0.\"tenant_id\"\r\n    ), \r\n    0\r\n  ) AS \"vrf_count\", \r\n  COALESCE(\r\n    (\r\n      SELECT \r\n        COUNT(*) AS \"c\" \r\n      FROM \r\n        \"virtualization_cluster\" U0 \r\n      WHERE \r\n        U0.\"tenant_id\" = (\"tenancy_tenant\".\"id\") \r\n      GROUP BY \r\n        U0.\"tenant_id\"\r\n    ), \r\n    0\r\n  ) AS \"cluster_count\" \r\nFROM \r\n  \"tenancy_tenant\" \r\nWHERE \r\n  \"tenancy_tenant\".\"group_id\" IN (\r\n    SELECT \r\n      U0.\"id\" \r\n    FROM \r\n      \"tenancy_tenantgroup\" U0 \r\n    WHERE \r\n      U0.\"id\" = 4\r\n  ) \r\nORDER BY \r\n  \"tenancy_tenant\".\"name\" ASC \r\nLIMIT \r\n  50\r\n```\r\n\r\nTheses request, for each kind of related objects making a count of them. So with 85k tenant, each count is executed 85k times.\r\nFrom my point of view, to display a dynamic selector with customer display field, it's not necessary to count every objects.\r\n\r\nTo be more specific on the pgsql server, the request is executed on RAM buffered datas. No hit on system SSD. The cache usage is around 30% and hit objects.\r\n\r\nWe already have a discussion about it on slack: https://netdev-community.slack.com/archives/C01P0FRSXRV/p1702476390699559\n", "before_files": [{"content": "import logging\n\nfrom django.contrib.contenttypes.models import ContentType\nfrom django.core.exceptions import ObjectDoesNotExist\nfrom django.db import transaction\nfrom django.http import Http404\nfrom rest_framework import status\nfrom rest_framework.response import Response\n\nfrom extras.models import ExportTemplate\nfrom netbox.api.exceptions import SerializerNotFound\nfrom netbox.api.serializers import BulkOperationSerializer\nfrom netbox.constants import NESTED_SERIALIZER_PREFIX\nfrom utilities.api import get_serializer_for_model\n\n__all__ = (\n    'BriefModeMixin',\n    'BulkDestroyModelMixin',\n    'BulkUpdateModelMixin',\n    'CustomFieldsMixin',\n    'ExportTemplatesMixin',\n    'ObjectValidationMixin',\n    'SequentialBulkCreatesMixin',\n)\n\n\nclass BriefModeMixin:\n    \"\"\"\n    Enables brief mode support, so that the client can invoke a model's nested serializer by passing e.g.\n        GET /api/dcim/sites/?brief=True\n    \"\"\"\n    brief = False\n    brief_prefetch_fields = []\n\n    def initialize_request(self, request, *args, **kwargs):\n        # Annotate whether brief mode is active\n        self.brief = request.method == 'GET' and request.GET.get('brief')\n\n        return super().initialize_request(request, *args, **kwargs)\n\n    def get_serializer_class(self):\n        logger = logging.getLogger(f'netbox.api.views.{self.__class__.__name__}')\n\n        # If using 'brief' mode, find and return the nested serializer for this model, if one exists\n        if self.brief:\n            logger.debug(\"Request is for 'brief' format; initializing nested serializer\")\n            try:\n                return get_serializer_for_model(self.queryset.model, prefix=NESTED_SERIALIZER_PREFIX)\n            except SerializerNotFound:\n                logger.debug(\n                    f\"Nested serializer for {self.queryset.model} not found! Using serializer {self.serializer_class}\"\n                )\n\n        return self.serializer_class\n\n    def get_queryset(self):\n        qs = super().get_queryset()\n\n        # If using brief mode, clear all prefetches from the queryset and append only brief_prefetch_fields (if any)\n        if self.brief:\n            return qs.prefetch_related(None).prefetch_related(*self.brief_prefetch_fields)\n\n        return qs\n\n\nclass CustomFieldsMixin:\n    \"\"\"\n    For models which support custom fields, populate the `custom_fields` context.\n    \"\"\"\n    def get_serializer_context(self):\n        context = super().get_serializer_context()\n\n        if hasattr(self.queryset.model, 'custom_fields'):\n            content_type = ContentType.objects.get_for_model(self.queryset.model)\n            context.update({\n                'custom_fields': content_type.custom_fields.all(),\n            })\n\n        return context\n\n\nclass ExportTemplatesMixin:\n    \"\"\"\n    Enable ExportTemplate support for list views.\n    \"\"\"\n    def list(self, request, *args, **kwargs):\n        if 'export' in request.GET:\n            content_type = ContentType.objects.get_for_model(self.get_serializer_class().Meta.model)\n            et = ExportTemplate.objects.filter(content_types=content_type, name=request.GET['export']).first()\n            if et is None:\n                raise Http404\n            queryset = self.filter_queryset(self.get_queryset())\n            return et.render_to_response(queryset)\n\n        return super().list(request, *args, **kwargs)\n\n\nclass SequentialBulkCreatesMixin:\n    \"\"\"\n    Perform bulk creation of new objects sequentially, rather than all at once. This ensures that any validation\n    which depends on the evaluation of existing objects (such as checking for free space within a rack) functions\n    appropriately.\n    \"\"\"\n    @transaction.atomic\n    def create(self, request, *args, **kwargs):\n        if not isinstance(request.data, list):\n            # Creating a single object\n            return super().create(request, *args, **kwargs)\n\n        return_data = []\n        for data in request.data:\n            serializer = self.get_serializer(data=data)\n            serializer.is_valid(raise_exception=True)\n            self.perform_create(serializer)\n            return_data.append(serializer.data)\n\n        headers = self.get_success_headers(serializer.data)\n\n        return Response(return_data, status=status.HTTP_201_CREATED, headers=headers)\n\n\nclass BulkUpdateModelMixin:\n    \"\"\"\n    Support bulk modification of objects using the list endpoint for a model. Accepts a PATCH action with a list of one\n    or more JSON objects, each specifying the numeric ID of an object to be updated as well as the attributes to be set.\n    For example:\n\n    PATCH /api/dcim/sites/\n    [\n        {\n            \"id\": 123,\n            \"name\": \"New name\"\n        },\n        {\n            \"id\": 456,\n            \"status\": \"planned\"\n        }\n    ]\n    \"\"\"\n    def bulk_update(self, request, *args, **kwargs):\n        partial = kwargs.pop('partial', False)\n        serializer = BulkOperationSerializer(data=request.data, many=True)\n        serializer.is_valid(raise_exception=True)\n        qs = self.get_queryset().filter(\n            pk__in=[o['id'] for o in serializer.data]\n        )\n\n        # Map update data by object ID\n        update_data = {\n            obj.pop('id'): obj for obj in request.data\n        }\n\n        data = self.perform_bulk_update(qs, update_data, partial=partial)\n\n        return Response(data, status=status.HTTP_200_OK)\n\n    def perform_bulk_update(self, objects, update_data, partial):\n        with transaction.atomic():\n            data_list = []\n            for obj in objects:\n                data = update_data.get(obj.id)\n                if hasattr(obj, 'snapshot'):\n                    obj.snapshot()\n                serializer = self.get_serializer(obj, data=data, partial=partial)\n                serializer.is_valid(raise_exception=True)\n                self.perform_update(serializer)\n                data_list.append(serializer.data)\n\n            return data_list\n\n    def bulk_partial_update(self, request, *args, **kwargs):\n        kwargs['partial'] = True\n        return self.bulk_update(request, *args, **kwargs)\n\n\nclass BulkDestroyModelMixin:\n    \"\"\"\n    Support bulk deletion of objects using the list endpoint for a model. Accepts a DELETE action with a list of one\n    or more JSON objects, each specifying the numeric ID of an object to be deleted. For example:\n\n    DELETE /api/dcim/sites/\n    [\n        {\"id\": 123},\n        {\"id\": 456}\n    ]\n    \"\"\"\n    def bulk_destroy(self, request, *args, **kwargs):\n        serializer = BulkOperationSerializer(data=request.data, many=True)\n        serializer.is_valid(raise_exception=True)\n        qs = self.get_queryset().filter(\n            pk__in=[o['id'] for o in serializer.data]\n        )\n\n        self.perform_bulk_destroy(qs)\n\n        return Response(status=status.HTTP_204_NO_CONTENT)\n\n    def perform_bulk_destroy(self, objects):\n        with transaction.atomic():\n            for obj in objects:\n                if hasattr(obj, 'snapshot'):\n                    obj.snapshot()\n                self.perform_destroy(obj)\n\n\nclass ObjectValidationMixin:\n\n    def _validate_objects(self, instance):\n        \"\"\"\n        Check that the provided instance or list of instances are matched by the current queryset. This confirms that\n        any newly created or modified objects abide by the attributes granted by any applicable ObjectPermissions.\n        \"\"\"\n        if type(instance) is list:\n            # Check that all instances are still included in the view's queryset\n            conforming_count = self.queryset.filter(pk__in=[obj.pk for obj in instance]).count()\n            if conforming_count != len(instance):\n                raise ObjectDoesNotExist\n        elif not self.queryset.filter(pk=instance.pk).exists():\n            raise ObjectDoesNotExist\n", "path": "netbox/netbox/api/viewsets/mixins.py"}], "after_files": [{"content": "import logging\n\nfrom django.contrib.contenttypes.models import ContentType\nfrom django.core.exceptions import ObjectDoesNotExist\nfrom django.db import transaction\nfrom django.http import Http404\nfrom rest_framework import status\nfrom rest_framework.response import Response\n\nfrom extras.models import ExportTemplate\nfrom netbox.api.exceptions import SerializerNotFound\nfrom netbox.api.serializers import BulkOperationSerializer\nfrom netbox.constants import NESTED_SERIALIZER_PREFIX\nfrom utilities.api import get_serializer_for_model\n\n__all__ = (\n    'BriefModeMixin',\n    'BulkDestroyModelMixin',\n    'BulkUpdateModelMixin',\n    'CustomFieldsMixin',\n    'ExportTemplatesMixin',\n    'ObjectValidationMixin',\n    'SequentialBulkCreatesMixin',\n)\n\n\nclass BriefModeMixin:\n    \"\"\"\n    Enables brief mode support, so that the client can invoke a model's nested serializer by passing e.g.\n        GET /api/dcim/sites/?brief=True\n    \"\"\"\n    brief = False\n    brief_prefetch_fields = []\n\n    def initialize_request(self, request, *args, **kwargs):\n        # Annotate whether brief mode is active\n        self.brief = request.method == 'GET' and request.GET.get('brief')\n\n        return super().initialize_request(request, *args, **kwargs)\n\n    def get_serializer_class(self):\n        logger = logging.getLogger(f'netbox.api.views.{self.__class__.__name__}')\n\n        # If using 'brief' mode, find and return the nested serializer for this model, if one exists\n        if self.brief:\n            logger.debug(\"Request is for 'brief' format; initializing nested serializer\")\n            try:\n                return get_serializer_for_model(self.queryset.model, prefix=NESTED_SERIALIZER_PREFIX)\n            except SerializerNotFound:\n                logger.debug(\n                    f\"Nested serializer for {self.queryset.model} not found! Using serializer {self.serializer_class}\"\n                )\n\n        return self.serializer_class\n\n    def get_queryset(self):\n        qs = super().get_queryset()\n\n        if self.brief:\n            serializer_class = self.get_serializer_class()\n\n            # Clear any annotations for fields not present on the nested serializer\n            for annotation in list(qs.query.annotations.keys()):\n                if annotation not in serializer_class().fields:\n                    qs.query.annotations.pop(annotation)\n\n            # Clear any prefetches from the queryset and append only brief_prefetch_fields (if any)\n            return qs.prefetch_related(None).prefetch_related(*self.brief_prefetch_fields)\n\n        return qs\n\n\nclass CustomFieldsMixin:\n    \"\"\"\n    For models which support custom fields, populate the `custom_fields` context.\n    \"\"\"\n    def get_serializer_context(self):\n        context = super().get_serializer_context()\n\n        if hasattr(self.queryset.model, 'custom_fields'):\n            content_type = ContentType.objects.get_for_model(self.queryset.model)\n            context.update({\n                'custom_fields': content_type.custom_fields.all(),\n            })\n\n        return context\n\n\nclass ExportTemplatesMixin:\n    \"\"\"\n    Enable ExportTemplate support for list views.\n    \"\"\"\n    def list(self, request, *args, **kwargs):\n        if 'export' in request.GET:\n            content_type = ContentType.objects.get_for_model(self.get_serializer_class().Meta.model)\n            et = ExportTemplate.objects.filter(content_types=content_type, name=request.GET['export']).first()\n            if et is None:\n                raise Http404\n            queryset = self.filter_queryset(self.get_queryset())\n            return et.render_to_response(queryset)\n\n        return super().list(request, *args, **kwargs)\n\n\nclass SequentialBulkCreatesMixin:\n    \"\"\"\n    Perform bulk creation of new objects sequentially, rather than all at once. This ensures that any validation\n    which depends on the evaluation of existing objects (such as checking for free space within a rack) functions\n    appropriately.\n    \"\"\"\n    @transaction.atomic\n    def create(self, request, *args, **kwargs):\n        if not isinstance(request.data, list):\n            # Creating a single object\n            return super().create(request, *args, **kwargs)\n\n        return_data = []\n        for data in request.data:\n            serializer = self.get_serializer(data=data)\n            serializer.is_valid(raise_exception=True)\n            self.perform_create(serializer)\n            return_data.append(serializer.data)\n\n        headers = self.get_success_headers(serializer.data)\n\n        return Response(return_data, status=status.HTTP_201_CREATED, headers=headers)\n\n\nclass BulkUpdateModelMixin:\n    \"\"\"\n    Support bulk modification of objects using the list endpoint for a model. Accepts a PATCH action with a list of one\n    or more JSON objects, each specifying the numeric ID of an object to be updated as well as the attributes to be set.\n    For example:\n\n    PATCH /api/dcim/sites/\n    [\n        {\n            \"id\": 123,\n            \"name\": \"New name\"\n        },\n        {\n            \"id\": 456,\n            \"status\": \"planned\"\n        }\n    ]\n    \"\"\"\n    def bulk_update(self, request, *args, **kwargs):\n        partial = kwargs.pop('partial', False)\n        serializer = BulkOperationSerializer(data=request.data, many=True)\n        serializer.is_valid(raise_exception=True)\n        qs = self.get_queryset().filter(\n            pk__in=[o['id'] for o in serializer.data]\n        )\n\n        # Map update data by object ID\n        update_data = {\n            obj.pop('id'): obj for obj in request.data\n        }\n\n        data = self.perform_bulk_update(qs, update_data, partial=partial)\n\n        return Response(data, status=status.HTTP_200_OK)\n\n    def perform_bulk_update(self, objects, update_data, partial):\n        with transaction.atomic():\n            data_list = []\n            for obj in objects:\n                data = update_data.get(obj.id)\n                if hasattr(obj, 'snapshot'):\n                    obj.snapshot()\n                serializer = self.get_serializer(obj, data=data, partial=partial)\n                serializer.is_valid(raise_exception=True)\n                self.perform_update(serializer)\n                data_list.append(serializer.data)\n\n            return data_list\n\n    def bulk_partial_update(self, request, *args, **kwargs):\n        kwargs['partial'] = True\n        return self.bulk_update(request, *args, **kwargs)\n\n\nclass BulkDestroyModelMixin:\n    \"\"\"\n    Support bulk deletion of objects using the list endpoint for a model. Accepts a DELETE action with a list of one\n    or more JSON objects, each specifying the numeric ID of an object to be deleted. For example:\n\n    DELETE /api/dcim/sites/\n    [\n        {\"id\": 123},\n        {\"id\": 456}\n    ]\n    \"\"\"\n    def bulk_destroy(self, request, *args, **kwargs):\n        serializer = BulkOperationSerializer(data=request.data, many=True)\n        serializer.is_valid(raise_exception=True)\n        qs = self.get_queryset().filter(\n            pk__in=[o['id'] for o in serializer.data]\n        )\n\n        self.perform_bulk_destroy(qs)\n\n        return Response(status=status.HTTP_204_NO_CONTENT)\n\n    def perform_bulk_destroy(self, objects):\n        with transaction.atomic():\n            for obj in objects:\n                if hasattr(obj, 'snapshot'):\n                    obj.snapshot()\n                self.perform_destroy(obj)\n\n\nclass ObjectValidationMixin:\n\n    def _validate_objects(self, instance):\n        \"\"\"\n        Check that the provided instance or list of instances are matched by the current queryset. This confirms that\n        any newly created or modified objects abide by the attributes granted by any applicable ObjectPermissions.\n        \"\"\"\n        if type(instance) is list:\n            # Check that all instances are still included in the view's queryset\n            conforming_count = self.queryset.filter(pk__in=[obj.pk for obj in instance]).count()\n            if conforming_count != len(instance):\n                raise ObjectDoesNotExist\n        elif not self.queryset.filter(pk=instance.pk).exists():\n            raise ObjectDoesNotExist\n", "path": "netbox/netbox/api/viewsets/mixins.py"}]}