Răsfoiți Sursa

Merge branch 'develop' into develop-2.10

Jeremy Stretch 5 ani în urmă
părinte
comite
915cf3e715

+ 8 - 0
docs/release-notes/version-2.9.md

@@ -1,5 +1,13 @@
 # NetBox v2.9
 # NetBox v2.9
 
 
+## v2.9.11 (FUTURE)
+
+### Bug Fixes
+
+* [#5383](https://github.com/netbox-community/netbox/issues/5383) - Fix setting user password via REST API
+
+---
+
 ## v2.9.10 (2020-11-24)
 ## v2.9.10 (2020-11-24)
 
 
 ### Enhancements
 ### Enhancements

+ 6 - 5
netbox/circuits/api/views.py

@@ -1,4 +1,4 @@
-from django.db.models import Count, Prefetch
+from django.db.models import Prefetch
 from rest_framework.routers import APIRootView
 from rest_framework.routers import APIRootView
 
 
 from circuits import filters
 from circuits import filters
@@ -6,6 +6,7 @@ from circuits.models import Provider, CircuitTermination, CircuitType, Circuit
 from dcim.api.views import PathEndpointMixin
 from dcim.api.views import PathEndpointMixin
 from extras.api.views import CustomFieldModelViewSet
 from extras.api.views import CustomFieldModelViewSet
 from netbox.api.views import ModelViewSet
 from netbox.api.views import ModelViewSet
+from utilities.utils import get_subquery
 from . import serializers
 from . import serializers
 
 
 
 
@@ -23,8 +24,8 @@ class CircuitsRootView(APIRootView):
 
 
 class ProviderViewSet(CustomFieldModelViewSet):
 class ProviderViewSet(CustomFieldModelViewSet):
     queryset = Provider.objects.prefetch_related('tags').annotate(
     queryset = Provider.objects.prefetch_related('tags').annotate(
-        circuit_count=Count('circuits')
-    ).order_by(*Provider._meta.ordering)
+        circuit_count=get_subquery(Circuit, 'provider')
+    )
     serializer_class = serializers.ProviderSerializer
     serializer_class = serializers.ProviderSerializer
     filterset_class = filters.ProviderFilterSet
     filterset_class = filters.ProviderFilterSet
 
 
@@ -35,8 +36,8 @@ class ProviderViewSet(CustomFieldModelViewSet):
 
 
 class CircuitTypeViewSet(ModelViewSet):
 class CircuitTypeViewSet(ModelViewSet):
     queryset = CircuitType.objects.annotate(
     queryset = CircuitType.objects.annotate(
-        circuit_count=Count('circuits')
-    ).order_by(*CircuitType._meta.ordering)
+        circuit_count=get_subquery(Circuit, 'type')
+    )
     serializer_class = serializers.CircuitTypeSerializer
     serializer_class = serializers.CircuitTypeSerializer
     filterset_class = filters.CircuitTypeFilterSet
     filterset_class = filters.CircuitTypeFilterSet
 
 

+ 16 - 6
netbox/circuits/views.py

@@ -1,12 +1,12 @@
 from django.contrib import messages
 from django.contrib import messages
 from django.db import transaction
 from django.db import transaction
-from django.db.models import Count
 from django.shortcuts import get_object_or_404, redirect, render
 from django.shortcuts import get_object_or_404, redirect, render
 from django_tables2 import RequestConfig
 from django_tables2 import RequestConfig
 
 
 from netbox.views import generic
 from netbox.views import generic
 from utilities.forms import ConfirmationForm
 from utilities.forms import ConfirmationForm
 from utilities.paginator import EnhancedPaginator, get_paginate_count
 from utilities.paginator import EnhancedPaginator, get_paginate_count
+from utilities.utils import get_subquery
 from . import filters, forms, tables
 from . import filters, forms, tables
 from .choices import CircuitTerminationSideChoices
 from .choices import CircuitTerminationSideChoices
 from .models import Circuit, CircuitTermination, CircuitType, Provider
 from .models import Circuit, CircuitTermination, CircuitType, Provider
@@ -17,7 +17,9 @@ from .models import Circuit, CircuitTermination, CircuitType, Provider
 #
 #
 
 
 class ProviderListView(generic.ObjectListView):
 class ProviderListView(generic.ObjectListView):
-    queryset = Provider.objects.annotate(count_circuits=Count('circuits')).order_by(*Provider._meta.ordering)
+    queryset = Provider.objects.annotate(
+        count_circuits=get_subquery(Circuit, 'provider')
+    )
     filterset = filters.ProviderFilterSet
     filterset = filters.ProviderFilterSet
     filterset_form = forms.ProviderFilterForm
     filterset_form = forms.ProviderFilterForm
     table = tables.ProviderTable
     table = tables.ProviderTable
@@ -64,14 +66,18 @@ class ProviderBulkImportView(generic.BulkImportView):
 
 
 
 
 class ProviderBulkEditView(generic.BulkEditView):
 class ProviderBulkEditView(generic.BulkEditView):
-    queryset = Provider.objects.annotate(count_circuits=Count('circuits')).order_by(*Provider._meta.ordering)
+    queryset = Provider.objects.annotate(
+        count_circuits=get_subquery(Circuit, 'provider')
+    )
     filterset = filters.ProviderFilterSet
     filterset = filters.ProviderFilterSet
     table = tables.ProviderTable
     table = tables.ProviderTable
     form = forms.ProviderBulkEditForm
     form = forms.ProviderBulkEditForm
 
 
 
 
 class ProviderBulkDeleteView(generic.BulkDeleteView):
 class ProviderBulkDeleteView(generic.BulkDeleteView):
-    queryset = Provider.objects.annotate(count_circuits=Count('circuits')).order_by(*Provider._meta.ordering)
+    queryset = Provider.objects.annotate(
+        count_circuits=get_subquery(Circuit, 'provider')
+    )
     filterset = filters.ProviderFilterSet
     filterset = filters.ProviderFilterSet
     table = tables.ProviderTable
     table = tables.ProviderTable
 
 
@@ -81,7 +87,9 @@ class ProviderBulkDeleteView(generic.BulkDeleteView):
 #
 #
 
 
 class CircuitTypeListView(generic.ObjectListView):
 class CircuitTypeListView(generic.ObjectListView):
-    queryset = CircuitType.objects.annotate(circuit_count=Count('circuits')).order_by(*CircuitType._meta.ordering)
+    queryset = CircuitType.objects.annotate(
+        circuit_count=get_subquery(Circuit, 'type')
+    )
     table = tables.CircuitTypeTable
     table = tables.CircuitTypeTable
 
 
 
 
@@ -101,7 +109,9 @@ class CircuitTypeBulkImportView(generic.BulkImportView):
 
 
 
 
 class CircuitTypeBulkDeleteView(generic.BulkDeleteView):
 class CircuitTypeBulkDeleteView(generic.BulkDeleteView):
-    queryset = CircuitType.objects.annotate(circuit_count=Count('circuits')).order_by(*CircuitType._meta.ordering)
+    queryset = CircuitType.objects.annotate(
+        circuit_count=get_subquery(Circuit, 'type')
+    )
     table = tables.CircuitTypeTable
     table = tables.CircuitTypeTable
 
 
 
 

+ 14 - 14
netbox/dcim/api/views.py

@@ -2,7 +2,7 @@ import socket
 from collections import OrderedDict
 from collections import OrderedDict
 
 
 from django.conf import settings
 from django.conf import settings
-from django.db.models import Count, F
+from django.db.models import F
 from django.http import HttpResponseForbidden, HttpResponse
 from django.http import HttpResponseForbidden, HttpResponse
 from django.shortcuts import get_object_or_404
 from django.shortcuts import get_object_or_404
 from drf_yasg import openapi
 from drf_yasg import openapi
@@ -125,7 +125,7 @@ class SiteViewSet(CustomFieldModelViewSet):
         vlan_count=get_subquery(VLAN, 'site'),
         vlan_count=get_subquery(VLAN, 'site'),
         circuit_count=get_subquery(Circuit, 'terminations__site'),
         circuit_count=get_subquery(Circuit, 'terminations__site'),
         virtualmachine_count=get_subquery(VirtualMachine, 'cluster__site'),
         virtualmachine_count=get_subquery(VirtualMachine, 'cluster__site'),
-    ).order_by(*Site._meta.ordering)
+    )
     serializer_class = serializers.SiteSerializer
     serializer_class = serializers.SiteSerializer
     filterset_class = filters.SiteFilterSet
     filterset_class = filters.SiteFilterSet
 
 
