فهرست منبع

Merge branch 'develop' into develop-2.10

Jeremy Stretch 5 سال پیش
والد
کامیت
915cf3e715

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

@@ -1,5 +1,13 @@
 # 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)
 
 ### 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 circuits import filters
@@ -6,6 +6,7 @@ from circuits.models import Provider, CircuitTermination, CircuitType, Circuit
 from dcim.api.views import PathEndpointMixin
 from extras.api.views import CustomFieldModelViewSet
 from netbox.api.views import ModelViewSet
+from utilities.utils import get_subquery
 from . import serializers
 
 
@@ -23,8 +24,8 @@ class CircuitsRootView(APIRootView):
 
 class ProviderViewSet(CustomFieldModelViewSet):
     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
     filterset_class = filters.ProviderFilterSet
 
@@ -35,8 +36,8 @@ class ProviderViewSet(CustomFieldModelViewSet):
 
 class CircuitTypeViewSet(ModelViewSet):
     queryset = CircuitType.objects.annotate(
-        circuit_count=Count('circuits')
-    ).order_by(*CircuitType._meta.ordering)
+        circuit_count=get_subquery(Circuit, 'type')
+    )
     serializer_class = serializers.CircuitTypeSerializer
     filterset_class = filters.CircuitTypeFilterSet
 

+ 16 - 6
netbox/circuits/views.py

@@ -1,12 +1,12 @@
 from django.contrib import messages
 from django.db import transaction
-from django.db.models import Count
 from django.shortcuts import get_object_or_404, redirect, render
 from django_tables2 import RequestConfig
 
 from netbox.views import generic
 from utilities.forms import ConfirmationForm
 from utilities.paginator import EnhancedPaginator, get_paginate_count
+from utilities.utils import get_subquery
 from . import filters, forms, tables
 from .choices import CircuitTerminationSideChoices
 from .models import Circuit, CircuitTermination, CircuitType, Provider
@@ -17,7 +17,9 @@ from .models import Circuit, CircuitTermination, CircuitType, Provider
 #
 
 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_form = forms.ProviderFilterForm
     table = tables.ProviderTable
@@ -64,14 +66,18 @@ class ProviderBulkImportView(generic.BulkImportView):
 
 
 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
     table = tables.ProviderTable
     form = forms.ProviderBulkEditForm
 
 
 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
     table = tables.ProviderTable
 
@@ -81,7 +87,9 @@ class ProviderBulkDeleteView(generic.BulkDeleteView):
 #
 
 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
 
 
@@ -101,7 +109,9 @@ class CircuitTypeBulkImportView(generic.BulkImportView):
 
 
 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
 
 

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

@@ -2,7 +2,7 @@ import socket
 from collections import OrderedDict
 
 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.shortcuts import get_object_or_404
 from drf_yasg import openapi
@@ -125,7 +125,7 @@ class SiteViewSet(CustomFieldModelViewSet):
         vlan_count=get_subquery(VLAN, 'site'),
         circuit_count=get_subquery(Circuit, 'terminations__site'),
         virtualmachine_count=get_subquery(VirtualMachine, 'cluster__site'),
-    ).order_by(*Site._meta.ordering)
+    )
     serializer_class = serializers.SiteSerializer
     filterset_class = filters.SiteFilterSet
 
@@ -152,8 +152,8 @@ class RackGroupViewSet(ModelViewSet):
 
 class RackRoleViewSet(ModelViewSet):
     queryset = RackRole.objects.annotate(
-        rack_count=Count('racks')
-    ).order_by(*RackRole._meta.ordering)
+        rack_count=get_subquery(Rack, 'role')
+    )
     serializer_class = serializers.RackRoleSerializer
     filterset_class = filters.RackRoleFilterSet
 
@@ -168,7 +168,7 @@ class RackViewSet(CustomFieldModelViewSet):
     ).annotate(
         device_count=get_subquery(Device, 'rack'),
         powerfeed_count=get_subquery(PowerFeed, 'rack')
-    ).order_by(*Rack._meta.ordering)
+    )
     serializer_class = serializers.RackSerializer
     filterset_class = filters.RackFilterSet
 
@@ -243,7 +243,7 @@ class ManufacturerViewSet(ModelViewSet):
         devicetype_count=get_subquery(DeviceType, 'manufacturer'),
         inventoryitem_count=get_subquery(InventoryItem, 'manufacturer'),
         platform_count=get_subquery(Platform, 'manufacturer')
-    ).order_by(*Manufacturer._meta.ordering)
+    )
     serializer_class = serializers.ManufacturerSerializer
     filterset_class = filters.ManufacturerFilterSet
 
