diff --git a/docs/release-notes/version-2.9.md b/docs/release-notes/version-2.9.md index 7e7ddd249..563220cb5 100644 --- a/docs/release-notes/version-2.9.md +++ b/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 diff --git a/netbox/circuits/api/views.py b/netbox/circuits/api/views.py index f64dbc2dd..ad497ee5f 100644 --- a/netbox/circuits/api/views.py +++ b/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 diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index 8e58cfffa..653c881a9 100644 --- a/netbox/circuits/views.py +++ b/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 diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 84da2a6af..071174c76 100644 --- a/netbox/dcim/api/views.py +++ b/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 diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 69dfe80a2..e2f542e5c 100644 --- a/netbox/dcim/views.py +++ b/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 diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py index 6bf78bf6a..38077c89a 100644 --- a/netbox/extras/api/views.py +++ b/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 diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 2e25d89f4..99295de1a 100644 --- a/netbox/extras/views.py +++ b/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 diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py index 9e80e216b..9d09bbe03 100644 --- a/netbox/ipam/api/views.py +++ b/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 diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 5769f0a68..1cbac27f6 100644 --- a/netbox/ipam/views.py +++ b/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 diff --git a/netbox/netbox/constants.py b/netbox/netbox/constants.py index de98a238a..a074bde4e 100644 --- a/netbox/netbox/constants.py +++ b/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', diff --git a/netbox/secrets/api/views.py b/netbox/secrets/api/views.py index 940d23a0b..1153b0508 100644 --- a/netbox/secrets/api/views.py +++ b/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 diff --git a/netbox/secrets/views.py b/netbox/secrets/views.py index 14253486d..214ec666c 100644 --- a/netbox/secrets/views.py +++ b/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 diff --git a/netbox/users/api/serializers.py b/netbox/users/api/serializers.py index 31e7915b8..eed0bd80e 100644 --- a/netbox/users/api/serializers.py +++ b/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): diff --git a/netbox/users/tests/test_api.py b/netbox/users/tests/test_api.py index 56bad7f24..ecbaeb952 100644 --- a/netbox/users/tests/test_api.py +++ b/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', }, ] diff --git a/netbox/utilities/testing/api.py b/netbox/utilities/testing/api.py index 189812944..b0caa4f32 100644 --- a/netbox/utilities/testing/api.py +++ b/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): """ diff --git a/netbox/utilities/testing/views.py b/netbox/utilities/testing/views.py index afbcbc057..d19848985 100644 --- a/netbox/utilities/testing/views.py +++ b/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) diff --git a/netbox/virtualization/api/views.py b/netbox/virtualization/api/views.py index 21416029b..2a2a149d4 100644 --- a/netbox/virtualization/api/views.py +++ b/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 diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py index 3c3b0b478..05fe32679 100644 --- a/netbox/virtualization/views.py +++ b/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