@@ -152,8 +152,8 @@ class RackGroupViewSet(ModelViewSet):
 
 
 class RackRoleViewSet(ModelViewSet):
 class RackRoleViewSet(ModelViewSet):
     queryset = RackRole.objects.annotate(
     queryset = RackRole.objects.annotate(
-        rack_count=Count('racks')
-    ).order_by(*RackRole._meta.ordering)
+        rack_count=get_subquery(Rack, 'role')
+    )
     serializer_class = serializers.RackRoleSerializer
     serializer_class = serializers.RackRoleSerializer
     filterset_class = filters.RackRoleFilterSet
     filterset_class = filters.RackRoleFilterSet
 
 
@@ -168,7 +168,7 @@ class RackViewSet(CustomFieldModelViewSet):
     ).annotate(
     ).annotate(
         device_count=get_subquery(Device, 'rack'),
         device_count=get_subquery(Device, 'rack'),
         powerfeed_count=get_subquery(PowerFeed, 'rack')
         powerfeed_count=get_subquery(PowerFeed, 'rack')
-    ).order_by(*Rack._meta.ordering)
+    )
     serializer_class = serializers.RackSerializer
     serializer_class = serializers.RackSerializer
     filterset_class = filters.RackFilterSet
     filterset_class = filters.RackFilterSet
 
 
@@ -243,7 +243,7 @@ class ManufacturerViewSet(ModelViewSet):
         devicetype_count=get_subquery(DeviceType, 'manufacturer'),
         devicetype_count=get_subquery(DeviceType, 'manufacturer'),
         inventoryitem_count=get_subquery(InventoryItem, 'manufacturer'),
         inventoryitem_count=get_subquery(InventoryItem, 'manufacturer'),
         platform_count=get_subquery(Platform, 'manufacturer')
         platform_count=get_subquery(Platform, 'manufacturer')
-    ).order_by(*Manufacturer._meta.ordering)
+    )
     serializer_class = serializers.ManufacturerSerializer
     serializer_class = serializers.ManufacturerSerializer
     filterset_class = filters.ManufacturerFilterSet
     filterset_class = filters.ManufacturerFilterSet
 
 
@@ -254,8 +254,8 @@ class ManufacturerViewSet(ModelViewSet):
 
 
 class DeviceTypeViewSet(CustomFieldModelViewSet):
 class DeviceTypeViewSet(CustomFieldModelViewSet):
     queryset = DeviceType.objects.prefetch_related('manufacturer', 'tags').annotate(
     queryset = DeviceType.objects.prefetch_related('manufacturer', 'tags').annotate(
-        device_count=Count('instances')
-    ).order_by(*DeviceType._meta.ordering)
+        device_count=get_subquery(Device, 'device_type')
+    )
     serializer_class = serializers.DeviceTypeSerializer
     serializer_class = serializers.DeviceTypeSerializer
     filterset_class = filters.DeviceTypeFilterSet
     filterset_class = filters.DeviceTypeFilterSet
 
 
@@ -320,7 +320,7 @@ class DeviceRoleViewSet(ModelViewSet):
     queryset = DeviceRole.objects.annotate(
     queryset = DeviceRole.objects.annotate(
         device_count=get_subquery(Device, 'device_role'),
         device_count=get_subquery(Device, 'device_role'),
         virtualmachine_count=get_subquery(VirtualMachine, 'role')
         virtualmachine_count=get_subquery(VirtualMachine, 'role')
-    ).order_by(*DeviceRole._meta.ordering)
+    )
     serializer_class = serializers.DeviceRoleSerializer
     serializer_class = serializers.DeviceRoleSerializer
     filterset_class = filters.DeviceRoleFilterSet
     filterset_class = filters.DeviceRoleFilterSet
 
 
@@ -333,7 +333,7 @@ class PlatformViewSet(ModelViewSet):
     queryset = Platform.objects.annotate(
     queryset = Platform.objects.annotate(
         device_count=get_subquery(Device, 'platform'),
         device_count=get_subquery(Device, 'platform'),
         virtualmachine_count=get_subquery(VirtualMachine, 'platform')
         virtualmachine_count=get_subquery(VirtualMachine, 'platform')
-    ).order_by(*Platform._meta.ordering)
+    )
     serializer_class = serializers.PlatformSerializer
     serializer_class = serializers.PlatformSerializer
     filterset_class = filters.PlatformFilterSet
     filterset_class = filters.PlatformFilterSet
 
 
@@ -596,8 +596,8 @@ class CableViewSet(ModelViewSet):
 
 
 class VirtualChassisViewSet(ModelViewSet):
 class VirtualChassisViewSet(ModelViewSet):
     queryset = VirtualChassis.objects.prefetch_related('tags').annotate(
     queryset = VirtualChassis.objects.prefetch_related('tags').annotate(
-        member_count=Count('members', distinct=True)
-    ).order_by(*VirtualChassis._meta.ordering)
+        member_count=get_subquery(Device, 'virtual_chassis')
+    )
     serializer_class = serializers.VirtualChassisSerializer
     serializer_class = serializers.VirtualChassisSerializer
     filterset_class = filters.VirtualChassisFilterSet
     filterset_class = filters.VirtualChassisFilterSet
 
 