@@ -254,8 +254,8 @@ class ManufacturerViewSet(ModelViewSet):
 
 class DeviceTypeViewSet(CustomFieldModelViewSet):
     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
     filterset_class = filters.DeviceTypeFilterSet
 
@@ -320,7 +320,7 @@ class DeviceRoleViewSet(ModelViewSet):
     queryset = DeviceRole.objects.annotate(
         device_count=get_subquery(Device, 'device_role'),
         virtualmachine_count=get_subquery(VirtualMachine, 'role')
-    ).order_by(*DeviceRole._meta.ordering)
+    )
     serializer_class = serializers.DeviceRoleSerializer
     filterset_class = filters.DeviceRoleFilterSet
 
@@ -333,7 +333,7 @@ class PlatformViewSet(ModelViewSet):
     queryset = Platform.objects.annotate(
         device_count=get_subquery(Device, 'platform'),
         virtualmachine_count=get_subquery(VirtualMachine, 'platform')
-    ).order_by(*Platform._meta.ordering)
+    )
     serializer_class = serializers.PlatformSerializer
     filterset_class = filters.PlatformFilterSet
 
@@ -596,8 +596,8 @@ class CableViewSet(ModelViewSet):
 
 class VirtualChassisViewSet(ModelViewSet):
     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
     filterset_class = filters.VirtualChassisFilterSet
 
@@ -610,8 +610,8 @@ class PowerPanelViewSet(ModelViewSet):
     queryset = PowerPanel.objects.prefetch_related(
         'site', 'rack_group'
     ).annotate(
-        powerfeed_count=Count('powerfeeds')
-    ).order_by(*PowerPanel._meta.ordering)
+        powerfeed_count=get_subquery(PowerFeed, 'power_panel')
+    )
     serializer_class = serializers.PowerPanelSerializer
     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.core.paginator import EmptyPage, PageNotAnInteger
 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.shortcuts import get_object_or_404, redirect, render
 from django.utils.html import escape
@@ -253,7 +253,9 @@ class RackGroupBulkDeleteView(generic.BulkDeleteView):
 #
 
 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
 
 
@@ -273,7 +275,9 @@ class RackRoleBulkImportView(generic.BulkImportView):
 
 
 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
 
 
@@ -282,9 +286,11 @@ class RackRoleBulkDeleteView(generic.BulkDeleteView):
 #
 
 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_form = forms.RackFilterForm
     table = tables.RackDetailTable
@@ -488,8 +494,8 @@ class ManufacturerBulkImportView(generic.BulkImportView):
 
 class ManufacturerBulkDeleteView(generic.BulkDeleteView):
     queryset = Manufacturer.objects.annotate(
-        devicetype_count=Count('device_types')
-    ).order_by(*Manufacturer._meta.ordering)
+        devicetype_count=get_subquery(DeviceType, 'manufacturer')
+    )
     table = tables.ManufacturerTable
 
 
@@ -498,9 +504,9 @@ class ManufacturerBulkDeleteView(generic.BulkDeleteView):
 #
 
 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_form = forms.DeviceTypeFilterForm
     table = tables.DeviceTypeTable
@@ -606,8 +612,8 @@ class DeviceTypeImportView(generic.ObjectImportView):
 
 class DeviceTypeBulkEditView(generic.BulkEditView):
     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
     table = tables.DeviceTypeTable
     form = forms.DeviceTypeBulkEditForm
@@ -615,8 +621,8 @@ class DeviceTypeBulkEditView(generic.BulkEditView):
 
 class DeviceTypeBulkDeleteView(generic.BulkDeleteView):
     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
     table = tables.DeviceTypeTable
 
@@ -2287,9 +2293,9 @@ class InterfaceConnectionsListView(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
     filterset = filters.VirtualChassisFilterSet
     filterset_form = forms.VirtualChassisFilterForm
@@ -2515,9 +2521,11 @@ class VirtualChassisBulkDeleteView(generic.BulkDeleteView):
 #
 
 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_form = forms.PowerPanelFilterForm
     table = tables.PowerPanelTable
@@ -2566,8 +2574,8 @@ class PowerPanelBulkDeleteView(generic.BulkDeleteView):
     queryset = PowerPanel.objects.prefetch_related(
         'site', 'rack_group'
     ).annotate(
-        rack_count=Count('powerfeeds')
-    ).order_by(*PowerPanel._meta.ordering)
+        powerfeed_count=get_subquery(PowerFeed, 'power_panel')
+    )
     filterset = filters.PowerPanelFilterSet
     table = tables.PowerPanelTable
 

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

@@ -1,5 +1,4 @@
 from django.contrib.contenttypes.models import ContentType
-from django.db.models import Count
 from django.http import Http404
 from django_rq.queues import get_connection
 from rest_framework import status
