Merge branch 'develop' into develop-2.10

This commit is contained in:
Jeremy Stretch 2020-12-01 11:42:55 -05:00
commit 915cf3e715
18 changed files with 197 additions and 119 deletions

View File

@ -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

View File

@ -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') circuit_count=get_subquery(Circuit, 'provider')
).order_by(*Provider._meta.ordering) )
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') circuit_count=get_subquery(Circuit, 'type')
).order_by(*CircuitType._meta.ordering) )
serializer_class = serializers.CircuitTypeSerializer serializer_class = serializers.CircuitTypeSerializer
filterset_class = filters.CircuitTypeFilterSet filterset_class = filters.CircuitTypeFilterSet

View File

@ -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

View File

@ -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') rack_count=get_subquery(Rack, 'role')
).order_by(*RackRole._meta.ordering) )
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') device_count=get_subquery(Device, 'device_type')
).order_by(*DeviceType._meta.ordering) )
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) member_count=get_subquery(Device, 'virtual_chassis')
).order_by(*VirtualChassis._meta.ordering) )
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') powerfeed_count=get_subquery(PowerFeed, 'power_panel')
).order_by(*PowerPanel._meta.ordering) )
serializer_class = serializers.PowerPanelSerializer serializer_class = serializers.PowerPanelSerializer
filterset_class = filters.PowerPanelFilterSet filterset_class = filters.PowerPanelFilterSet

View File

@ -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( queryset = Rack.objects.prefetch_related(
device_count=Count('devices') 'site', 'group', 'tenant', 'role', 'devices__device_type'
).order_by(*Rack._meta.ordering) ).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') devicetype_count=get_subquery(DeviceType, 'manufacturer')
).order_by(*Manufacturer._meta.ordering) )
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( queryset = DeviceType.objects.prefetch_related('manufacturer').annotate(
instance_count=Count('instances') instance_count=get_subquery(Device, 'device_type')
).order_by(*DeviceType._meta.ordering) )
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') instance_count=get_subquery(Device, 'device_type')
).order_by(*DeviceType._meta.ordering) )
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') instance_count=get_subquery(Device, 'device_type')
).order_by(*DeviceType._meta.ordering) )
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( queryset = VirtualChassis.objects.prefetch_related('master').annotate(
member_count=Count('members', distinct=True) member_count=get_subquery(Device, 'virtual_chassis')
).order_by(*VirtualChassis._meta.ordering) )
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( queryset = PowerPanel.objects.prefetch_related(
powerfeed_count=Count('powerfeeds') 'site', 'rack_group'
).order_by(*PowerPanel._meta.ordering) ).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') powerfeed_count=get_subquery(PowerFeed, 'power_panel')
).order_by(*PowerPanel._meta.ordering) )
filterset = filters.PowerPanelFilterSet filterset = filters.PowerPanelFilterSet
table = tables.PowerPanelTable table = tables.PowerPanelTable

View File

@ -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, get_subquery
from utilities.utils import copy_safe_request
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') tagged_items=get_subquery(TaggedItem, 'tag')
).order_by(*Tag._meta.ordering) )
serializer_class = serializers.TagSerializer serializer_class = serializers.TagSerializer
filterset_class = filters.TagFilterSet filterset_class = filters.TagFilterSet

View File

@ -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') items=get_subquery(TaggedItem, 'tag')
).order_by(*Tag._meta.ordering) )
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') items=get_subquery(TaggedItem, 'tag')
).order_by(*Tag._meta.ordering) )
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') items=get_subquery(TaggedItem, 'tag')
).order_by(*Tag._meta.ordering) )
table = tables.TagTable table = tables.TagTable

View File

@ -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') aggregate_count=get_subquery(Aggregate, 'rir')
).order_by(*RIR._meta.ordering) )
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') vlan_count=get_subquery(VLAN, 'group')
).order_by(*VLANGroup._meta.ordering) )
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

View File

@ -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( queryset = VLANGroup.objects.prefetch_related('site').annotate(
vlan_count=Count('vlans') vlan_count=get_subquery(VLAN, 'group')
).order_by(*VLANGroup._meta.ordering) )
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') vlan_count=get_subquery(VLAN, 'group')
).order_by(*VLANGroup._meta.ordering) )
filterset = filters.VLANGroupFilterSet filterset = filters.VLANGroupFilterSet
table = tables.VLANGroupTable table = tables.VLANGroupTable

View File

@ -33,8 +33,8 @@ SEARCH_TYPES = OrderedDict((
# Circuits # Circuits
('provider', { ('provider', {
'queryset': Provider.objects.annotate( 'queryset': Provider.objects.annotate(
count_circuits=Count('circuits') count_circuits=get_subquery(Circuit, 'provider')
).order_by(*Provider._meta.ordering), ),
'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( 'queryset': RackGroup.objects.add_related_count(
rack_count=Count('racks') RackGroup.objects.all(),
).order_by(*RackGroup._meta.ordering), 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') instance_count=get_subquery(Device, 'device_type')
).order_by(*DeviceType._meta.ordering), ),
'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) member_count=get_subquery(Device, 'virtual_chassis')
).order_by(*VirtualChassis._meta.ordering), ),
'filterset': VirtualChassisFilterSet, 'filterset': VirtualChassisFilterSet,
'table': VirtualChassisTable, 'table': VirtualChassisTable,
'url': 'dcim:virtualchassis_list', 'url': 'dcim:virtualchassis_list',

View File

@ -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') secret_count=get_subquery(Secret, 'role')
).order_by(*SecretRole._meta.ordering) )
serializer_class = serializers.SecretRoleSerializer serializer_class = serializers.SecretRoleSerializer
filterset_class = filters.SecretRoleFilterSet filterset_class = filters.SecretRoleFilterSet

View File

@ -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

View File

@ -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', 'id', 'url', 'username', 'password', 'first_name', 'last_name', 'email', 'is_staff', 'is_active',
'groups', '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):

View File

@ -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',
}, },
] ]

View File

@ -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):
""" """

View File

@ -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 :param instance: Python object instance
:data: Dictionary of test data used to define the instance :param 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 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)

View File

@ -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') cluster_count=get_subquery(Cluster, 'type')
).order_by(*ClusterType._meta.ordering) )
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') cluster_count=get_subquery(Cluster, 'group')
).order_by(*ClusterGroup._meta.ordering) )
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

View File

@ -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