@@ -610,8 +610,8 @@ class PowerPanelViewSet(ModelViewSet):
     queryset = PowerPanel.objects.prefetch_related(
     queryset = PowerPanel.objects.prefetch_related(
         'site', 'rack_group'
         'site', 'rack_group'
     ).annotate(
     ).annotate(
-        powerfeed_count=Count('powerfeeds')
-    ).order_by(*PowerPanel._meta.ordering)
+        powerfeed_count=get_subquery(PowerFeed, 'power_panel')
+    )
     serializer_class = serializers.PowerPanelSerializer
     serializer_class = serializers.PowerPanelSerializer
     filterset_class = filters.PowerPanelFilterSet
     filterset_class = filters.PowerPanelFilterSet
 
 

+ 31 - 23
netbox/dcim/views.py

@@ -4,7 +4,7 @@ from django.contrib import messages
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
 from django.core.paginator import EmptyPage, PageNotAnInteger
 from django.core.paginator import EmptyPage, PageNotAnInteger
 from django.db import transaction
 from django.db import transaction
-from django.db.models import Count, F, Prefetch
+from django.db.models import F, Prefetch
 from django.forms import ModelMultipleChoiceField, MultipleHiddenInput, modelformset_factory
 from django.forms import ModelMultipleChoiceField, MultipleHiddenInput, modelformset_factory
 from django.shortcuts import get_object_or_404, redirect, render
 from django.shortcuts import get_object_or_404, redirect, render
 from django.utils.html import escape
 from django.utils.html import escape
@@ -253,7 +253,9 @@ class RackGroupBulkDeleteView(generic.BulkDeleteView):
 #
 #
 
 
 class RackRoleListView(generic.ObjectListView):
 class RackRoleListView(generic.ObjectListView):
-    queryset = RackRole.objects.annotate(rack_count=Count('racks')).order_by(*RackRole._meta.ordering)
+    queryset = RackRole.objects.annotate(
+        rack_count=get_subquery(Rack, 'role')
+    )
     table = tables.RackRoleTable
     table = tables.RackRoleTable
 
 
 
 
@@ -273,7 +275,9 @@ class RackRoleBulkImportView(generic.BulkImportView):
 
 
 
 
 class RackRoleBulkDeleteView(generic.BulkDeleteView):
 class RackRoleBulkDeleteView(generic.BulkDeleteView):
-    queryset = RackRole.objects.annotate(rack_count=Count('racks')).order_by(*RackRole._meta.ordering)
+    queryset = RackRole.objects.annotate(
+        rack_count=get_subquery(Rack, 'role')
+    )
     table = tables.RackRoleTable
     table = tables.RackRoleTable
 
 
 
 
@@ -282,9 +286,11 @@ class RackRoleBulkDeleteView(generic.BulkDeleteView):
 #
 #
 
 
 class RackListView(generic.ObjectListView):
 class RackListView(generic.ObjectListView):
-    queryset = Rack.objects.annotate(
-        device_count=Count('devices')
-    ).order_by(*Rack._meta.ordering)
+    queryset = Rack.objects.prefetch_related(
+        'site', 'group', 'tenant', 'role', 'devices__device_type'
+    ).annotate(
+        device_count=get_subquery(Device, 'rack')
+    )
     filterset = filters.RackFilterSet
     filterset = filters.RackFilterSet
     filterset_form = forms.RackFilterForm
     filterset_form = forms.RackFilterForm
     table = tables.RackDetailTable
     table = tables.RackDetailTable
@@ -488,8 +494,8 @@ class ManufacturerBulkImportView(generic.BulkImportView):
 
 
 class ManufacturerBulkDeleteView(generic.BulkDeleteView):
 class ManufacturerBulkDeleteView(generic.BulkDeleteView):
     queryset = Manufacturer.objects.annotate(
     queryset = Manufacturer.objects.annotate(
-        devicetype_count=Count('device_types')
-    ).order_by(*Manufacturer._meta.ordering)
+        devicetype_count=get_subquery(DeviceType, 'manufacturer')
+    )
     table = tables.ManufacturerTable
     table = tables.ManufacturerTable
 
 
 
 
@@ -498,9 +504,9 @@ class ManufacturerBulkDeleteView(generic.BulkDeleteView):
 #
 #
 
 
 class DeviceTypeListView(generic.ObjectListView):
 class DeviceTypeListView(generic.ObjectListView):
-    queryset = DeviceType.objects.annotate(
-        instance_count=Count('instances')
-    ).order_by(*DeviceType._meta.ordering)
+    queryset = DeviceType.objects.prefetch_related('manufacturer').annotate(
+        instance_count=get_subquery(Device, 'device_type')
+    )
     filterset = filters.DeviceTypeFilterSet
     filterset = filters.DeviceTypeFilterSet
     filterset_form = forms.DeviceTypeFilterForm
     filterset_form = forms.DeviceTypeFilterForm
     table = tables.DeviceTypeTable
     table = tables.DeviceTypeTable
@@ -606,8 +612,8 @@ class DeviceTypeImportView(generic.ObjectImportView):
 
 
 class DeviceTypeBulkEditView(generic.BulkEditView):
 class DeviceTypeBulkEditView(generic.BulkEditView):
     queryset = DeviceType.objects.prefetch_related('manufacturer').annotate(
     queryset = DeviceType.objects.prefetch_related('manufacturer').annotate(
-        instance_count=Count('instances')
-    ).order_by(*DeviceType._meta.ordering)
+        instance_count=get_subquery(Device, 'device_type')
+    )
     filterset = filters.DeviceTypeFilterSet
     filterset = filters.DeviceTypeFilterSet
     table = tables.DeviceTypeTable
     table = tables.DeviceTypeTable
     form = forms.DeviceTypeBulkEditForm
     form = forms.DeviceTypeBulkEditForm
@@ -615,8 +621,8 @@ class DeviceTypeBulkEditView(generic.BulkEditView):
 
 
 class DeviceTypeBulkDeleteView(generic.BulkDeleteView):
 class DeviceTypeBulkDeleteView(generic.BulkDeleteView):
     queryset = DeviceType.objects.prefetch_related('manufacturer').annotate(
     queryset = DeviceType.objects.prefetch_related('manufacturer').annotate(
-        instance_count=Count('instances')
-    ).order_by(*DeviceType._meta.ordering)
+        instance_count=get_subquery(Device, 'device_type')
+    )
     filterset = filters.DeviceTypeFilterSet
     filterset = filters.DeviceTypeFilterSet
     table = tables.DeviceTypeTable
     table = tables.DeviceTypeTable
 
 
@@ -2287,9 +2293,9 @@ class InterfaceConnectionsListView(generic.ObjectListView):
 #
 #
 
 
 class VirtualChassisListView(generic.ObjectListView):
 class VirtualChassisListView(generic.ObjectListView):
-    queryset = VirtualChassis.objects.annotate(
-        member_count=Count('members', distinct=True)
-    ).order_by(*VirtualChassis._meta.ordering)
+    queryset = VirtualChassis.objects.prefetch_related('master').annotate(
+        member_count=get_subquery(Device, 'virtual_chassis')
+    )
     table = tables.VirtualChassisTable
     table = tables.VirtualChassisTable
     filterset = filters.VirtualChassisFilterSet
     filterset = filters.VirtualChassisFilterSet
     filterset_form = forms.VirtualChassisFilterForm
     filterset_form = forms.VirtualChassisFilterForm