@@ -12,15 +11,17 @@ from rq import Worker
 
 from extras import filters
 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.scripts import get_script, get_scripts, run_script
-from netbox.api.views import ModelViewSet
 from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
 from netbox.api.metadata import ContentTypeMetadata
+from netbox.api.views import ModelViewSet
 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
 
 
@@ -101,8 +102,8 @@ class ExportTemplateViewSet(ModelViewSet):
 
 class TagViewSet(ModelViewSet):
     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
     filterset_class = filters.TagFilterSet
 

+ 9 - 9
netbox/extras/views.py

@@ -1,7 +1,7 @@
 from django import template
 from django.contrib import messages
 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.shortcuts import get_object_or_404, redirect, render
 from django.views.generic import View
@@ -12,11 +12,11 @@ from rq import Worker
 from netbox.views import generic
 from utilities.forms import ConfirmationForm
 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 . import filters, forms, tables
 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 .scripts import get_scripts, run_script
 
@@ -27,8 +27,8 @@ from .scripts import get_scripts, run_script
 
 class TagListView(generic.ObjectListView):
     queryset = Tag.objects.annotate(
-        items=Count('extras_taggeditem_items')
-    ).order_by(*Tag._meta.ordering)
+        items=get_subquery(TaggedItem, 'tag')
+    )
     filterset = filters.TagFilterSet
     filterset_form = forms.TagFilterForm
     table = tables.TagTable
@@ -52,16 +52,16 @@ class TagBulkImportView(generic.BulkImportView):
 
 class TagBulkEditView(generic.BulkEditView):
     queryset = Tag.objects.annotate(
-        items=Count('extras_taggeditem_items')
-    ).order_by(*Tag._meta.ordering)
+        items=get_subquery(TaggedItem, 'tag')
+    )
     table = tables.TagTable
     form = forms.TagBulkEditForm
 
 
 class TagBulkDeleteView(generic.BulkDeleteView):
     queryset = Tag.objects.annotate(
-        items=Count('extras_taggeditem_items')
-    ).order_by(*Tag._meta.ordering)
+        items=get_subquery(TaggedItem, 'tag')
+    )
     table = tables.TagTable
 
 

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

@@ -1,5 +1,4 @@
 from django.conf import settings
-from django.db.models import Count
 from django.shortcuts import get_object_or_404
 from django_pglocks import advisory_lock
 from drf_yasg.utils import swagger_auto_schema
@@ -35,7 +34,7 @@ class VRFViewSet(CustomFieldModelViewSet):
     ).annotate(
         ipaddress_count=get_subquery(IPAddress, 'vrf'),
         prefix_count=get_subquery(Prefix, 'vrf')
-    ).order_by(*VRF._meta.ordering)
+    )
     serializer_class = serializers.VRFSerializer
     filterset_class = filters.VRFFilterSet
 
@@ -56,8 +55,8 @@ class RouteTargetViewSet(CustomFieldModelViewSet):
 
 class RIRViewSet(ModelViewSet):
     queryset = RIR.objects.annotate(
-        aggregate_count=Count('aggregates')
-    ).order_by(*RIR._meta.ordering)
+        aggregate_count=get_subquery(Aggregate, 'rir')
+    )
     serializer_class = serializers.RIRSerializer
     filterset_class = filters.RIRFilterSet
 
@@ -80,7 +79,7 @@ class RoleViewSet(ModelViewSet):
     queryset = Role.objects.annotate(
         prefix_count=get_subquery(Prefix, 'role'),
         vlan_count=get_subquery(VLAN, 'role')
-    ).order_by(*Role._meta.ordering)
+    )
     serializer_class = serializers.RoleSerializer
     filterset_class = filters.RoleFilterSet
 
@@ -92,7 +91,7 @@ class RoleViewSet(ModelViewSet):
 class PrefixViewSet(CustomFieldModelViewSet):
     queryset = Prefix.objects.prefetch_related(
         'site', 'vrf__tenant', 'tenant', 'vlan', 'role', 'tags'
-    ).order_by(*Prefix._meta.ordering)
+    )
     serializer_class = serializers.PrefixSerializer
     filterset_class = filters.PrefixFilterSet
 
@@ -262,7 +261,7 @@ class PrefixViewSet(CustomFieldModelViewSet):
 class IPAddressViewSet(CustomFieldModelViewSet):
     queryset = IPAddress.objects.prefetch_related(
         'vrf__tenant', 'tenant', 'nat_inside', 'nat_outside', 'tags', 'assigned_object'
-    ).order_by(*IPAddress._meta.ordering)
+    )
     serializer_class = serializers.IPAddressSerializer
     filterset_class = filters.IPAddressFilterSet
 