@@ -2515,9 +2521,11 @@ class VirtualChassisBulkDeleteView(generic.BulkDeleteView):
 #
 #
 
 
 class PowerPanelListView(generic.ObjectListView):
 class PowerPanelListView(generic.ObjectListView):
-    queryset = PowerPanel.objects.annotate(
-        powerfeed_count=Count('powerfeeds')
-    ).order_by(*PowerPanel._meta.ordering)
+    queryset = PowerPanel.objects.prefetch_related(
+        'site', 'rack_group'
+    ).annotate(
+        powerfeed_count=get_subquery(PowerFeed, 'power_panel')
+    )
     filterset = filters.PowerPanelFilterSet
     filterset = filters.PowerPanelFilterSet
     filterset_form = forms.PowerPanelFilterForm
     filterset_form = forms.PowerPanelFilterForm
     table = tables.PowerPanelTable
     table = tables.PowerPanelTable
@@ -2566,8 +2574,8 @@ class PowerPanelBulkDeleteView(generic.BulkDeleteView):
     queryset = PowerPanel.objects.prefetch_related(
     queryset = PowerPanel.objects.prefetch_related(
         'site', 'rack_group'
         'site', 'rack_group'
     ).annotate(
     ).annotate(
-        rack_count=Count('powerfeeds')
-    ).order_by(*PowerPanel._meta.ordering)
+        powerfeed_count=get_subquery(PowerFeed, 'power_panel')
+    )
     filterset = filters.PowerPanelFilterSet
     filterset = filters.PowerPanelFilterSet
     table = tables.PowerPanelTable
     table = tables.PowerPanelTable
 
 

+ 8 - 7
netbox/extras/api/views.py

@@ -1,5 +1,4 @@
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
-from django.db.models import Count
 from django.http import Http404
 from django.http import Http404
 from django_rq.queues import get_connection
 from django_rq.queues import get_connection
 from rest_framework import status
 from rest_framework import status
@@ -12,15 +11,17 @@ from rq import Worker
 
 
 from extras import filters
 from extras import filters
 from extras.choices import JobResultStatusChoices
 from extras.choices import JobResultStatusChoices
-from extras.models import ConfigContext, CustomField, ExportTemplate, ImageAttachment, ObjectChange, JobResult, Tag
+from extras.models import (
+    ConfigContext, ExportTemplate, ImageAttachment, ObjectChange, JobResult, Tag, TaggedItem,
+)
+from extras.models import CustomField
 from extras.reports import get_report, get_reports, run_report
 from extras.reports import get_report, get_reports, run_report
 from extras.scripts import get_script, get_scripts, run_script
 from extras.scripts import get_script, get_scripts, run_script
-from netbox.api.views import ModelViewSet
 from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
 from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
 from netbox.api.metadata import ContentTypeMetadata
 from netbox.api.metadata import ContentTypeMetadata
+from netbox.api.views import ModelViewSet
 from utilities.exceptions import RQWorkerNotRunningException
 from utilities.exceptions import RQWorkerNotRunningException
-from utilities.querysets import RestrictedQuerySet
-from utilities.utils import copy_safe_request
+from utilities.utils import copy_safe_request, get_subquery
 from . import serializers
 from . import serializers
 
 
 
 
@@ -101,8 +102,8 @@ class ExportTemplateViewSet(ModelViewSet):
 
 
 class TagViewSet(ModelViewSet):
 class TagViewSet(ModelViewSet):
     queryset = Tag.objects.annotate(
     queryset = Tag.objects.annotate(
-        tagged_items=Count('extras_taggeditem_items')
-    ).order_by(*Tag._meta.ordering)
+        tagged_items=get_subquery(TaggedItem, 'tag')
+    )
     serializer_class = serializers.TagSerializer
     serializer_class = serializers.TagSerializer
     filterset_class = filters.TagFilterSet
     filterset_class = filters.TagFilterSet
 
 

+ 9 - 9
netbox/extras/views.py

@@ -1,7 +1,7 @@
 from django import template
 from django import template
 from django.contrib import messages
 from django.contrib import messages
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
-from django.db.models import Count, Q
+from django.db.models import Q
 from django.http import Http404, HttpResponseForbidden
 from django.http import Http404, HttpResponseForbidden
 from django.shortcuts import get_object_or_404, redirect, render
 from django.shortcuts import get_object_or_404, redirect, render
 from django.views.generic import View
 from django.views.generic import View
@@ -12,11 +12,11 @@ from rq import Worker
 from netbox.views import generic
 from netbox.views import generic
 from utilities.forms import ConfirmationForm
 from utilities.forms import ConfirmationForm
 from utilities.paginator import EnhancedPaginator, get_paginate_count
 from utilities.paginator import EnhancedPaginator, get_paginate_count
-from utilities.utils import copy_safe_request, shallow_compare_dict
+from utilities.utils import copy_safe_request, get_subquery, shallow_compare_dict
 from utilities.views import ContentTypePermissionRequiredMixin
 from utilities.views import ContentTypePermissionRequiredMixin
 from . import filters, forms, tables
 from . import filters, forms, tables
 from .choices import JobResultStatusChoices
 from .choices import JobResultStatusChoices
-from .models import ConfigContext, ImageAttachment, ObjectChange, JobResult, Tag
+from .models import ConfigContext, ImageAttachment, ObjectChange, JobResult, Tag, TaggedItem
 from .reports import get_report, get_reports, run_report
 from .reports import get_report, get_reports, run_report
 from .scripts import get_scripts, run_script
 from .scripts import get_scripts, run_script
 
 
@@ -27,8 +27,8 @@ from .scripts import get_scripts, run_script
 
 
 class TagListView(generic.ObjectListView):
 class TagListView(generic.ObjectListView):
     queryset = Tag.objects.annotate(
     queryset = Tag.objects.annotate(
-        items=Count('extras_taggeditem_items')
-    ).order_by(*Tag._meta.ordering)
+        items=get_subquery(TaggedItem, 'tag')
+    )
     filterset = filters.TagFilterSet
     filterset = filters.TagFilterSet
     filterset_form = forms.TagFilterForm
     filterset_form = forms.TagFilterForm
     table = tables.TagTable
     table = tables.TagTable
@@ -52,16 +52,16 @@ class TagBulkImportView(generic.BulkImportView):
 
 
 class TagBulkEditView(generic.BulkEditView):
 class TagBulkEditView(generic.BulkEditView):
     queryset = Tag.objects.annotate(
     queryset = Tag.objects.annotate(
-        items=Count('extras_taggeditem_items')
-    ).order_by(*Tag._meta.ordering)
+        items=get_subquery(TaggedItem, 'tag')
+    )
     table = tables.TagTable
     table = tables.TagTable
     form = forms.TagBulkEditForm
     form = forms.TagBulkEditForm
 
 
 
 
 class TagBulkDeleteView(generic.BulkDeleteView):
 class TagBulkDeleteView(generic.BulkDeleteView):
     queryset = Tag.objects.annotate(
     queryset = Tag.objects.annotate(
-        items=Count('extras_taggeditem_items')
-    ).order_by(*Tag._meta.ordering)
+        items=get_subquery(TaggedItem, 'tag')
+    )
     table = tables.TagTable
     table = tables.TagTable
 
 
 
 

+ 9 - 10
netbox/ipam/api/views.py

@@ -1,5 +1,4 @@
 from django.conf import settings
 from django.conf import settings
-from django.db.models import Count
 from django.shortcuts import get_object_or_404
 from django.shortcuts import get_object_or_404
 from django_pglocks import advisory_lock
 from django_pglocks import advisory_lock
 from drf_yasg.utils import swagger_auto_schema
 from drf_yasg.utils import swagger_auto_schema
@@ -35,7 +34,7 @@ class VRFViewSet(CustomFieldModelViewSet):
     ).annotate(
     ).annotate(
         ipaddress_count=get_subquery(IPAddress, 'vrf'),
         ipaddress_count=get_subquery(IPAddress, 'vrf'),
         prefix_count=get_subquery(Prefix, 'vrf')
         prefix_count=get_subquery(Prefix, 'vrf')
-    ).order_by(*VRF._meta.ordering)
+    )
     serializer_class = serializers.VRFSerializer
     serializer_class = serializers.VRFSerializer
     filterset_class = filters.VRFFilterSet
     filterset_class = filters.VRFFilterSet
 
 
@@ -56,8 +55,8 @@ class RouteTargetViewSet(CustomFieldModelViewSet):
 
 
 class RIRViewSet(ModelViewSet):
 class RIRViewSet(ModelViewSet):
     queryset = RIR.objects.annotate(
     queryset = RIR.objects.annotate(
-        aggregate_count=Count('aggregates')
-    ).order_by(*RIR._meta.ordering)
+        aggregate_count=get_subquery(Aggregate, 'rir')
+    )
     serializer_class = serializers.RIRSerializer
     serializer_class = serializers.RIRSerializer
     filterset_class = filters.RIRFilterSet
     filterset_class = filters.RIRFilterSet
 
 
@@ -80,7 +79,7 @@ class RoleViewSet(ModelViewSet):
     queryset = Role.objects.annotate(
     queryset = Role.objects.annotate(
         prefix_count=get_subquery(Prefix, 'role'),
         prefix_count=get_subquery(Prefix, 'role'),
         vlan_count=get_subquery(VLAN, 'role')
         vlan_count=get_subquery(VLAN, 'role')
-    ).order_by(*Role._meta.ordering)
+    )
     serializer_class = serializers.RoleSerializer
     serializer_class = serializers.RoleSerializer
     filterset_class = filters.RoleFilterSet
     filterset_class = filters.RoleFilterSet
 
 
@@ -92,7 +91,7 @@ class RoleViewSet(ModelViewSet):
 class PrefixViewSet(CustomFieldModelViewSet):
 class PrefixViewSet(CustomFieldModelViewSet):
     queryset = Prefix.objects.prefetch_related(
     queryset = Prefix.objects.prefetch_related(
         'site', 'vrf__tenant', 'tenant', 'vlan', 'role', 'tags'
         'site', 'vrf__tenant', 'tenant', 'vlan', 'role', 'tags'
-    ).order_by(*Prefix._meta.ordering)
+    )
     serializer_class = serializers.PrefixSerializer
     serializer_class = serializers.PrefixSerializer
     filterset_class = filters.PrefixFilterSet
     filterset_class = filters.PrefixFilterSet
 
 
@@ -262,7 +261,7 @@ class PrefixViewSet(CustomFieldModelViewSet):
 class IPAddressViewSet(CustomFieldModelViewSet):
 class IPAddressViewSet(CustomFieldModelViewSet):
     queryset = IPAddress.objects.prefetch_related(
     queryset = IPAddress.objects.prefetch_related(
         'vrf__tenant', 'tenant', 'nat_inside', 'nat_outside', 'tags', 'assigned_object'
         'vrf__tenant', 'tenant', 'nat_inside', 'nat_outside', 'tags', 'assigned_object'
-    ).order_by(*IPAddress._meta.ordering)
+    )
     serializer_class = serializers.IPAddressSerializer
     serializer_class = serializers.IPAddressSerializer
     filterset_class = filters.IPAddressFilterSet
     filterset_class = filters.IPAddressFilterSet
 
 
@@ -273,8 +272,8 @@ class IPAddressViewSet(CustomFieldModelViewSet):
 
 
 class VLANGroupViewSet(ModelViewSet):
 class VLANGroupViewSet(ModelViewSet):
     queryset = VLANGroup.objects.prefetch_related('site').annotate(
     queryset = VLANGroup.objects.prefetch_related('site').annotate(
-        vlan_count=Count('vlans')
-    ).order_by(*VLANGroup._meta.ordering)
+        vlan_count=get_subquery(VLAN, 'group')
+    )
     serializer_class = serializers.VLANGroupSerializer
     serializer_class = serializers.VLANGroupSerializer
     filterset_class = filters.VLANGroupFilterSet
     filterset_class = filters.VLANGroupFilterSet
 
 
@@ -288,7 +287,7 @@ class VLANViewSet(CustomFieldModelViewSet):
         'site', 'group', 'tenant', 'role', 'tags'
         'site', 'group', 'tenant', 'role', 'tags'
     ).annotate(
     ).annotate(
         prefix_count=get_subquery(Prefix, 'vlan')
         prefix_count=get_subquery(Prefix, 'vlan')
-    ).order_by(*VLAN._meta.ordering)
+    )
     serializer_class = serializers.VLANSerializer
     serializer_class = serializers.VLANSerializer
     filterset_class = filters.VLANFilterSet
     filterset_class = filters.VLANFilterSet
 
 

+ 13 - 9
netbox/ipam/views.py

@@ -1,4 +1,4 @@
-from django.db.models import Count, Prefetch
+from django.db.models import Prefetch
 from django.db.models.expressions import RawSQL
 from django.db.models.expressions import RawSQL
 from django.shortcuts import get_object_or_404, redirect, render
 from django.shortcuts import get_object_or_404, redirect, render
 from django_tables2 import RequestConfig
 from django_tables2 import RequestConfig
@@ -139,7 +139,9 @@ class RouteTargetBulkDeleteView(generic.BulkDeleteView):
 #
 #
 
 
 class RIRListView(generic.ObjectListView):
 class RIRListView(generic.ObjectListView):
-    queryset = RIR.objects.annotate(aggregate_count=Count('aggregates')).order_by(*RIR._meta.ordering)
+    queryset = RIR.objects.annotate(
+        aggregate_count=get_subquery(Aggregate, 'rir')
+    )
     filterset = filters.RIRFilterSet
     filterset = filters.RIRFilterSet
     filterset_form = forms.RIRFilterForm
     filterset_form = forms.RIRFilterForm
     table = tables.RIRTable
     table = tables.RIRTable