@@ -273,8 +272,8 @@ class IPAddressViewSet(CustomFieldModelViewSet):
 
 class VLANGroupViewSet(ModelViewSet):
     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
     filterset_class = filters.VLANGroupFilterSet
 
@@ -288,7 +287,7 @@ class VLANViewSet(CustomFieldModelViewSet):
         'site', 'group', 'tenant', 'role', 'tags'
     ).annotate(
         prefix_count=get_subquery(Prefix, 'vlan')
-    ).order_by(*VLAN._meta.ordering)
+    )
     serializer_class = serializers.VLANSerializer
     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.shortcuts import get_object_or_404, redirect, render
 from django_tables2 import RequestConfig
@@ -139,7 +139,9 @@ class RouteTargetBulkDeleteView(generic.BulkDeleteView):
 #
 
 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_form = forms.RIRFilterForm
     table = tables.RIRTable
@@ -162,7 +164,9 @@ class RIRBulkImportView(generic.BulkImportView):
 
 
 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
     table = tables.RIRTable
 
@@ -174,7 +178,7 @@ class RIRBulkDeleteView(generic.BulkDeleteView):
 class AggregateListView(generic.ObjectListView):
     queryset = Aggregate.objects.annotate(
         child_count=RawSQL('SELECT COUNT(*) FROM ipam_prefix WHERE ipam_prefix.prefix <<= ipam_aggregate.prefix', ())
-    ).order_by(*Aggregate._meta.ordering)
+    )
     filterset = filters.AggregateFilterSet
     filterset_form = forms.AggregateFilterForm
     table = tables.AggregateDetailTable
@@ -628,9 +632,9 @@ class IPAddressBulkDeleteView(generic.BulkDeleteView):
 #
 
 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_form = forms.VLANGroupFilterForm
     table = tables.VLANGroupTable
@@ -653,8 +657,8 @@ class VLANGroupBulkImportView(generic.BulkImportView):
 
 class VLANGroupBulkDeleteView(generic.BulkDeleteView):
     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
     table = tables.VLANGroupTable
 

+ 13 - 9
netbox/netbox/constants.py

@@ -33,8 +33,8 @@ SEARCH_TYPES = OrderedDict((
     # Circuits
     ('provider', {
         'queryset': Provider.objects.annotate(
-            count_circuits=Count('circuits')
-        ).order_by(*Provider._meta.ordering),
+            count_circuits=get_subquery(Circuit, 'provider')
+        ),
         'filterset': ProviderFilterSet,
         'table': ProviderTable,
         'url': 'circuits:provider_list',
@@ -61,17 +61,21 @@ SEARCH_TYPES = OrderedDict((
         'url': 'dcim:rack_list',
     }),
     ('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,
         'table': RackGroupTable,
         'url': 'dcim:rackgroup_list',
     }),
     ('devicetype', {
         '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,
         'table': DeviceTypeTable,
         'url': 'dcim:devicetype_list',
@@ -86,8 +90,8 @@ SEARCH_TYPES = OrderedDict((
     }),
     ('virtualchassis', {
         '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,
         'table': VirtualChassisTable,
         'url': 'dcim:virtualchassis_list',

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

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

+ 7 - 3
netbox/secrets/views.py

@@ -2,12 +2,12 @@ import base64
 import logging
 
 from django.contrib import messages
-from django.db.models import Count
 from django.shortcuts import get_object_or_404, redirect, render
 from django.utils.html import escape
 from django.utils.safestring import mark_safe
 
 from netbox.views import generic
+from utilities.utils import get_subquery
 from . import filters, forms, tables
 from .models import SecretRole, Secret, SessionKey, UserKey
 
@@ -27,7 +27,9 @@ def get_session_key(request):
 #
 
 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
 
 
@@ -47,7 +49,9 @@ class SecretRoleBulkImportView(generic.BulkImportView):
 
 
 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
 
 

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

@@ -19,9 +19,23 @@ class UserSerializer(ValidatedModelSerializer):
     class Meta:
         model = User
         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):

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

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

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

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

+ 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}"
         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
         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 = {
-            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)

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

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

+ 13 - 5
netbox/virtualization/views.py

@@ -1,6 +1,6 @@
 from django.contrib import messages
 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.urls import reverse
 
@@ -21,7 +21,9 @@ from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterf
 #
 
 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
 
 
@@ -41,7 +43,9 @@ class ClusterTypeBulkImportView(generic.BulkImportView):
 
 
 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
 
 
@@ -50,7 +54,9 @@ class ClusterTypeBulkDeleteView(generic.BulkDeleteView):
 #
 
 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
 
 
@@ -70,7 +76,9 @@ class ClusterGroupBulkImportView(generic.BulkImportView):
 
 
 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