@@ -162,7 +164,9 @@ class RIRBulkImportView(generic.BulkImportView):
 
 
 
 
 class RIRBulkDeleteView(generic.BulkDeleteView):
 class RIRBulkDeleteView(generic.BulkDeleteView):
-    queryset = RIR.objects.annotate(aggregate_count=Count('aggregates')).order_by(*RIR._meta.ordering)
+    queryset = RIR.objects.annotate(
+        aggregate_count=get_subquery(Aggregate, 'rir')
+    )
     filterset = filters.RIRFilterSet
     filterset = filters.RIRFilterSet
     table = tables.RIRTable
     table = tables.RIRTable
 
 
@@ -174,7 +178,7 @@ class RIRBulkDeleteView(generic.BulkDeleteView):
 class AggregateListView(generic.ObjectListView):
 class AggregateListView(generic.ObjectListView):
     queryset = Aggregate.objects.annotate(
     queryset = Aggregate.objects.annotate(
         child_count=RawSQL('SELECT COUNT(*) FROM ipam_prefix WHERE ipam_prefix.prefix <<= ipam_aggregate.prefix', ())
         child_count=RawSQL('SELECT COUNT(*) FROM ipam_prefix WHERE ipam_prefix.prefix <<= ipam_aggregate.prefix', ())
-    ).order_by(*Aggregate._meta.ordering)
+    )
     filterset = filters.AggregateFilterSet
     filterset = filters.AggregateFilterSet
     filterset_form = forms.AggregateFilterForm
     filterset_form = forms.AggregateFilterForm
     table = tables.AggregateDetailTable
     table = tables.AggregateDetailTable
@@ -628,9 +632,9 @@ class IPAddressBulkDeleteView(generic.BulkDeleteView):
 #
 #
 
 
 class VLANGroupListView(generic.ObjectListView):
 class VLANGroupListView(generic.ObjectListView):
-    queryset = VLANGroup.objects.annotate(
-        vlan_count=Count('vlans')
-    ).order_by(*VLANGroup._meta.ordering)
+    queryset = VLANGroup.objects.prefetch_related('site').annotate(
+        vlan_count=get_subquery(VLAN, 'group')
+    )
     filterset = filters.VLANGroupFilterSet
     filterset = filters.VLANGroupFilterSet
     filterset_form = forms.VLANGroupFilterForm
     filterset_form = forms.VLANGroupFilterForm
     table = tables.VLANGroupTable
     table = tables.VLANGroupTable
@@ -653,8 +657,8 @@ class VLANGroupBulkImportView(generic.BulkImportView):
 
 
 class VLANGroupBulkDeleteView(generic.BulkDeleteView):
 class VLANGroupBulkDeleteView(generic.BulkDeleteView):
     queryset = VLANGroup.objects.prefetch_related('site').annotate(
     queryset = VLANGroup.objects.prefetch_related('site').annotate(
-        vlan_count=Count('vlans')
-    ).order_by(*VLANGroup._meta.ordering)
+        vlan_count=get_subquery(VLAN, 'group')
+    )
     filterset = filters.VLANGroupFilterSet
     filterset = filters.VLANGroupFilterSet
     table = tables.VLANGroupTable
     table = tables.VLANGroupTable
 
 

+ 13 - 9
netbox/netbox/constants.py

@@ -33,8 +33,8 @@ SEARCH_TYPES = OrderedDict((
     # Circuits
     # Circuits
     ('provider', {
     ('provider', {
         'queryset': Provider.objects.annotate(
         'queryset': Provider.objects.annotate(
-            count_circuits=Count('circuits')
-        ).order_by(*Provider._meta.ordering),
+            count_circuits=get_subquery(Circuit, 'provider')
+        ),
         'filterset': ProviderFilterSet,
         'filterset': ProviderFilterSet,
         'table': ProviderTable,
         'table': ProviderTable,
         'url': 'circuits:provider_list',
         'url': 'circuits:provider_list',
@@ -61,17 +61,21 @@ SEARCH_TYPES = OrderedDict((
         'url': 'dcim:rack_list',
         'url': 'dcim:rack_list',
     }),
     }),
     ('rackgroup', {
     ('rackgroup', {
-        'queryset': RackGroup.objects.prefetch_related('site').annotate(
-            rack_count=Count('racks')
-        ).order_by(*RackGroup._meta.ordering),
+        'queryset': RackGroup.objects.add_related_count(
+            RackGroup.objects.all(),
+            Rack,
+            'group',
+            'rack_count',
+            cumulative=True
+        ).prefetch_related('site'),
         'filterset': RackGroupFilterSet,
         'filterset': RackGroupFilterSet,
         'table': RackGroupTable,
         'table': RackGroupTable,
         'url': 'dcim:rackgroup_list',
         'url': 'dcim:rackgroup_list',
     }),
     }),
     ('devicetype', {
     ('devicetype', {
         'queryset': DeviceType.objects.prefetch_related('manufacturer').annotate(
         'queryset': DeviceType.objects.prefetch_related('manufacturer').annotate(
-            instance_count=Count('instances')
-        ).order_by(*DeviceType._meta.ordering),
+            instance_count=get_subquery(Device, 'device_type')
+        ),
         'filterset': DeviceTypeFilterSet,
         'filterset': DeviceTypeFilterSet,
         'table': DeviceTypeTable,
         'table': DeviceTypeTable,
         'url': 'dcim:devicetype_list',
         'url': 'dcim:devicetype_list',
@@ -86,8 +90,8 @@ SEARCH_TYPES = OrderedDict((
     }),
     }),
     ('virtualchassis', {
     ('virtualchassis', {
         'queryset': VirtualChassis.objects.prefetch_related('master').annotate(
         'queryset': VirtualChassis.objects.prefetch_related('master').annotate(
-            member_count=Count('members', distinct=True)
-        ).order_by(*VirtualChassis._meta.ordering),
+            member_count=get_subquery(Device, 'virtual_chassis')
+        ),
         'filterset': VirtualChassisFilterSet,
         'filterset': VirtualChassisFilterSet,
         'table': VirtualChassisTable,
         'table': VirtualChassisTable,
         'url': 'dcim:virtualchassis_list',
         'url': 'dcim:virtualchassis_list',

+ 3 - 3
netbox/secrets/api/views.py

@@ -1,7 +1,6 @@
 import base64
 import base64
 
 
 from Crypto.PublicKey import RSA
 from Crypto.PublicKey import RSA
-from django.db.models import Count
 from django.http import HttpResponseBadRequest
 from django.http import HttpResponseBadRequest
 from rest_framework.exceptions import ValidationError
 from rest_framework.exceptions import ValidationError
 from rest_framework.permissions import IsAuthenticated
 from rest_framework.permissions import IsAuthenticated
@@ -13,6 +12,7 @@ from netbox.api.views import ModelViewSet
 from secrets import filters
 from secrets import filters
 from secrets.exceptions import InvalidKey
 from secrets.exceptions import InvalidKey
 from secrets.models import Secret, SecretRole, SessionKey, UserKey
 from secrets.models import Secret, SecretRole, SessionKey, UserKey
+from utilities.utils import get_subquery
 from . import serializers
 from . import serializers
 
 
 ERR_USERKEY_MISSING = "No UserKey found for the current user."
 ERR_USERKEY_MISSING = "No UserKey found for the current user."
@@ -35,8 +35,8 @@ class SecretsRootView(APIRootView):
 
 
 class SecretRoleViewSet(ModelViewSet):
 class SecretRoleViewSet(ModelViewSet):
     queryset = SecretRole.objects.annotate(
     queryset = SecretRole.objects.annotate(
-        secret_count=Count('secrets')
-    ).order_by(*SecretRole._meta.ordering)
+        secret_count=get_subquery(Secret, 'role')
+    )
     serializer_class = serializers.SecretRoleSerializer
     serializer_class = serializers.SecretRoleSerializer
     filterset_class = filters.SecretRoleFilterSet
     filterset_class = filters.SecretRoleFilterSet
 
 

+ 7 - 3
netbox/secrets/views.py

@@ -2,12 +2,12 @@ import base64
 import logging
 import logging
 
 
 from django.contrib import messages
 from django.contrib import messages
-from django.db.models import Count
 from django.shortcuts import get_object_or_404, redirect, render
 from django.shortcuts import get_object_or_404, redirect, render
 from django.utils.html import escape
 from django.utils.html import escape
 from django.utils.safestring import mark_safe
 from django.utils.safestring import mark_safe
 
 
 from netbox.views import generic
 from netbox.views import generic
+from utilities.utils import get_subquery
 from . import filters, forms, tables
 from . import filters, forms, tables
 from .models import SecretRole, Secret, SessionKey, UserKey
 from .models import SecretRole, Secret, SessionKey, UserKey
 
 
@@ -27,7 +27,9 @@ def get_session_key(request):
 #
 #
 
 
 class SecretRoleListView(generic.ObjectListView):
 class SecretRoleListView(generic.ObjectListView):
-    queryset = SecretRole.objects.annotate(secret_count=Count('secrets')).order_by(*SecretRole._meta.ordering)
+    queryset = SecretRole.objects.annotate(
+        secret_count=get_subquery(Secret, 'role')
+    )
     table = tables.SecretRoleTable
     table = tables.SecretRoleTable
 
 
 
 
@@ -47,7 +49,9 @@ class SecretRoleBulkImportView(generic.BulkImportView):
 
 
 
 
 class SecretRoleBulkDeleteView(generic.BulkDeleteView):
 class SecretRoleBulkDeleteView(generic.BulkDeleteView):
-    queryset = SecretRole.objects.annotate(secret_count=Count('secrets')).order_by(*SecretRole._meta.ordering)
+    queryset = SecretRole.objects.annotate(
+        secret_count=get_subquery(Secret, 'role')
+    )
     table = tables.SecretRoleTable
     table = tables.SecretRoleTable
 
 
 
 

+ 16 - 2
netbox/users/api/serializers.py

@@ -19,9 +19,23 @@ class UserSerializer(ValidatedModelSerializer):
     class Meta:
     class Meta:
         model = User
         model = User
         fields = (
         fields = (
-            'id', 'url', 'username', 'first_name', 'last_name', 'email', 'is_staff', 'is_active', 'date_joined',
-            'groups',
+            'id', 'url', 'username', 'password', 'first_name', 'last_name', 'email', 'is_staff', 'is_active',
+            'date_joined', 'groups',
         )
         )
+        extra_kwargs = {
+            'password': {'write_only': True}
+        }
+
+    def create(self, validated_data):
+        """
+        Extract the password from validated data and set it separately to ensure proper hash generation.
+        """
+        password = validated_data.pop('password')
+        user = super().create(validated_data)
+        user.set_password(password)
+        user.save()
+
+        return user
 
 
 
 
 class GroupSerializer(ValidatedModelSerializer):
 class GroupSerializer(ValidatedModelSerializer):

+ 4 - 0
netbox/users/tests/test_api.py

@@ -21,15 +21,19 @@ class UserTest(APIViewTestCases.APIViewTestCase):
     model = User
     model = User
     view_namespace = 'users'
     view_namespace = 'users'
     brief_fields = ['id', 'url', 'username']
     brief_fields = ['id', 'url', 'username']
+    validation_excluded_fields = ['password']
     create_data = [
     create_data = [
         {
         {
             'username': 'User_4',
             'username': 'User_4',
+            'password': 'password4',
         },
         },
         {
         {
             'username': 'User_5',
             'username': 'User_5',
+            'password': 'password5',
         },
         },
         {
         {
             'username': 'User_6',
             'username': 'User_6',
+            'password': 'password6',
         },
         },
     ]
     ]
 
 

+ 10 - 1
netbox/utilities/testing/api.py

@@ -174,6 +174,7 @@ class APIViewTestCases:
 
 
     class CreateObjectViewTestCase(APITestCase):
     class CreateObjectViewTestCase(APITestCase):
         create_data = []
         create_data = []
+        validation_excluded_fields = []
 
 
         def test_create_object_without_permission(self):
         def test_create_object_without_permission(self):
             """
             """
@@ -205,6 +206,7 @@ class APIViewTestCases:
             self.assertInstanceEqual(
             self.assertInstanceEqual(
                 self._get_queryset().get(pk=response.data['id']),
                 self._get_queryset().get(pk=response.data['id']),
                 self.create_data[0],
                 self.create_data[0],
+                exclude=self.validation_excluded_fields,
                 api=True
                 api=True
             )
             )
 
 
@@ -232,12 +234,14 @@ class APIViewTestCases:
                 self.assertInstanceEqual(
                 self.assertInstanceEqual(
                     self._get_queryset().get(pk=obj['id']),
                     self._get_queryset().get(pk=obj['id']),
                     self.create_data[i],
                     self.create_data[i],
+                    exclude=self.validation_excluded_fields,
                     api=True
                     api=True
                 )
                 )
 
 
     class UpdateObjectViewTestCase(APITestCase):
     class UpdateObjectViewTestCase(APITestCase):
         update_data = {}
         update_data = {}
         bulk_update_data = None
         bulk_update_data = None
+        validation_excluded_fields = []
 
 
         def test_update_object_without_permission(self):
         def test_update_object_without_permission(self):
             """
             """
@@ -270,7 +274,12 @@ class APIViewTestCases:
             response = self.client.patch(url, update_data, format='json', **self.header)
             response = self.client.patch(url, update_data, format='json', **self.header)
             self.assertHttpStatus(response, status.HTTP_200_OK)
             self.assertHttpStatus(response, status.HTTP_200_OK)
             instance.refresh_from_db()
             instance.refresh_from_db()
-            self.assertInstanceEqual(instance, update_data, api=True)
+            self.assertInstanceEqual(
+                instance,
+                update_data,
+                exclude=self.validation_excluded_fields,
+                api=True
+            )
 
 
         def test_bulk_update_objects(self):
         def test_bulk_update_objects(self):
             """
             """

+ 12 - 7
netbox/utilities/testing/views.py

@@ -126,20 +126,25 @@ class TestCase(_TestCase):
             err_message = f"Expected HTTP status {expected_status}; received {response.status_code}: {err}"
             err_message = f"Expected HTTP status {expected_status}; received {response.status_code}: {err}"
         self.assertEqual(response.status_code, expected_status, err_message)
         self.assertEqual(response.status_code, expected_status, err_message)
 
 
-    def assertInstanceEqual(self, instance, data, api=False):
+    def assertInstanceEqual(self, instance, data, exclude=None, api=False):
         """
         """
         Compare a model instance to a dictionary, checking that its attribute values match those specified
         Compare a model instance to a dictionary, checking that its attribute values match those specified
         in the dictionary.
         in the dictionary.
 
 
-        :instance: Python object instance
-        :data: Dictionary of test data used to define the instance
-        :api: Set to True is the data is a JSON representation of the instance
+        :param instance: Python object instance
+        :param data: Dictionary of test data used to define the instance
+        :param exclude: List of fields to exclude from comparison (e.g. passwords, which get hashed)
+        :param api: Set to True is the data is a JSON representation of the instance
         """
         """
-        model_dict = self.model_to_dict(instance, fields=data.keys(), api=api)
+        if exclude is None:
+            exclude = []
 
 
-        # Omit any dictionary keys which are not instance attributes
+        fields = [k for k in data.keys() if k not in exclude]
+        model_dict = self.model_to_dict(instance, fields=fields, api=api)
+
+        # Omit any dictionary keys which are not instance attributes or have been excluded
         relevant_data = {
         relevant_data = {
-            k: v for k, v in data.items() if hasattr(instance, k)
+            k: v for k, v in data.items() if hasattr(instance, k) and k not in exclude
         }
         }
 
 
         self.assertDictEqual(model_dict, relevant_data)
         self.assertDictEqual(model_dict, relevant_data)

+ 5 - 6
netbox/virtualization/api/views.py

@@ -1,4 +1,3 @@
-from django.db.models import Count
 from rest_framework.routers import APIRootView
 from rest_framework.routers import APIRootView
 
 
 from dcim.models import Device
 from dcim.models import Device
@@ -23,16 +22,16 @@ class VirtualizationRootView(APIRootView):
 
 
 class ClusterTypeViewSet(ModelViewSet):
 class ClusterTypeViewSet(ModelViewSet):
     queryset = ClusterType.objects.annotate(
     queryset = ClusterType.objects.annotate(
-        cluster_count=Count('clusters')
-    ).order_by(*ClusterType._meta.ordering)
+        cluster_count=get_subquery(Cluster, 'type')
+    )
     serializer_class = serializers.ClusterTypeSerializer
     serializer_class = serializers.ClusterTypeSerializer
     filterset_class = filters.ClusterTypeFilterSet
     filterset_class = filters.ClusterTypeFilterSet
 
 
 
 
 class ClusterGroupViewSet(ModelViewSet):
 class ClusterGroupViewSet(ModelViewSet):
     queryset = ClusterGroup.objects.annotate(
     queryset = ClusterGroup.objects.annotate(
-        cluster_count=Count('clusters')
-    ).order_by(*ClusterGroup._meta.ordering)
+        cluster_count=get_subquery(Cluster, 'group')
+    )
     serializer_class = serializers.ClusterGroupSerializer
     serializer_class = serializers.ClusterGroupSerializer
     filterset_class = filters.ClusterGroupFilterSet
     filterset_class = filters.ClusterGroupFilterSet
 
 
@@ -43,7 +42,7 @@ class ClusterViewSet(CustomFieldModelViewSet):
     ).annotate(
     ).annotate(
         device_count=get_subquery(Device, 'cluster'),
         device_count=get_subquery(Device, 'cluster'),
         virtualmachine_count=get_subquery(VirtualMachine, 'cluster')
         virtualmachine_count=get_subquery(VirtualMachine, 'cluster')
-    ).order_by(*Cluster._meta.ordering)
+    )
     serializer_class = serializers.ClusterSerializer
     serializer_class = serializers.ClusterSerializer
     filterset_class = filters.ClusterFilterSet
     filterset_class = filters.ClusterFilterSet
 
 

+ 13 - 5
netbox/virtualization/views.py

@@ -1,6 +1,6 @@
 from django.contrib import messages
 from django.contrib import messages
 from django.db import transaction
 from django.db import transaction
-from django.db.models import Count, Prefetch
+from django.db.models import Prefetch
 from django.shortcuts import get_object_or_404, redirect, render
 from django.shortcuts import get_object_or_404, redirect, render
 from django.urls import reverse
 from django.urls import reverse
 
 
@@ -21,7 +21,9 @@ from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterf
 #
 #
 
 
 class ClusterTypeListView(generic.ObjectListView):
 class ClusterTypeListView(generic.ObjectListView):
-    queryset = ClusterType.objects.annotate(cluster_count=Count('clusters')).order_by(*ClusterType._meta.ordering)
+    queryset = ClusterType.objects.annotate(
+        cluster_count=get_subquery(Cluster, 'type')
+    )
     table = tables.ClusterTypeTable
     table = tables.ClusterTypeTable
 
 
 
 
@@ -41,7 +43,9 @@ class ClusterTypeBulkImportView(generic.BulkImportView):
 
 
 
 
 class ClusterTypeBulkDeleteView(generic.BulkDeleteView):
 class ClusterTypeBulkDeleteView(generic.BulkDeleteView):
-    queryset = ClusterType.objects.annotate(cluster_count=Count('clusters')).order_by(*ClusterType._meta.ordering)
+    queryset = ClusterType.objects.annotate(
+        cluster_count=get_subquery(Cluster, 'type')
+    )
     table = tables.ClusterTypeTable
     table = tables.ClusterTypeTable
 
 
 
 
@@ -50,7 +54,9 @@ class ClusterTypeBulkDeleteView(generic.BulkDeleteView):
 #
 #
 
 
 class ClusterGroupListView(generic.ObjectListView):
 class ClusterGroupListView(generic.ObjectListView):
-    queryset = ClusterGroup.objects.annotate(cluster_count=Count('clusters')).order_by(*ClusterGroup._meta.ordering)
+    queryset = ClusterGroup.objects.annotate(
+        cluster_count=get_subquery(Cluster, 'group')
+    )
     table = tables.ClusterGroupTable
     table = tables.ClusterGroupTable
 
 
 
 
@@ -70,7 +76,9 @@ class ClusterGroupBulkImportView(generic.BulkImportView):
 
 
 
 
 class ClusterGroupBulkDeleteView(generic.BulkDeleteView):
 class ClusterGroupBulkDeleteView(generic.BulkDeleteView):
-    queryset = ClusterGroup.objects.annotate(cluster_count=Count('clusters')).order_by(*ClusterGroup._meta.ordering)
+    queryset = ClusterGroup.objects.annotate(
+        cluster_count=get_subquery(Cluster, 'group')
+    )
     table = tables.ClusterGroupTable
     table = tables.ClusterGroupTable