Merge branch 'feature' into 14438-script-model

This commit is contained in:
Arthur 2024-02-16 09:00:11 -08:00
commit 3182977a88
46 changed files with 529 additions and 375 deletions

View File

@ -304,6 +304,7 @@ A particular object within NetBox. Each ObjectVar must specify a particular mode
* `model` - The model class * `model` - The model class
* `query_params` - A dictionary of query parameters to use when retrieving available options (optional) * `query_params` - A dictionary of query parameters to use when retrieving available options (optional)
* `context` - A custom dictionary mapping template context variables to fields, used when rendering `<option>` elements within the dropdown menu (optional; see below)
* `null_option` - A label representing a "null" or empty choice (optional) * `null_option` - A label representing a "null" or empty choice (optional)
To limit the selections available within the list, additional query parameters can be passed as the `query_params` dictionary. For example, to show only devices with an "active" status: To limit the selections available within the list, additional query parameters can be passed as the `query_params` dictionary. For example, to show only devices with an "active" status:
@ -331,6 +332,22 @@ site = ObjectVar(
) )
``` ```
#### Context Variables
Custom context variables can be passed to override the default attribute names or to display additional information, such as a parent object.
| Name | Default | Description |
|---------------|-----------------|------------------------------------------------------------------------------|
| `value` | `"id"` | The attribute which contains the option's value |
| `label` | `"display"` | The attribute used as the option's human-friendly label |
| `description` | `"description"` | The attribute to use as a description |
| `depth`[^1] | `"_depth"` | The attribute which indicates an object's depth within a recursive hierarchy |
| `disabled` | -- | The attribute which, if true, signifies that the option should be disabled |
| `parent` | -- | The attribute which represents the object's parent object |
| `count`[^1] | -- | The attribute which contains a numeric count of related objects |
[^1]: The value of this attribute must be a positive integer
### MultiObjectVar ### MultiObjectVar
Similar to `ObjectVar`, but allows for the selection of multiple objects. Similar to `ObjectVar`, but allows for the selection of multiple objects.

View File

@ -5,6 +5,7 @@
### Breaking Changes ### Breaking Changes
* The deprecated `device_role` & `device_role_id` filters for devices have been removed. (Use `role` and `role_id` instead.) * The deprecated `device_role` & `device_role_id` filters for devices have been removed. (Use `role` and `role_id` instead.)
* The legacy reports functionality has been dropped. Reports will be automatically converted to custom scripts on upgrade.
### New Features ### New Features
@ -15,6 +16,8 @@ The NetBox user interface has been completely refreshed and updated.
### Enhancements ### Enhancements
* [#12851](https://github.com/netbox-community/netbox/issues/12851) - Replace bleach HTML sanitization library with nh3 * [#12851](https://github.com/netbox-community/netbox/issues/12851) - Replace bleach HTML sanitization library with nh3
* [#13283](https://github.com/netbox-community/netbox/issues/13283) - Display additional context on API-backed dropdown fields
* [#14237](https://github.com/netbox-community/netbox/issues/14237) - Automatically clear dependent selection fields when modifying a parent selection
* [#14637](https://github.com/netbox-community/netbox/issues/14637) - Upgrade to Django 5.0 * [#14637](https://github.com/netbox-community/netbox/issues/14637) - Upgrade to Django 5.0
* [#14672](https://github.com/netbox-community/netbox/issues/14672) - Add support for Python 3.12 * [#14672](https://github.com/netbox-community/netbox/issues/14672) - Add support for Python 3.12
* [#14728](https://github.com/netbox-community/netbox/issues/14728) - The plugins list view has been moved from the legacy admin UI to the main NetBox UI * [#14728](https://github.com/netbox-community/netbox/issues/14728) - The plugins list view has been moved from the legacy admin UI to the main NetBox UI
@ -23,6 +26,7 @@ The NetBox user interface has been completely refreshed and updated.
### Other Changes ### Other Changes
* [#12325](https://github.com/netbox-community/netbox/issues/12325) - The Django admin UI is now disabled by default (set `DJANGO_ADMIN_ENABLED` to True to enable it) * [#12325](https://github.com/netbox-community/netbox/issues/12325) - The Django admin UI is now disabled by default (set `DJANGO_ADMIN_ENABLED` to True to enable it)
* [#12510](https://github.com/netbox-community/netbox/issues/12510) - Dropped support for legacy reports
* [#12795](https://github.com/netbox-community/netbox/issues/12795) - NetBox now uses a custom User model rather than the stock model provided by Django * [#12795](https://github.com/netbox-community/netbox/issues/12795) - NetBox now uses a custom User model rather than the stock model provided by Django
* [#13647](https://github.com/netbox-community/netbox/issues/13647) - Squash all database migrations prior to v3.7 * [#13647](https://github.com/netbox-community/netbox/issues/13647) - Squash all database migrations prior to v3.7
* [#14092](https://github.com/netbox-community/netbox/issues/14092) - Remove backward compatibility for importing plugin resources from `extras.plugins` (now `netbox.plugins`) * [#14092](https://github.com/netbox-community/netbox/issues/14092) - Remove backward compatibility for importing plugin resources from `extras.plugins` (now `netbox.plugins`)

View File

@ -292,6 +292,7 @@ nav:
- git Cheat Sheet: 'development/git-cheat-sheet.md' - git Cheat Sheet: 'development/git-cheat-sheet.md'
- Release Notes: - Release Notes:
- Summary: 'release-notes/index.md' - Summary: 'release-notes/index.md'
- Version 4.0: 'release-notes/version-4.0.md'
- Version 3.7: 'release-notes/version-3.7.md' - Version 3.7: 'release-notes/version-3.7.md'
- Version 3.6: 'release-notes/version-3.6.md' - Version 3.6: 'release-notes/version-3.6.md'
- Version 3.5: 'release-notes/version-3.5.md' - Version 3.5: 'release-notes/version-3.5.md'

View File

@ -1,8 +1,8 @@
from drf_spectacular.utils import extend_schema_field, extend_schema_serializer from drf_spectacular.utils import extend_schema_serializer
from drf_spectacular.types import OpenApiTypes
from rest_framework import serializers from rest_framework import serializers
from circuits.models import * from circuits.models import *
from netbox.api.fields import RelatedObjectCountField
from netbox.api.serializers import WritableNestedSerializer from netbox.api.serializers import WritableNestedSerializer
__all__ = [ __all__ = [
@ -36,7 +36,7 @@ class NestedProviderNetworkSerializer(WritableNestedSerializer):
) )
class NestedProviderSerializer(WritableNestedSerializer): class NestedProviderSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail') url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
circuit_count = serializers.IntegerField(read_only=True) circuit_count = RelatedObjectCountField('circuits')
class Meta: class Meta:
model = Provider model = Provider
@ -64,7 +64,7 @@ class NestedProviderAccountSerializer(WritableNestedSerializer):
) )
class NestedCircuitTypeSerializer(WritableNestedSerializer): class NestedCircuitTypeSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail') url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
circuit_count = serializers.IntegerField(read_only=True) circuit_count = RelatedObjectCountField('circuits')
class Meta: class Meta:
model = CircuitType model = CircuitType

View File

@ -6,7 +6,7 @@ from dcim.api.nested_serializers import NestedSiteSerializer
from dcim.api.serializers import CabledObjectSerializer from dcim.api.serializers import CabledObjectSerializer
from ipam.models import ASN from ipam.models import ASN
from ipam.api.nested_serializers import NestedASNSerializer from ipam.api.nested_serializers import NestedASNSerializer
from netbox.api.fields import ChoiceField, SerializedPKRelatedField from netbox.api.fields import ChoiceField, RelatedObjectCountField, SerializedPKRelatedField
from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
from tenancy.api.nested_serializers import NestedTenantSerializer from tenancy.api.nested_serializers import NestedTenantSerializer
from .nested_serializers import * from .nested_serializers import *
@ -32,7 +32,7 @@ class ProviderSerializer(NetBoxModelSerializer):
) )
# Related object counts # Related object counts
circuit_count = serializers.IntegerField(read_only=True) circuit_count = RelatedObjectCountField('circuits')
class Meta: class Meta:
model = Provider model = Provider
@ -80,13 +80,15 @@ class ProviderNetworkSerializer(NetBoxModelSerializer):
class CircuitTypeSerializer(NetBoxModelSerializer): class CircuitTypeSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail') url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
circuit_count = serializers.IntegerField(read_only=True)
# Related object counts
circuit_count = RelatedObjectCountField('circuits')
class Meta: class Meta:
model = CircuitType model = CircuitType
fields = [ fields = [
'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created', 'last_updated', 'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created',
'circuit_count', 'last_updated', 'circuit_count',
] ]

View File

@ -4,7 +4,6 @@ from circuits import filtersets
from circuits.models import * from circuits.models import *
from dcim.api.views import PassThroughPortMixin from dcim.api.views import PassThroughPortMixin
from netbox.api.viewsets import NetBoxModelViewSet from netbox.api.viewsets import NetBoxModelViewSet
from utilities.utils import count_related
from . import serializers from . import serializers
@ -21,9 +20,7 @@ class CircuitsRootView(APIRootView):
# #
class ProviderViewSet(NetBoxModelViewSet): class ProviderViewSet(NetBoxModelViewSet):
queryset = Provider.objects.prefetch_related('asns', 'tags').annotate( queryset = Provider.objects.all()
circuit_count=count_related(Circuit, 'provider')
)
serializer_class = serializers.ProviderSerializer serializer_class = serializers.ProviderSerializer
filterset_class = filtersets.ProviderFilterSet filterset_class = filtersets.ProviderFilterSet
@ -33,9 +30,7 @@ class ProviderViewSet(NetBoxModelViewSet):
# #
class CircuitTypeViewSet(NetBoxModelViewSet): class CircuitTypeViewSet(NetBoxModelViewSet):
queryset = CircuitType.objects.prefetch_related('tags').annotate( queryset = CircuitType.objects.all()
circuit_count=count_related(Circuit, 'type')
)
serializer_class = serializers.CircuitTypeSerializer serializer_class = serializers.CircuitTypeSerializer
filterset_class = filtersets.CircuitTypeFilterSet filterset_class = filtersets.CircuitTypeFilterSet
@ -45,9 +40,7 @@ class CircuitTypeViewSet(NetBoxModelViewSet):
# #
class CircuitViewSet(NetBoxModelViewSet): class CircuitViewSet(NetBoxModelViewSet):
queryset = Circuit.objects.prefetch_related( queryset = Circuit.objects.all()
'type', 'tenant', 'provider', 'provider_account', 'termination_a', 'termination_z'
).prefetch_related('tags')
serializer_class = serializers.CircuitSerializer serializer_class = serializers.CircuitSerializer
filterset_class = filtersets.CircuitFilterSet filterset_class = filtersets.CircuitFilterSet
@ -57,12 +50,9 @@ class CircuitViewSet(NetBoxModelViewSet):
# #
class CircuitTerminationViewSet(PassThroughPortMixin, NetBoxModelViewSet): class CircuitTerminationViewSet(PassThroughPortMixin, NetBoxModelViewSet):
queryset = CircuitTermination.objects.prefetch_related( queryset = CircuitTermination.objects.all()
'circuit', 'site', 'provider_network', 'cable__terminations'
)
serializer_class = serializers.CircuitTerminationSerializer serializer_class = serializers.CircuitTerminationSerializer
filterset_class = filtersets.CircuitTerminationFilterSet filterset_class = filtersets.CircuitTerminationFilterSet
brief_prefetch_fields = ['circuit']
# #
@ -70,7 +60,7 @@ class CircuitTerminationViewSet(PassThroughPortMixin, NetBoxModelViewSet):
# #
class ProviderAccountViewSet(NetBoxModelViewSet): class ProviderAccountViewSet(NetBoxModelViewSet):
queryset = ProviderAccount.objects.prefetch_related('provider', 'tags') queryset = ProviderAccount.objects.all()
serializer_class = serializers.ProviderAccountSerializer serializer_class = serializers.ProviderAccountSerializer
filterset_class = filtersets.ProviderAccountFilterSet filterset_class = filtersets.ProviderAccountFilterSet
@ -80,6 +70,6 @@ class ProviderAccountViewSet(NetBoxModelViewSet):
# #
class ProviderNetworkViewSet(NetBoxModelViewSet): class ProviderNetworkViewSet(NetBoxModelViewSet):
queryset = ProviderNetwork.objects.prefetch_related('tags') queryset = ProviderNetwork.objects.all()
serializer_class = serializers.ProviderNetworkSerializer serializer_class = serializers.ProviderNetworkSerializer
filterset_class = filtersets.ProviderNetworkFilterSet filterset_class = filtersets.ProviderNetworkFilterSet

View File

@ -2,7 +2,7 @@ from rest_framework import serializers
from core.choices import * from core.choices import *
from core.models import * from core.models import *
from netbox.api.fields import ChoiceField, ContentTypeField from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField
from netbox.api.serializers import BaseModelSerializer, NetBoxModelSerializer from netbox.api.serializers import BaseModelSerializer, NetBoxModelSerializer
from netbox.utils import get_data_backend_choices from netbox.utils import get_data_backend_choices
from users.api.nested_serializers import NestedUserSerializer from users.api.nested_serializers import NestedUserSerializer
@ -28,9 +28,7 @@ class DataSourceSerializer(NetBoxModelSerializer):
) )
# Related object counts # Related object counts
file_count = serializers.IntegerField( file_count = RelatedObjectCountField('datafiles')
read_only=True
)
class Meta: class Meta:
model = DataSource model = DataSource

View File

@ -9,7 +9,6 @@ from rest_framework.viewsets import ReadOnlyModelViewSet
from core import filtersets from core import filtersets
from core.models import * from core.models import *
from netbox.api.viewsets import NetBoxModelViewSet, NetBoxReadOnlyModelViewSet from netbox.api.viewsets import NetBoxModelViewSet, NetBoxReadOnlyModelViewSet
from utilities.utils import count_related
from . import serializers from . import serializers
@ -22,9 +21,7 @@ class CoreRootView(APIRootView):
class DataSourceViewSet(NetBoxModelViewSet): class DataSourceViewSet(NetBoxModelViewSet):
queryset = DataSource.objects.annotate( queryset = DataSource.objects.all()
file_count=count_related(DataFile, 'source')
)
serializer_class = serializers.DataSourceSerializer serializer_class = serializers.DataSourceSerializer
filterset_class = filtersets.DataSourceFilterSet filterset_class = filtersets.DataSourceFilterSet
@ -44,7 +41,7 @@ class DataSourceViewSet(NetBoxModelViewSet):
class DataFileViewSet(NetBoxReadOnlyModelViewSet): class DataFileViewSet(NetBoxReadOnlyModelViewSet):
queryset = DataFile.objects.defer('data').prefetch_related('source') queryset = DataFile.objects.defer('data')
serializer_class = serializers.DataFileSerializer serializer_class = serializers.DataFileSerializer
filterset_class = filtersets.DataFileFilterSet filterset_class = filtersets.DataFileFilterSet
@ -53,6 +50,6 @@ class JobViewSet(ReadOnlyModelViewSet):
""" """
Retrieve a list of job results Retrieve a list of job results
""" """
queryset = Job.objects.prefetch_related('user') queryset = Job.objects.all()
serializer_class = serializers.JobSerializer serializer_class = serializers.JobSerializer
filterset_class = filtersets.JobFilterSet filterset_class = filtersets.JobFilterSet

View File

@ -2,7 +2,8 @@ from drf_spectacular.utils import extend_schema_serializer
from rest_framework import serializers from rest_framework import serializers
from dcim import models from dcim import models
from netbox.api.serializers import BaseModelSerializer, WritableNestedSerializer from netbox.api.fields import RelatedObjectCountField
from netbox.api.serializers import WritableNestedSerializer
__all__ = [ __all__ = [
'ComponentNestedModuleSerializer', 'ComponentNestedModuleSerializer',
@ -110,7 +111,7 @@ class NestedLocationSerializer(WritableNestedSerializer):
) )
class NestedRackRoleSerializer(WritableNestedSerializer): class NestedRackRoleSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
rack_count = serializers.IntegerField(read_only=True) rack_count = RelatedObjectCountField('racks')
class Meta: class Meta:
model = models.RackRole model = models.RackRole
@ -122,7 +123,7 @@ class NestedRackRoleSerializer(WritableNestedSerializer):
) )
class NestedRackSerializer(WritableNestedSerializer): class NestedRackSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail')
device_count = serializers.IntegerField(read_only=True) device_count = RelatedObjectCountField('devices')
class Meta: class Meta:
model = models.Rack model = models.Rack
@ -150,7 +151,7 @@ class NestedRackReservationSerializer(WritableNestedSerializer):
) )
class NestedManufacturerSerializer(WritableNestedSerializer): class NestedManufacturerSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
devicetype_count = serializers.IntegerField(read_only=True) devicetype_count = RelatedObjectCountField('device_types')
class Meta: class Meta:
model = models.Manufacturer model = models.Manufacturer
@ -163,7 +164,7 @@ class NestedManufacturerSerializer(WritableNestedSerializer):
class NestedDeviceTypeSerializer(WritableNestedSerializer): class NestedDeviceTypeSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
manufacturer = NestedManufacturerSerializer(read_only=True) manufacturer = NestedManufacturerSerializer(read_only=True)
device_count = serializers.IntegerField(read_only=True) device_count = RelatedObjectCountField('instances')
class Meta: class Meta:
model = models.DeviceType model = models.DeviceType
@ -173,7 +174,6 @@ class NestedDeviceTypeSerializer(WritableNestedSerializer):
class NestedModuleTypeSerializer(WritableNestedSerializer): class NestedModuleTypeSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:moduletype-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:moduletype-detail')
manufacturer = NestedManufacturerSerializer(read_only=True) manufacturer = NestedManufacturerSerializer(read_only=True)
# module_count = serializers.IntegerField(read_only=True)
class Meta: class Meta:
model = models.ModuleType model = models.ModuleType
@ -274,8 +274,8 @@ class NestedInventoryItemTemplateSerializer(WritableNestedSerializer):
) )
class NestedDeviceRoleSerializer(WritableNestedSerializer): class NestedDeviceRoleSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
device_count = serializers.IntegerField(read_only=True) device_count = RelatedObjectCountField('devices')
virtualmachine_count = serializers.IntegerField(read_only=True) virtualmachine_count = RelatedObjectCountField('virtual_machines')
class Meta: class Meta:
model = models.DeviceRole model = models.DeviceRole
@ -287,8 +287,8 @@ class NestedDeviceRoleSerializer(WritableNestedSerializer):
) )
class NestedPlatformSerializer(WritableNestedSerializer): class NestedPlatformSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
device_count = serializers.IntegerField(read_only=True) device_count = RelatedObjectCountField('devices')
virtualmachine_count = serializers.IntegerField(read_only=True) virtualmachine_count = RelatedObjectCountField('virtual_machines')
class Meta: class Meta:
model = models.Platform model = models.Platform
@ -445,7 +445,7 @@ class NestedInventoryItemSerializer(WritableNestedSerializer):
) )
class NestedInventoryItemRoleSerializer(WritableNestedSerializer): class NestedInventoryItemRoleSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemrole-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemrole-detail')
inventoryitem_count = serializers.IntegerField(read_only=True) inventoryitem_count = RelatedObjectCountField('inventory_items')
class Meta: class Meta:
model = models.InventoryItemRole model = models.InventoryItemRole
@ -490,7 +490,7 @@ class NestedVirtualChassisSerializer(WritableNestedSerializer):
) )
class NestedPowerPanelSerializer(WritableNestedSerializer): class NestedPowerPanelSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail')
powerfeed_count = serializers.IntegerField(read_only=True) powerfeed_count = RelatedObjectCountField('powerfeeds')
class Meta: class Meta:
model = models.PowerPanel model = models.PowerPanel

View File

@ -15,7 +15,7 @@ from ipam.api.nested_serializers import (
NestedASNSerializer, NestedIPAddressSerializer, NestedVLANSerializer, NestedVRFSerializer, NestedASNSerializer, NestedIPAddressSerializer, NestedVLANSerializer, NestedVRFSerializer,
) )
from ipam.models import ASN, VLAN from ipam.models import ASN, VLAN
from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField, SerializedPKRelatedField
from netbox.api.serializers import ( from netbox.api.serializers import (
GenericObjectSerializer, NestedGroupModelSerializer, NetBoxModelSerializer, ValidatedModelSerializer, GenericObjectSerializer, NestedGroupModelSerializer, NetBoxModelSerializer, ValidatedModelSerializer,
WritableNestedSerializer, WritableNestedSerializer,
@ -144,12 +144,12 @@ class SiteSerializer(NetBoxModelSerializer):
) )
# Related object counts # Related object counts
circuit_count = serializers.IntegerField(read_only=True) circuit_count = RelatedObjectCountField('circuit_terminations')
device_count = serializers.IntegerField(read_only=True) device_count = RelatedObjectCountField('devices')
prefix_count = serializers.IntegerField(read_only=True) prefix_count = RelatedObjectCountField('prefixes')
rack_count = serializers.IntegerField(read_only=True) rack_count = RelatedObjectCountField('racks')
virtualmachine_count = serializers.IntegerField(read_only=True) vlan_count = RelatedObjectCountField('vlans')
vlan_count = serializers.IntegerField(read_only=True) virtualmachine_count = RelatedObjectCountField('virtual_machines')
class Meta: class Meta:
model = Site model = Site
@ -184,7 +184,9 @@ class LocationSerializer(NestedGroupModelSerializer):
class RackRoleSerializer(NetBoxModelSerializer): class RackRoleSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
rack_count = serializers.IntegerField(read_only=True)
# Related object counts
rack_count = RelatedObjectCountField('racks')
class Meta: class Meta:
model = RackRole model = RackRole
@ -207,8 +209,10 @@ class RackSerializer(NetBoxModelSerializer):
width = ChoiceField(choices=RackWidthChoices, required=False) width = ChoiceField(choices=RackWidthChoices, required=False)
outer_unit = ChoiceField(choices=RackDimensionUnitChoices, allow_blank=True, required=False, allow_null=True) outer_unit = ChoiceField(choices=RackDimensionUnitChoices, allow_blank=True, required=False, allow_null=True)
weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True) weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True)
device_count = serializers.IntegerField(read_only=True)
powerfeed_count = serializers.IntegerField(read_only=True) # Related object counts
device_count = RelatedObjectCountField('devices')
powerfeed_count = RelatedObjectCountField('powerfeeds')
class Meta: class Meta:
model = Rack model = Rack
@ -299,9 +303,11 @@ class RackElevationDetailFilterSerializer(serializers.Serializer):
class ManufacturerSerializer(NetBoxModelSerializer): class ManufacturerSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
devicetype_count = serializers.IntegerField(read_only=True)
inventoryitem_count = serializers.IntegerField(read_only=True) # Related object counts
platform_count = serializers.IntegerField(read_only=True) devicetype_count = RelatedObjectCountField('device_types')
inventoryitem_count = RelatedObjectCountField('inventory_items')
platform_count = RelatedObjectCountField('platforms')
class Meta: class Meta:
model = Manufacturer model = Manufacturer
@ -325,7 +331,6 @@ class DeviceTypeSerializer(NetBoxModelSerializer):
subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False, allow_null=True) subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False, allow_null=True)
airflow = ChoiceField(choices=DeviceAirflowChoices, allow_blank=True, required=False, allow_null=True) airflow = ChoiceField(choices=DeviceAirflowChoices, allow_blank=True, required=False, allow_null=True)
weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True) weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True)
device_count = serializers.IntegerField(read_only=True)
# Counter fields # Counter fields
console_port_template_count = serializers.IntegerField(read_only=True) console_port_template_count = serializers.IntegerField(read_only=True)
@ -339,6 +344,9 @@ class DeviceTypeSerializer(NetBoxModelSerializer):
module_bay_template_count = serializers.IntegerField(read_only=True) module_bay_template_count = serializers.IntegerField(read_only=True)
inventory_item_template_count = serializers.IntegerField(read_only=True) inventory_item_template_count = serializers.IntegerField(read_only=True)
# Related object counts
device_count = RelatedObjectCountField('instances')
class Meta: class Meta:
model = DeviceType model = DeviceType
fields = [ fields = [
@ -636,8 +644,10 @@ class InventoryItemTemplateSerializer(ValidatedModelSerializer):
class DeviceRoleSerializer(NetBoxModelSerializer): class DeviceRoleSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
config_template = NestedConfigTemplateSerializer(required=False, allow_null=True, default=None) config_template = NestedConfigTemplateSerializer(required=False, allow_null=True, default=None)
device_count = serializers.IntegerField(read_only=True)
virtualmachine_count = serializers.IntegerField(read_only=True) # Related object counts
device_count = RelatedObjectCountField('devices')
virtualmachine_count = RelatedObjectCountField('virtual_machines')
class Meta: class Meta:
model = DeviceRole model = DeviceRole
@ -651,8 +661,10 @@ class PlatformSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
manufacturer = NestedManufacturerSerializer(required=False, allow_null=True) manufacturer = NestedManufacturerSerializer(required=False, allow_null=True)
config_template = NestedConfigTemplateSerializer(required=False, allow_null=True, default=None) config_template = NestedConfigTemplateSerializer(required=False, allow_null=True, default=None)
device_count = serializers.IntegerField(read_only=True)
virtualmachine_count = serializers.IntegerField(read_only=True) # Related object counts
device_count = RelatedObjectCountField('devices')
virtualmachine_count = RelatedObjectCountField('virtual_machines')
class Meta: class Meta:
model = Platform model = Platform
@ -761,7 +773,7 @@ class VirtualDeviceContextSerializer(NetBoxModelSerializer):
status = ChoiceField(choices=VirtualDeviceContextStatusChoices) status = ChoiceField(choices=VirtualDeviceContextStatusChoices)
# Related object counts # Related object counts
interface_count = serializers.IntegerField(read_only=True) interface_count = RelatedObjectCountField('interfaces')
class Meta: class Meta:
model = VirtualDeviceContext model = VirtualDeviceContext
@ -1092,7 +1104,9 @@ class InventoryItemSerializer(NetBoxModelSerializer):
class InventoryItemRoleSerializer(NetBoxModelSerializer): class InventoryItemRoleSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemrole-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemrole-detail')
inventoryitem_count = serializers.IntegerField(read_only=True)
# Related object counts
inventoryitem_count = RelatedObjectCountField('inventory_items')
class Meta: class Meta:
model = InventoryItemRole model = InventoryItemRole
@ -1204,7 +1218,9 @@ class PowerPanelSerializer(NetBoxModelSerializer):
allow_null=True, allow_null=True,
default=None default=None
) )
powerfeed_count = serializers.IntegerField(read_only=True)
# Related object counts
powerfeed_count = RelatedObjectCountField('powerfeeds')
class Meta: class Meta:
model = PowerPanel model = PowerPanel

View File

@ -13,7 +13,6 @@ from dcim.constants import CABLE_TRACE_SVG_DEFAULT_WIDTH
from dcim.models import * from dcim.models import *
from dcim.svg import CableTraceSVG from dcim.svg import CableTraceSVG
from extras.api.mixins import ConfigContextQuerySetMixin, RenderConfigMixin from extras.api.mixins import ConfigContextQuerySetMixin, RenderConfigMixin
from ipam.models import Prefix, VLAN
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.pagination import StripCountAnnotationsPaginator from netbox.api.pagination import StripCountAnnotationsPaginator
@ -23,7 +22,6 @@ from netbox.constants import NESTED_SERIALIZER_PREFIX
from utilities.api import get_serializer_for_model from utilities.api import get_serializer_for_model
from utilities.query_functions import CollateAsChar from utilities.query_functions import CollateAsChar
from utilities.utils import count_related from utilities.utils import count_related
from virtualization.models import VirtualMachine
from . import serializers from . import serializers
from .exceptions import MissingFilterException from .exceptions import MissingFilterException
@ -103,7 +101,7 @@ class RegionViewSet(MPTTLockedMixin, NetBoxModelViewSet):
'region', 'region',
'site_count', 'site_count',
cumulative=True cumulative=True
).prefetch_related('tags') )
serializer_class = serializers.RegionSerializer serializer_class = serializers.RegionSerializer
filterset_class = filtersets.RegionFilterSet filterset_class = filtersets.RegionFilterSet
@ -119,7 +117,7 @@ class SiteGroupViewSet(MPTTLockedMixin, NetBoxModelViewSet):
'group', 'group',
'site_count', 'site_count',
cumulative=True cumulative=True
).prefetch_related('tags') )
serializer_class = serializers.SiteGroupSerializer serializer_class = serializers.SiteGroupSerializer
filterset_class = filtersets.SiteGroupFilterSet filterset_class = filtersets.SiteGroupFilterSet
@ -129,16 +127,7 @@ class SiteGroupViewSet(MPTTLockedMixin, NetBoxModelViewSet):
# #
class SiteViewSet(NetBoxModelViewSet): class SiteViewSet(NetBoxModelViewSet):
queryset = Site.objects.prefetch_related( queryset = Site.objects.all()
'region', 'tenant', 'asns', 'tags'
).annotate(
device_count=count_related(Device, 'site'),
rack_count=count_related(Rack, 'site'),
prefix_count=count_related(Prefix, 'site'),
vlan_count=count_related(VLAN, 'site'),
circuit_count=count_related(Circuit, 'terminations__site'),
virtualmachine_count=count_related(VirtualMachine, 'cluster__site')
)
serializer_class = serializers.SiteSerializer serializer_class = serializers.SiteSerializer
filterset_class = filtersets.SiteFilterSet filterset_class = filtersets.SiteFilterSet
@ -160,7 +149,7 @@ class LocationViewSet(MPTTLockedMixin, NetBoxModelViewSet):
'location', 'location',
'rack_count', 'rack_count',
cumulative=True cumulative=True
).prefetch_related('site', 'tags') )
serializer_class = serializers.LocationSerializer serializer_class = serializers.LocationSerializer
filterset_class = filtersets.LocationFilterSet filterset_class = filtersets.LocationFilterSet
@ -170,9 +159,7 @@ class LocationViewSet(MPTTLockedMixin, NetBoxModelViewSet):
# #
class RackRoleViewSet(NetBoxModelViewSet): class RackRoleViewSet(NetBoxModelViewSet):
queryset = RackRole.objects.prefetch_related('tags').annotate( queryset = RackRole.objects.all()
rack_count=count_related(Rack, 'role')
)
serializer_class = serializers.RackRoleSerializer serializer_class = serializers.RackRoleSerializer
filterset_class = filtersets.RackRoleFilterSet filterset_class = filtersets.RackRoleFilterSet
@ -182,12 +169,7 @@ class RackRoleViewSet(NetBoxModelViewSet):
# #
class RackViewSet(NetBoxModelViewSet): class RackViewSet(NetBoxModelViewSet):
queryset = Rack.objects.prefetch_related( queryset = Rack.objects.all()
'site', 'location', 'role', 'tenant', 'tags'
).annotate(
device_count=count_related(Device, 'rack'),
powerfeed_count=count_related(PowerFeed, 'rack')
)
serializer_class = serializers.RackSerializer serializer_class = serializers.RackSerializer
filterset_class = filtersets.RackFilterSet filterset_class = filtersets.RackFilterSet
@ -249,7 +231,7 @@ class RackViewSet(NetBoxModelViewSet):
# #
class RackReservationViewSet(NetBoxModelViewSet): class RackReservationViewSet(NetBoxModelViewSet):
queryset = RackReservation.objects.prefetch_related('rack', 'user', 'tenant') queryset = RackReservation.objects.all()
serializer_class = serializers.RackReservationSerializer serializer_class = serializers.RackReservationSerializer
filterset_class = filtersets.RackReservationFilterSet filterset_class = filtersets.RackReservationFilterSet
@ -259,11 +241,7 @@ class RackReservationViewSet(NetBoxModelViewSet):
# #
class ManufacturerViewSet(NetBoxModelViewSet): class ManufacturerViewSet(NetBoxModelViewSet):
queryset = Manufacturer.objects.prefetch_related('tags').annotate( queryset = Manufacturer.objects.all()
devicetype_count=count_related(DeviceType, 'manufacturer'),
inventoryitem_count=count_related(InventoryItem, 'manufacturer'),
platform_count=count_related(Platform, 'manufacturer')
)
serializer_class = serializers.ManufacturerSerializer serializer_class = serializers.ManufacturerSerializer
filterset_class = filtersets.ManufacturerFilterSet filterset_class = filtersets.ManufacturerFilterSet
@ -273,21 +251,15 @@ class ManufacturerViewSet(NetBoxModelViewSet):
# #
class DeviceTypeViewSet(NetBoxModelViewSet): class DeviceTypeViewSet(NetBoxModelViewSet):
queryset = DeviceType.objects.prefetch_related('manufacturer', 'default_platform', 'tags').annotate( queryset = DeviceType.objects.all()
device_count=count_related(Device, 'device_type')
)
serializer_class = serializers.DeviceTypeSerializer serializer_class = serializers.DeviceTypeSerializer
filterset_class = filtersets.DeviceTypeFilterSet filterset_class = filtersets.DeviceTypeFilterSet
brief_prefetch_fields = ['manufacturer']
class ModuleTypeViewSet(NetBoxModelViewSet): class ModuleTypeViewSet(NetBoxModelViewSet):
queryset = ModuleType.objects.prefetch_related('manufacturer', 'tags').annotate( queryset = ModuleType.objects.all()
# module_count=count_related(Module, 'module_type')
)
serializer_class = serializers.ModuleTypeSerializer serializer_class = serializers.ModuleTypeSerializer
filterset_class = filtersets.ModuleTypeFilterSet filterset_class = filtersets.ModuleTypeFilterSet
brief_prefetch_fields = ['manufacturer']
# #
@ -295,61 +267,61 @@ class ModuleTypeViewSet(NetBoxModelViewSet):
# #
class ConsolePortTemplateViewSet(NetBoxModelViewSet): class ConsolePortTemplateViewSet(NetBoxModelViewSet):
queryset = ConsolePortTemplate.objects.prefetch_related('device_type__manufacturer') queryset = ConsolePortTemplate.objects.all()
serializer_class = serializers.ConsolePortTemplateSerializer serializer_class = serializers.ConsolePortTemplateSerializer
filterset_class = filtersets.ConsolePortTemplateFilterSet filterset_class = filtersets.ConsolePortTemplateFilterSet
class ConsoleServerPortTemplateViewSet(NetBoxModelViewSet): class ConsoleServerPortTemplateViewSet(NetBoxModelViewSet):
queryset = ConsoleServerPortTemplate.objects.prefetch_related('device_type__manufacturer') queryset = ConsoleServerPortTemplate.objects.all()
serializer_class = serializers.ConsoleServerPortTemplateSerializer serializer_class = serializers.ConsoleServerPortTemplateSerializer
filterset_class = filtersets.ConsoleServerPortTemplateFilterSet filterset_class = filtersets.ConsoleServerPortTemplateFilterSet
class PowerPortTemplateViewSet(NetBoxModelViewSet): class PowerPortTemplateViewSet(NetBoxModelViewSet):
queryset = PowerPortTemplate.objects.prefetch_related('device_type__manufacturer') queryset = PowerPortTemplate.objects.all()
serializer_class = serializers.PowerPortTemplateSerializer serializer_class = serializers.PowerPortTemplateSerializer
filterset_class = filtersets.PowerPortTemplateFilterSet filterset_class = filtersets.PowerPortTemplateFilterSet
class PowerOutletTemplateViewSet(NetBoxModelViewSet): class PowerOutletTemplateViewSet(NetBoxModelViewSet):
queryset = PowerOutletTemplate.objects.prefetch_related('device_type__manufacturer') queryset = PowerOutletTemplate.objects.all()
serializer_class = serializers.PowerOutletTemplateSerializer serializer_class = serializers.PowerOutletTemplateSerializer
filterset_class = filtersets.PowerOutletTemplateFilterSet filterset_class = filtersets.PowerOutletTemplateFilterSet
class InterfaceTemplateViewSet(NetBoxModelViewSet): class InterfaceTemplateViewSet(NetBoxModelViewSet):
queryset = InterfaceTemplate.objects.prefetch_related('device_type__manufacturer') queryset = InterfaceTemplate.objects.all()
serializer_class = serializers.InterfaceTemplateSerializer serializer_class = serializers.InterfaceTemplateSerializer
filterset_class = filtersets.InterfaceTemplateFilterSet filterset_class = filtersets.InterfaceTemplateFilterSet
class FrontPortTemplateViewSet(NetBoxModelViewSet): class FrontPortTemplateViewSet(NetBoxModelViewSet):
queryset = FrontPortTemplate.objects.prefetch_related('device_type__manufacturer') queryset = FrontPortTemplate.objects.all()
serializer_class = serializers.FrontPortTemplateSerializer serializer_class = serializers.FrontPortTemplateSerializer
filterset_class = filtersets.FrontPortTemplateFilterSet filterset_class = filtersets.FrontPortTemplateFilterSet
class RearPortTemplateViewSet(NetBoxModelViewSet): class RearPortTemplateViewSet(NetBoxModelViewSet):
queryset = RearPortTemplate.objects.prefetch_related('device_type__manufacturer') queryset = RearPortTemplate.objects.all()
serializer_class = serializers.RearPortTemplateSerializer serializer_class = serializers.RearPortTemplateSerializer
filterset_class = filtersets.RearPortTemplateFilterSet filterset_class = filtersets.RearPortTemplateFilterSet
class ModuleBayTemplateViewSet(NetBoxModelViewSet): class ModuleBayTemplateViewSet(NetBoxModelViewSet):
queryset = ModuleBayTemplate.objects.prefetch_related('device_type__manufacturer') queryset = ModuleBayTemplate.objects.all()
serializer_class = serializers.ModuleBayTemplateSerializer serializer_class = serializers.ModuleBayTemplateSerializer
filterset_class = filtersets.ModuleBayTemplateFilterSet filterset_class = filtersets.ModuleBayTemplateFilterSet
class DeviceBayTemplateViewSet(NetBoxModelViewSet): class DeviceBayTemplateViewSet(NetBoxModelViewSet):
queryset = DeviceBayTemplate.objects.prefetch_related('device_type__manufacturer') queryset = DeviceBayTemplate.objects.all()
serializer_class = serializers.DeviceBayTemplateSerializer serializer_class = serializers.DeviceBayTemplateSerializer
filterset_class = filtersets.DeviceBayTemplateFilterSet filterset_class = filtersets.DeviceBayTemplateFilterSet
class InventoryItemTemplateViewSet(MPTTLockedMixin, NetBoxModelViewSet): class InventoryItemTemplateViewSet(MPTTLockedMixin, NetBoxModelViewSet):
queryset = InventoryItemTemplate.objects.prefetch_related('device_type__manufacturer', 'role') queryset = InventoryItemTemplate.objects.all()
serializer_class = serializers.InventoryItemTemplateSerializer serializer_class = serializers.InventoryItemTemplateSerializer
filterset_class = filtersets.InventoryItemTemplateFilterSet filterset_class = filtersets.InventoryItemTemplateFilterSet
@ -359,10 +331,7 @@ class InventoryItemTemplateViewSet(MPTTLockedMixin, NetBoxModelViewSet):
# #
class DeviceRoleViewSet(NetBoxModelViewSet): class DeviceRoleViewSet(NetBoxModelViewSet):
queryset = DeviceRole.objects.prefetch_related('config_template', 'tags').annotate( queryset = DeviceRole.objects.all()
device_count=count_related(Device, 'role'),
virtualmachine_count=count_related(VirtualMachine, 'role')
)
serializer_class = serializers.DeviceRoleSerializer serializer_class = serializers.DeviceRoleSerializer
filterset_class = filtersets.DeviceRoleFilterSet filterset_class = filtersets.DeviceRoleFilterSet
@ -372,10 +341,7 @@ class DeviceRoleViewSet(NetBoxModelViewSet):
# #
class PlatformViewSet(NetBoxModelViewSet): class PlatformViewSet(NetBoxModelViewSet):
queryset = Platform.objects.prefetch_related('config_template', 'tags').annotate( queryset = Platform.objects.all()
device_count=count_related(Device, 'platform'),
virtualmachine_count=count_related(VirtualMachine, 'platform')
)
serializer_class = serializers.PlatformSerializer serializer_class = serializers.PlatformSerializer
filterset_class = filtersets.PlatformFilterSet filterset_class = filtersets.PlatformFilterSet
@ -391,8 +357,7 @@ class DeviceViewSet(
NetBoxModelViewSet NetBoxModelViewSet
): ):
queryset = Device.objects.prefetch_related( queryset = Device.objects.prefetch_related(
'device_type__manufacturer', 'role', 'tenant', 'platform', 'site', 'location', 'rack', 'parent_bay', 'parent_bay', # Referenced by DeviceSerializer.get_parent_device()
'virtual_chassis__master', 'primary_ip4__nat_outside', 'primary_ip6__nat_outside', 'config_template', 'tags',
) )
filterset_class = filtersets.DeviceFilterSet filterset_class = filtersets.DeviceFilterSet
pagination_class = StripCountAnnotationsPaginator pagination_class = StripCountAnnotationsPaginator
@ -419,19 +384,13 @@ class DeviceViewSet(
class VirtualDeviceContextViewSet(NetBoxModelViewSet): class VirtualDeviceContextViewSet(NetBoxModelViewSet):
queryset = VirtualDeviceContext.objects.prefetch_related( queryset = VirtualDeviceContext.objects.all()
'device__device_type', 'device', 'tenant', 'tags',
).annotate(
interface_count=count_related(Interface, 'vdcs'),
)
serializer_class = serializers.VirtualDeviceContextSerializer serializer_class = serializers.VirtualDeviceContextSerializer
filterset_class = filtersets.VirtualDeviceContextFilterSet filterset_class = filtersets.VirtualDeviceContextFilterSet
class ModuleViewSet(NetBoxModelViewSet): class ModuleViewSet(NetBoxModelViewSet):
queryset = Module.objects.prefetch_related( queryset = Module.objects.all()
'device', 'module_bay', 'module_type__manufacturer', 'tags',
)
serializer_class = serializers.ModuleSerializer serializer_class = serializers.ModuleSerializer
filterset_class = filtersets.ModuleFilterSet filterset_class = filtersets.ModuleFilterSet
@ -442,49 +401,45 @@ class ModuleViewSet(NetBoxModelViewSet):
class ConsolePortViewSet(PathEndpointMixin, NetBoxModelViewSet): class ConsolePortViewSet(PathEndpointMixin, NetBoxModelViewSet):
queryset = ConsolePort.objects.prefetch_related( queryset = ConsolePort.objects.prefetch_related(
'device', 'module__module_bay', '_path', 'cable__terminations', 'tags' '_path', 'cable__terminations',
) )
serializer_class = serializers.ConsolePortSerializer serializer_class = serializers.ConsolePortSerializer
filterset_class = filtersets.ConsolePortFilterSet filterset_class = filtersets.ConsolePortFilterSet
brief_prefetch_fields = ['device']
class ConsoleServerPortViewSet(PathEndpointMixin, NetBoxModelViewSet): class ConsoleServerPortViewSet(PathEndpointMixin, NetBoxModelViewSet):
queryset = ConsoleServerPort.objects.prefetch_related( queryset = ConsoleServerPort.objects.prefetch_related(
'device', 'module__module_bay', '_path', 'cable__terminations', 'tags' '_path', 'cable__terminations',
) )
serializer_class = serializers.ConsoleServerPortSerializer serializer_class = serializers.ConsoleServerPortSerializer
filterset_class = filtersets.ConsoleServerPortFilterSet filterset_class = filtersets.ConsoleServerPortFilterSet
brief_prefetch_fields = ['device']
class PowerPortViewSet(PathEndpointMixin, NetBoxModelViewSet): class PowerPortViewSet(PathEndpointMixin, NetBoxModelViewSet):
queryset = PowerPort.objects.prefetch_related( queryset = PowerPort.objects.prefetch_related(
'device', 'module__module_bay', '_path', 'cable__terminations', 'tags' '_path', 'cable__terminations',
) )
serializer_class = serializers.PowerPortSerializer serializer_class = serializers.PowerPortSerializer
filterset_class = filtersets.PowerPortFilterSet filterset_class = filtersets.PowerPortFilterSet
brief_prefetch_fields = ['device']
class PowerOutletViewSet(PathEndpointMixin, NetBoxModelViewSet): class PowerOutletViewSet(PathEndpointMixin, NetBoxModelViewSet):
queryset = PowerOutlet.objects.prefetch_related( queryset = PowerOutlet.objects.prefetch_related(
'device', 'module__module_bay', '_path', 'cable__terminations', 'tags' '_path', 'cable__terminations',
) )
serializer_class = serializers.PowerOutletSerializer serializer_class = serializers.PowerOutletSerializer
filterset_class = filtersets.PowerOutletFilterSet filterset_class = filtersets.PowerOutletFilterSet
brief_prefetch_fields = ['device']
class InterfaceViewSet(PathEndpointMixin, NetBoxModelViewSet): class InterfaceViewSet(PathEndpointMixin, NetBoxModelViewSet):
queryset = Interface.objects.prefetch_related( queryset = Interface.objects.prefetch_related(
'device', 'module__module_bay', 'parent', 'bridge', 'lag', '_path', 'cable__terminations', 'wireless_lans', '_path', 'cable__terminations',
'untagged_vlan', 'tagged_vlans', 'vrf', 'ip_addresses', 'fhrp_group_assignments', 'tags', 'l2vpn_terminations', 'l2vpn_terminations', # Referenced by InterfaceSerializer.l2vpn_termination
'vdcs', 'ip_addresses', # Referenced by Interface.count_ipaddresses()
'fhrp_group_assignments', # Referenced by Interface.count_fhrp_groups()
) )
serializer_class = serializers.InterfaceSerializer serializer_class = serializers.InterfaceSerializer
filterset_class = filtersets.InterfaceFilterSet filterset_class = filtersets.InterfaceFilterSet
brief_prefetch_fields = ['device']
def get_bulk_destroy_queryset(self): def get_bulk_destroy_queryset(self):
# Ensure child interfaces are deleted prior to their parents # Ensure child interfaces are deleted prior to their parents
@ -493,41 +448,36 @@ class InterfaceViewSet(PathEndpointMixin, NetBoxModelViewSet):
class FrontPortViewSet(PassThroughPortMixin, NetBoxModelViewSet): class FrontPortViewSet(PassThroughPortMixin, NetBoxModelViewSet):
queryset = FrontPort.objects.prefetch_related( queryset = FrontPort.objects.prefetch_related(
'device__device_type__manufacturer', 'module__module_bay', 'rear_port', 'cable__terminations', 'tags' 'cable__terminations',
) )
serializer_class = serializers.FrontPortSerializer serializer_class = serializers.FrontPortSerializer
filterset_class = filtersets.FrontPortFilterSet filterset_class = filtersets.FrontPortFilterSet
brief_prefetch_fields = ['device']
class RearPortViewSet(PassThroughPortMixin, NetBoxModelViewSet): class RearPortViewSet(PassThroughPortMixin, NetBoxModelViewSet):
queryset = RearPort.objects.prefetch_related( queryset = RearPort.objects.prefetch_related(
'device__device_type__manufacturer', 'module__module_bay', 'cable__terminations', 'tags' 'cable__terminations',
) )
serializer_class = serializers.RearPortSerializer serializer_class = serializers.RearPortSerializer
filterset_class = filtersets.RearPortFilterSet filterset_class = filtersets.RearPortFilterSet
brief_prefetch_fields = ['device']
class ModuleBayViewSet(NetBoxModelViewSet): class ModuleBayViewSet(NetBoxModelViewSet):
queryset = ModuleBay.objects.prefetch_related('tags', 'installed_module') queryset = ModuleBay.objects.all()
serializer_class = serializers.ModuleBaySerializer serializer_class = serializers.ModuleBaySerializer
filterset_class = filtersets.ModuleBayFilterSet filterset_class = filtersets.ModuleBayFilterSet
brief_prefetch_fields = ['device']
class DeviceBayViewSet(NetBoxModelViewSet): class DeviceBayViewSet(NetBoxModelViewSet):
queryset = DeviceBay.objects.prefetch_related('installed_device', 'tags') queryset = DeviceBay.objects.all()
serializer_class = serializers.DeviceBaySerializer serializer_class = serializers.DeviceBaySerializer
filterset_class = filtersets.DeviceBayFilterSet filterset_class = filtersets.DeviceBayFilterSet
brief_prefetch_fields = ['device']
class InventoryItemViewSet(MPTTLockedMixin, NetBoxModelViewSet): class InventoryItemViewSet(MPTTLockedMixin, NetBoxModelViewSet):
queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer', 'tags') queryset = InventoryItem.objects.all()
serializer_class = serializers.InventoryItemSerializer serializer_class = serializers.InventoryItemSerializer
filterset_class = filtersets.InventoryItemFilterSet filterset_class = filtersets.InventoryItemFilterSet
brief_prefetch_fields = ['device']
# #
@ -535,9 +485,7 @@ class InventoryItemViewSet(MPTTLockedMixin, NetBoxModelViewSet):
# #
class InventoryItemRoleViewSet(NetBoxModelViewSet): class InventoryItemRoleViewSet(NetBoxModelViewSet):
queryset = InventoryItemRole.objects.prefetch_related('tags').annotate( queryset = InventoryItemRole.objects.all()
inventoryitem_count=count_related(InventoryItem, 'role')
)
serializer_class = serializers.InventoryItemRoleSerializer serializer_class = serializers.InventoryItemRoleSerializer
filterset_class = filtersets.InventoryItemRoleFilterSet filterset_class = filtersets.InventoryItemRoleFilterSet
@ -554,7 +502,7 @@ class CableViewSet(NetBoxModelViewSet):
class CableTerminationViewSet(NetBoxModelViewSet): class CableTerminationViewSet(NetBoxModelViewSet):
metadata_class = ContentTypeMetadata metadata_class = ContentTypeMetadata
queryset = CableTermination.objects.prefetch_related('cable', 'termination') queryset = CableTermination.objects.all()
serializer_class = serializers.CableTerminationSerializer serializer_class = serializers.CableTerminationSerializer
filterset_class = filtersets.CableTerminationFilterSet filterset_class = filtersets.CableTerminationFilterSet
@ -564,10 +512,9 @@ class CableTerminationViewSet(NetBoxModelViewSet):
# #
class VirtualChassisViewSet(NetBoxModelViewSet): class VirtualChassisViewSet(NetBoxModelViewSet):
queryset = VirtualChassis.objects.prefetch_related('tags') queryset = VirtualChassis.objects.all()
serializer_class = serializers.VirtualChassisSerializer serializer_class = serializers.VirtualChassisSerializer
filterset_class = filtersets.VirtualChassisFilterSet filterset_class = filtersets.VirtualChassisFilterSet
brief_prefetch_fields = ['master']
# #
@ -575,11 +522,7 @@ class VirtualChassisViewSet(NetBoxModelViewSet):
# #
class PowerPanelViewSet(NetBoxModelViewSet): class PowerPanelViewSet(NetBoxModelViewSet):
queryset = PowerPanel.objects.prefetch_related( queryset = PowerPanel.objects.all()
'site', 'location'
).annotate(
powerfeed_count=count_related(PowerFeed, 'power_panel')
)
serializer_class = serializers.PowerPanelSerializer serializer_class = serializers.PowerPanelSerializer
filterset_class = filtersets.PowerPanelFilterSet filterset_class = filtersets.PowerPanelFilterSet
@ -590,7 +533,7 @@ class PowerPanelViewSet(NetBoxModelViewSet):
class PowerFeedViewSet(PathEndpointMixin, NetBoxModelViewSet): class PowerFeedViewSet(PathEndpointMixin, NetBoxModelViewSet):
queryset = PowerFeed.objects.prefetch_related( queryset = PowerFeed.objects.prefetch_related(
'power_panel', 'rack', '_path', 'cable__terminations', 'tags' '_path', 'cable__terminations',
) )
serializer_class = serializers.PowerFeedSerializer serializer_class = serializers.PowerFeedSerializer
filterset_class = filtersets.PowerFeedFilterSet filterset_class = filtersets.PowerFeedFilterSet

View File

@ -557,6 +557,9 @@ class DeviceBulkEditForm(NetBoxModelBulkEditForm):
label=_('Device type'), label=_('Device type'),
queryset=DeviceType.objects.all(), queryset=DeviceType.objects.all(),
required=False, required=False,
context={
'parent': 'manufacturer',
},
query_params={ query_params={
'manufacturer_id': '$manufacturer' 'manufacturer_id': '$manufacturer'
} }
@ -640,6 +643,9 @@ class ModuleBulkEditForm(NetBoxModelBulkEditForm):
required=False, required=False,
query_params={ query_params={
'manufacturer_id': '$manufacturer' 'manufacturer_id': '$manufacturer'
},
context={
'parent': 'manufacturer',
} }
) )
status = forms.ChoiceField( status = forms.ChoiceField(

View File

@ -30,7 +30,9 @@ def get_cable_form(a_type, b_type):
attrs[f'{cable_end}_terminations'] = DynamicModelMultipleChoiceField( attrs[f'{cable_end}_terminations'] = DynamicModelMultipleChoiceField(
queryset=term_cls.objects.all(), queryset=term_cls.objects.all(),
label=term_cls._meta.verbose_name.title(), label=term_cls._meta.verbose_name.title(),
disabled_indicator='_occupied', context={
'disabled': '_occupied',
},
query_params={ query_params={
'device_id': f'$termination_{cable_end}_device', 'device_id': f'$termination_{cable_end}_device',
'kind': 'physical', # Exclude virtual interfaces 'kind': 'physical', # Exclude virtual interfaces
@ -52,7 +54,9 @@ def get_cable_form(a_type, b_type):
attrs[f'{cable_end}_terminations'] = DynamicModelMultipleChoiceField( attrs[f'{cable_end}_terminations'] = DynamicModelMultipleChoiceField(
queryset=term_cls.objects.all(), queryset=term_cls.objects.all(),
label=_('Power Feed'), label=_('Power Feed'),
disabled_indicator='_occupied', context={
'disabled': '_occupied',
},
query_params={ query_params={
'power_panel_id': f'$termination_{cable_end}_powerpanel', 'power_panel_id': f'$termination_{cable_end}_powerpanel',
} }
@ -72,7 +76,9 @@ def get_cable_form(a_type, b_type):
attrs[f'{cable_end}_terminations'] = DynamicModelMultipleChoiceField( attrs[f'{cable_end}_terminations'] = DynamicModelMultipleChoiceField(
queryset=term_cls.objects.all(), queryset=term_cls.objects.all(),
label=_('Side'), label=_('Side'),
disabled_indicator='_occupied', context={
'disabled': '_occupied',
},
query_params={ query_params={
'circuit_id': f'$termination_{cable_end}_circuit', 'circuit_id': f'$termination_{cable_end}_circuit',
} }

View File

@ -426,7 +426,7 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
widget=APISelect( widget=APISelect(
api_url='/api/dcim/racks/{{rack}}/elevation/', api_url='/api/dcim/racks/{{rack}}/elevation/',
attrs={ attrs={
'disabled-indicator': 'device', 'ts-disabled-field': 'device',
'data-dynamic-params': '[{"fieldName":"face","queryParam":"face"}]' 'data-dynamic-params': '[{"fieldName":"face","queryParam":"face"}]'
}, },
) )
@ -434,6 +434,9 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
device_type = DynamicModelChoiceField( device_type = DynamicModelChoiceField(
label=_('Device type'), label=_('Device type'),
queryset=DeviceType.objects.all(), queryset=DeviceType.objects.all(),
context={
'parent': 'manufacturer',
},
selector=True selector=True
) )
role = DynamicModelChoiceField( role = DynamicModelChoiceField(
@ -461,6 +464,9 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
label=_('Virtual chassis'), label=_('Virtual chassis'),
queryset=VirtualChassis.objects.all(), queryset=VirtualChassis.objects.all(),
required=False, required=False,
context={
'parent': 'master',
},
selector=True selector=True
) )
vc_position = forms.IntegerField( vc_position = forms.IntegerField(
@ -568,6 +574,9 @@ class ModuleForm(ModuleCommonForm, NetBoxModelForm):
module_type = DynamicModelChoiceField( module_type = DynamicModelChoiceField(
label=_('Module type'), label=_('Module type'),
queryset=ModuleType.objects.all(), queryset=ModuleType.objects.all(),
context={
'parent': 'manufacturer',
},
selector=True selector=True
) )
comments = CommentField() comments = CommentField()
@ -774,7 +783,10 @@ class VCMemberSelectForm(forms.Form):
class ComponentTemplateForm(forms.ModelForm): class ComponentTemplateForm(forms.ModelForm):
device_type = DynamicModelChoiceField( device_type = DynamicModelChoiceField(
label=_('Device type'), label=_('Device type'),
queryset=DeviceType.objects.all() queryset=DeviceType.objects.all(),
context={
'parent': 'manufacturer',
}
) )
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -789,12 +801,18 @@ class ModularComponentTemplateForm(ComponentTemplateForm):
device_type = DynamicModelChoiceField( device_type = DynamicModelChoiceField(
label=_('Device type'), label=_('Device type'),
queryset=DeviceType.objects.all().all(), queryset=DeviceType.objects.all().all(),
required=False required=False,
context={
'parent': 'manufacturer',
}
) )
module_type = DynamicModelChoiceField( module_type = DynamicModelChoiceField(
label=_('Module type'), label=_('Module type'),
queryset=ModuleType.objects.all(), queryset=ModuleType.objects.all(),
required=False required=False,
context={
'parent': 'manufacturer',
}
) )
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):

View File

@ -233,7 +233,7 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='powerfeed', model_name='powerfeed',
name='rack', name='rack',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='dcim.rack'), field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='powerfeeds', to='dcim.rack'),
), ),
migrations.AddField( migrations.AddField(
model_name='powerfeed', model_name='powerfeed',

View File

@ -84,6 +84,7 @@ class PowerFeed(PrimaryModel, PathEndpoint, CabledObjectModel):
rack = models.ForeignKey( rack = models.ForeignKey(
to='Rack', to='Rack',
on_delete=models.PROTECT, on_delete=models.PROTECT,
related_name='powerfeeds',
blank=True, blank=True,
null=True null=True
) )

View File

@ -3,7 +3,6 @@ from django.core.exceptions import ObjectDoesNotExist
from drf_spectacular.types import OpenApiTypes from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers from rest_framework import serializers
from rest_framework.fields import ListField
from core.api.nested_serializers import NestedDataSourceSerializer, NestedDataFileSerializer, NestedJobSerializer from core.api.nested_serializers import NestedDataSourceSerializer, NestedDataFileSerializer, NestedJobSerializer
from core.api.serializers import JobSerializer from core.api.serializers import JobSerializer
@ -16,7 +15,7 @@ from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site
from extras.choices import * from extras.choices import *
from extras.models import * from extras.models import *
from netbox.api.exceptions import SerializerNotFound from netbox.api.exceptions import SerializerNotFound
from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField, SerializedPKRelatedField
from netbox.api.serializers import BaseModelSerializer, NetBoxModelSerializer, ValidatedModelSerializer from netbox.api.serializers import BaseModelSerializer, NetBoxModelSerializer, ValidatedModelSerializer
from netbox.api.serializers.features import TaggableModelSerializer from netbox.api.serializers.features import TaggableModelSerializer
from netbox.constants import NESTED_SERIALIZER_PREFIX from netbox.constants import NESTED_SERIALIZER_PREFIX
@ -285,7 +284,9 @@ class TagSerializer(ValidatedModelSerializer):
many=True, many=True,
required=False required=False
) )
tagged_items = serializers.IntegerField(read_only=True)
# Related object counts
tagged_items = RelatedObjectCountField('extras_taggeditem_items')
class Meta: class Meta:
model = Tag model = Tag

View File

@ -22,7 +22,7 @@ from netbox.api.metadata import ContentTypeMetadata
from netbox.api.renderers import TextRenderer from netbox.api.renderers import TextRenderer
from netbox.api.viewsets import NetBoxModelViewSet from netbox.api.viewsets import NetBoxModelViewSet
from utilities.exceptions import RQWorkerNotRunningException from utilities.exceptions import RQWorkerNotRunningException
from utilities.utils import copy_safe_request, count_related from utilities.utils import copy_safe_request
from . import serializers from . import serializers
from .mixins import ConfigTemplateRenderMixin from .mixins import ConfigTemplateRenderMixin
@ -114,7 +114,7 @@ class CustomLinkViewSet(NetBoxModelViewSet):
class ExportTemplateViewSet(SyncedDataMixin, NetBoxModelViewSet): class ExportTemplateViewSet(SyncedDataMixin, NetBoxModelViewSet):
metadata_class = ContentTypeMetadata metadata_class = ContentTypeMetadata
queryset = ExportTemplate.objects.prefetch_related('data_source', 'data_file') queryset = ExportTemplate.objects.all()
serializer_class = serializers.ExportTemplateSerializer serializer_class = serializers.ExportTemplateSerializer
filterset_class = filtersets.ExportTemplateFilterSet filterset_class = filtersets.ExportTemplateFilterSet
@ -146,9 +146,7 @@ class BookmarkViewSet(NetBoxModelViewSet):
# #
class TagViewSet(NetBoxModelViewSet): class TagViewSet(NetBoxModelViewSet):
queryset = Tag.objects.annotate( queryset = Tag.objects.all()
tagged_items=count_related(TaggedItem, 'tag')
)
serializer_class = serializers.TagSerializer serializer_class = serializers.TagSerializer
filterset_class = filtersets.TagFilterSet filterset_class = filtersets.TagFilterSet
@ -180,10 +178,7 @@ class JournalEntryViewSet(NetBoxModelViewSet):
# #
class ConfigContextViewSet(SyncedDataMixin, NetBoxModelViewSet): class ConfigContextViewSet(SyncedDataMixin, NetBoxModelViewSet):
queryset = ConfigContext.objects.prefetch_related( queryset = ConfigContext.objects.all()
'regions', 'site_groups', 'sites', 'locations', 'roles', 'platforms', 'tenant_groups', 'tenants', 'data_source',
'data_file',
)
serializer_class = serializers.ConfigContextSerializer serializer_class = serializers.ConfigContextSerializer
filterset_class = filtersets.ConfigContextFilterSet filterset_class = filtersets.ConfigContextFilterSet
@ -193,7 +188,7 @@ class ConfigContextViewSet(SyncedDataMixin, NetBoxModelViewSet):
# #
class ConfigTemplateViewSet(SyncedDataMixin, ConfigTemplateRenderMixin, NetBoxModelViewSet): class ConfigTemplateViewSet(SyncedDataMixin, ConfigTemplateRenderMixin, NetBoxModelViewSet):
queryset = ConfigTemplate.objects.prefetch_related('data_source', 'data_file') queryset = ConfigTemplate.objects.all()
serializer_class = serializers.ConfigTemplateSerializer serializer_class = serializers.ConfigTemplateSerializer
filterset_class = filtersets.ConfigTemplateFilterSet filterset_class = filtersets.ConfigTemplateFilterSet
@ -283,7 +278,7 @@ class ObjectChangeViewSet(ReadOnlyModelViewSet):
Retrieve a list of recent changes. Retrieve a list of recent changes.
""" """
metadata_class = ContentTypeMetadata metadata_class = ContentTypeMetadata
queryset = ObjectChange.objects.valid_models().prefetch_related('user') queryset = ObjectChange.objects.valid_models()
serializer_class = serializers.ObjectChangeSerializer serializer_class = serializers.ObjectChangeSerializer
filterset_class = filtersets.ObjectChangeFilterSet filterset_class = filtersets.ObjectChangeFilterSet

View File

@ -193,16 +193,19 @@ class ObjectVar(ScriptVariable):
:param model: The NetBox model being referenced :param model: The NetBox model being referenced
:param query_params: A dictionary of additional query parameters to attach when making REST API requests (optional) :param query_params: A dictionary of additional query parameters to attach when making REST API requests (optional)
:param context: A custom dictionary mapping template context variables to fields, used when rendering <option>
elements within the dropdown menu (optional)
:param null_option: The label to use as a "null" selection option (optional) :param null_option: The label to use as a "null" selection option (optional)
""" """
form_field = DynamicModelChoiceField form_field = DynamicModelChoiceField
def __init__(self, model, query_params=None, null_option=None, *args, **kwargs): def __init__(self, model, query_params=None, context=None, null_option=None, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.field_attrs.update({ self.field_attrs.update({
'queryset': model.objects.all(), 'queryset': model.objects.all(),
'query_params': query_params, 'query_params': query_params,
'context': context,
'null_option': null_option, 'null_option': null_option,
}) })

View File

@ -2,6 +2,7 @@ from drf_spectacular.utils import extend_schema_serializer
from rest_framework import serializers from rest_framework import serializers
from ipam import models from ipam import models
from netbox.api.fields import RelatedObjectCountField
from netbox.api.serializers import WritableNestedSerializer from netbox.api.serializers import WritableNestedSerializer
from .field_serializers import IPAddressField from .field_serializers import IPAddressField
@ -58,7 +59,7 @@ class NestedASNSerializer(WritableNestedSerializer):
) )
class NestedVRFSerializer(WritableNestedSerializer): class NestedVRFSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vrf-detail') url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vrf-detail')
prefix_count = serializers.IntegerField(read_only=True) prefix_count = RelatedObjectCountField('prefixes')
class Meta: class Meta:
model = models.VRF model = models.VRF
@ -86,7 +87,7 @@ class NestedRouteTargetSerializer(WritableNestedSerializer):
) )
class NestedRIRSerializer(WritableNestedSerializer): class NestedRIRSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail') url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail')
aggregate_count = serializers.IntegerField(read_only=True) aggregate_count = RelatedObjectCountField('aggregates')
class Meta: class Meta:
model = models.RIR model = models.RIR
@ -116,10 +117,11 @@ class NestedFHRPGroupSerializer(WritableNestedSerializer):
class NestedFHRPGroupAssignmentSerializer(WritableNestedSerializer): class NestedFHRPGroupAssignmentSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:fhrpgroupassignment-detail') url = serializers.HyperlinkedIdentityField(view_name='ipam-api:fhrpgroupassignment-detail')
group = NestedFHRPGroupSerializer()
class Meta: class Meta:
model = models.FHRPGroupAssignment model = models.FHRPGroupAssignment
fields = ['id', 'url', 'display', 'interface_type', 'interface_id', 'group_id', 'priority'] fields = ['id', 'url', 'display', 'group', 'interface_type', 'interface_id', 'priority']
# #
@ -131,8 +133,8 @@ class NestedFHRPGroupAssignmentSerializer(WritableNestedSerializer):
) )
class NestedRoleSerializer(WritableNestedSerializer): class NestedRoleSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:role-detail') url = serializers.HyperlinkedIdentityField(view_name='ipam-api:role-detail')
prefix_count = serializers.IntegerField(read_only=True) prefix_count = RelatedObjectCountField('prefixes')
vlan_count = serializers.IntegerField(read_only=True) vlan_count = RelatedObjectCountField('vlans')
class Meta: class Meta:
model = models.Role model = models.Role
@ -144,7 +146,7 @@ class NestedRoleSerializer(WritableNestedSerializer):
) )
class NestedVLANGroupSerializer(WritableNestedSerializer): class NestedVLANGroupSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail') url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail')
vlan_count = serializers.IntegerField(read_only=True) vlan_count = RelatedObjectCountField('vlans')
class Meta: class Meta:
model = models.VLANGroup model = models.VLANGroup

View File

@ -6,7 +6,7 @@ from dcim.api.nested_serializers import NestedDeviceSerializer, NestedSiteSerial
from ipam.choices import * from ipam.choices import *
from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS, VLANGROUP_SCOPE_TYPES from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS, VLANGROUP_SCOPE_TYPES
from ipam.models import * from ipam.models import *
from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField, SerializedPKRelatedField
from netbox.api.serializers import NetBoxModelSerializer from netbox.api.serializers import NetBoxModelSerializer
from netbox.constants import NESTED_SERIALIZER_PREFIX from netbox.constants import NESTED_SERIALIZER_PREFIX
from tenancy.api.nested_serializers import NestedTenantSerializer from tenancy.api.nested_serializers import NestedTenantSerializer
@ -43,8 +43,10 @@ class ASNSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asn-detail') url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asn-detail')
rir = NestedRIRSerializer(required=False, allow_null=True) rir = NestedRIRSerializer(required=False, allow_null=True)
tenant = NestedTenantSerializer(required=False, allow_null=True) tenant = NestedTenantSerializer(required=False, allow_null=True)
site_count = serializers.IntegerField(read_only=True)
provider_count = serializers.IntegerField(read_only=True) # Related object counts
site_count = RelatedObjectCountField('sites')
provider_count = RelatedObjectCountField('providers')
class Meta: class Meta:
model = ASN model = ASN
@ -90,8 +92,10 @@ class VRFSerializer(NetBoxModelSerializer):
required=False, required=False,
many=True many=True
) )
ipaddress_count = serializers.IntegerField(read_only=True)
prefix_count = serializers.IntegerField(read_only=True) # Related object counts
ipaddress_count = RelatedObjectCountField('ip_addresses')
prefix_count = RelatedObjectCountField('prefixes')
class Meta: class Meta:
model = VRF model = VRF
@ -124,7 +128,9 @@ class RouteTargetSerializer(NetBoxModelSerializer):
class RIRSerializer(NetBoxModelSerializer): class RIRSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail') url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail')
aggregate_count = serializers.IntegerField(read_only=True)
# Related object counts
aggregate_count = RelatedObjectCountField('aggregates')
class Meta: class Meta:
model = RIR model = RIR
@ -195,8 +201,10 @@ class FHRPGroupAssignmentSerializer(NetBoxModelSerializer):
class RoleSerializer(NetBoxModelSerializer): class RoleSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:role-detail') url = serializers.HyperlinkedIdentityField(view_name='ipam-api:role-detail')
prefix_count = serializers.IntegerField(read_only=True)
vlan_count = serializers.IntegerField(read_only=True) # Related object counts
prefix_count = RelatedObjectCountField('prefixes')
vlan_count = RelatedObjectCountField('vlans')
class Meta: class Meta:
model = Role model = Role
@ -218,9 +226,11 @@ class VLANGroupSerializer(NetBoxModelSerializer):
) )
scope_id = serializers.IntegerField(allow_null=True, required=False, default=None) scope_id = serializers.IntegerField(allow_null=True, required=False, default=None)
scope = serializers.SerializerMethodField(read_only=True) scope = serializers.SerializerMethodField(read_only=True)
vlan_count = serializers.IntegerField(read_only=True)
utilization = serializers.CharField(read_only=True) utilization = serializers.CharField(read_only=True)
# Related object counts
vlan_count = RelatedObjectCountField('vlans')
class Meta: class Meta:
model = VLANGroup model = VLANGroup
fields = [ fields = [
@ -247,7 +257,9 @@ class VLANSerializer(NetBoxModelSerializer):
status = ChoiceField(choices=VLANStatusChoices, required=False) status = ChoiceField(choices=VLANStatusChoices, required=False)
role = NestedRoleSerializer(required=False, allow_null=True) role = NestedRoleSerializer(required=False, allow_null=True)
l2vpn_termination = NestedL2VPNTerminationSerializer(read_only=True, allow_null=True) l2vpn_termination = NestedL2VPNTerminationSerializer(read_only=True, allow_null=True)
prefix_count = serializers.IntegerField(read_only=True)
# Related object counts
prefix_count = RelatedObjectCountField('prefixes')
class Meta: class Meta:
model = VLAN model = VLAN

View File

@ -12,8 +12,6 @@ from rest_framework.response import Response
from rest_framework.routers import APIRootView from rest_framework.routers import APIRootView
from rest_framework.views import APIView from rest_framework.views import APIView
from circuits.models import Provider
from dcim.models import Site
from ipam import filtersets from ipam import filtersets
from ipam.models import * from ipam.models import *
from ipam.utils import get_next_available_prefix from ipam.utils import get_next_available_prefix
@ -22,7 +20,6 @@ from netbox.api.viewsets.mixins import ObjectValidationMixin
from netbox.config import get_config from netbox.config import get_config
from netbox.constants import ADVISORY_LOCK_KEYS from netbox.constants import ADVISORY_LOCK_KEYS
from utilities.api import get_serializer_for_model from utilities.api import get_serializer_for_model
from utilities.utils import count_related
from . import serializers from . import serializers
@ -39,64 +36,49 @@ class IPAMRootView(APIRootView):
# #
class ASNRangeViewSet(NetBoxModelViewSet): class ASNRangeViewSet(NetBoxModelViewSet):
queryset = ASNRange.objects.prefetch_related('tenant', 'rir').all() queryset = ASNRange.objects.all()
serializer_class = serializers.ASNRangeSerializer serializer_class = serializers.ASNRangeSerializer
filterset_class = filtersets.ASNRangeFilterSet filterset_class = filtersets.ASNRangeFilterSet
class ASNViewSet(NetBoxModelViewSet): class ASNViewSet(NetBoxModelViewSet):
queryset = ASN.objects.prefetch_related('tenant', 'rir').annotate( queryset = ASN.objects.all()
site_count=count_related(Site, 'asns'),
provider_count=count_related(Provider, 'asns')
)
serializer_class = serializers.ASNSerializer serializer_class = serializers.ASNSerializer
filterset_class = filtersets.ASNFilterSet filterset_class = filtersets.ASNFilterSet
class VRFViewSet(NetBoxModelViewSet): class VRFViewSet(NetBoxModelViewSet):
queryset = VRF.objects.prefetch_related('tenant').prefetch_related( queryset = VRF.objects.all()
'import_targets', 'export_targets', 'tags'
).annotate(
ipaddress_count=count_related(IPAddress, 'vrf'),
prefix_count=count_related(Prefix, 'vrf')
)
serializer_class = serializers.VRFSerializer serializer_class = serializers.VRFSerializer
filterset_class = filtersets.VRFFilterSet filterset_class = filtersets.VRFFilterSet
class RouteTargetViewSet(NetBoxModelViewSet): class RouteTargetViewSet(NetBoxModelViewSet):
queryset = RouteTarget.objects.prefetch_related('tenant').prefetch_related('tags') queryset = RouteTarget.objects.all()
serializer_class = serializers.RouteTargetSerializer serializer_class = serializers.RouteTargetSerializer
filterset_class = filtersets.RouteTargetFilterSet filterset_class = filtersets.RouteTargetFilterSet
class RIRViewSet(NetBoxModelViewSet): class RIRViewSet(NetBoxModelViewSet):
queryset = RIR.objects.annotate( queryset = RIR.objects.all()
aggregate_count=count_related(Aggregate, 'rir')
).prefetch_related('tags')
serializer_class = serializers.RIRSerializer serializer_class = serializers.RIRSerializer
filterset_class = filtersets.RIRFilterSet filterset_class = filtersets.RIRFilterSet
class AggregateViewSet(NetBoxModelViewSet): class AggregateViewSet(NetBoxModelViewSet):
queryset = Aggregate.objects.prefetch_related('rir').prefetch_related('tags') queryset = Aggregate.objects.all()
serializer_class = serializers.AggregateSerializer serializer_class = serializers.AggregateSerializer
filterset_class = filtersets.AggregateFilterSet filterset_class = filtersets.AggregateFilterSet
class RoleViewSet(NetBoxModelViewSet): class RoleViewSet(NetBoxModelViewSet):
queryset = Role.objects.annotate( queryset = Role.objects.all()
prefix_count=count_related(Prefix, 'role'),
vlan_count=count_related(VLAN, 'role')
).prefetch_related('tags')
serializer_class = serializers.RoleSerializer serializer_class = serializers.RoleSerializer
filterset_class = filtersets.RoleFilterSet filterset_class = filtersets.RoleFilterSet
class PrefixViewSet(NetBoxModelViewSet): class PrefixViewSet(NetBoxModelViewSet):
queryset = Prefix.objects.prefetch_related( queryset = Prefix.objects.all()
'site', 'vrf__tenant', 'tenant', 'vlan', 'role', 'tags'
)
serializer_class = serializers.PrefixSerializer serializer_class = serializers.PrefixSerializer
filterset_class = filtersets.PrefixFilterSet filterset_class = filtersets.PrefixFilterSet
@ -109,7 +91,7 @@ class PrefixViewSet(NetBoxModelViewSet):
class IPRangeViewSet(NetBoxModelViewSet): class IPRangeViewSet(NetBoxModelViewSet):
queryset = IPRange.objects.prefetch_related('vrf', 'role', 'tenant', 'tags') queryset = IPRange.objects.all()
serializer_class = serializers.IPRangeSerializer serializer_class = serializers.IPRangeSerializer
filterset_class = filtersets.IPRangeFilterSet filterset_class = filtersets.IPRangeFilterSet
@ -117,9 +99,7 @@ class IPRangeViewSet(NetBoxModelViewSet):
class IPAddressViewSet(NetBoxModelViewSet): class IPAddressViewSet(NetBoxModelViewSet):
queryset = IPAddress.objects.prefetch_related( queryset = IPAddress.objects.all()
'vrf__tenant', 'tenant', 'nat_inside', 'nat_outside', 'tags', 'assigned_object'
)
serializer_class = serializers.IPAddressSerializer serializer_class = serializers.IPAddressSerializer
filterset_class = filtersets.IPAddressFilterSet filterset_class = filtersets.IPAddressFilterSet
@ -137,44 +117,39 @@ class IPAddressViewSet(NetBoxModelViewSet):
class FHRPGroupViewSet(NetBoxModelViewSet): class FHRPGroupViewSet(NetBoxModelViewSet):
queryset = FHRPGroup.objects.prefetch_related('ip_addresses', 'tags') queryset = FHRPGroup.objects.all()
serializer_class = serializers.FHRPGroupSerializer serializer_class = serializers.FHRPGroupSerializer
filterset_class = filtersets.FHRPGroupFilterSet filterset_class = filtersets.FHRPGroupFilterSet
brief_prefetch_fields = ('ip_addresses',)
class FHRPGroupAssignmentViewSet(NetBoxModelViewSet): class FHRPGroupAssignmentViewSet(NetBoxModelViewSet):
queryset = FHRPGroupAssignment.objects.prefetch_related('group', 'interface') queryset = FHRPGroupAssignment.objects.all()
serializer_class = serializers.FHRPGroupAssignmentSerializer serializer_class = serializers.FHRPGroupAssignmentSerializer
filterset_class = filtersets.FHRPGroupAssignmentFilterSet filterset_class = filtersets.FHRPGroupAssignmentFilterSet
class VLANGroupViewSet(NetBoxModelViewSet): class VLANGroupViewSet(NetBoxModelViewSet):
queryset = VLANGroup.objects.annotate_utilization().prefetch_related('tags') queryset = VLANGroup.objects.annotate_utilization()
serializer_class = serializers.VLANGroupSerializer serializer_class = serializers.VLANGroupSerializer
filterset_class = filtersets.VLANGroupFilterSet filterset_class = filtersets.VLANGroupFilterSet
class VLANViewSet(NetBoxModelViewSet): class VLANViewSet(NetBoxModelViewSet):
queryset = VLAN.objects.prefetch_related( queryset = VLAN.objects.prefetch_related(
'site', 'group', 'tenant', 'role', 'tags' 'l2vpn_terminations', # Referenced by VLANSerializer.l2vpn_termination
).annotate(
prefix_count=count_related(Prefix, 'vlan')
) )
serializer_class = serializers.VLANSerializer serializer_class = serializers.VLANSerializer
filterset_class = filtersets.VLANFilterSet filterset_class = filtersets.VLANFilterSet
class ServiceTemplateViewSet(NetBoxModelViewSet): class ServiceTemplateViewSet(NetBoxModelViewSet):
queryset = ServiceTemplate.objects.prefetch_related('tags') queryset = ServiceTemplate.objects.all()
serializer_class = serializers.ServiceTemplateSerializer serializer_class = serializers.ServiceTemplateSerializer
filterset_class = filtersets.ServiceTemplateFilterSet filterset_class = filtersets.ServiceTemplateFilterSet
class ServiceViewSet(NetBoxModelViewSet): class ServiceViewSet(NetBoxModelViewSet):
queryset = Service.objects.prefetch_related( queryset = Service.objects.all()
'device', 'virtual_machine', 'tags', 'ipaddresses'
)
serializer_class = serializers.ServiceSerializer serializer_class = serializers.ServiceSerializer
filterset_class = filtersets.ServiceFilterSet filterset_class = filtersets.ServiceFilterSet

View File

@ -267,14 +267,20 @@ class IPRangeForm(TenancyForm, NetBoxModelForm):
class IPAddressForm(TenancyForm, NetBoxModelForm): class IPAddressForm(TenancyForm, NetBoxModelForm):
interface = DynamicModelChoiceField( interface = DynamicModelChoiceField(
label=_('Interface'),
queryset=Interface.objects.all(), queryset=Interface.objects.all(),
required=False, required=False,
context={
'parent': 'device',
},
selector=True, selector=True,
label=_('Interface'),
) )
vminterface = DynamicModelChoiceField( vminterface = DynamicModelChoiceField(
queryset=VMInterface.objects.all(), queryset=VMInterface.objects.all(),
required=False, required=False,
context={
'parent': 'virtual_machine',
},
selector=True, selector=True,
label=_('Interface'), label=_('Interface'),
) )

View File

@ -760,7 +760,7 @@ class FHRPGroupTest(APIViewTestCases.APIViewTestCase):
class FHRPGroupAssignmentTest(APIViewTestCases.APIViewTestCase): class FHRPGroupAssignmentTest(APIViewTestCases.APIViewTestCase):
model = FHRPGroupAssignment model = FHRPGroupAssignment
brief_fields = ['display', 'group_id', 'id', 'interface_id', 'interface_type', 'priority', 'url'] brief_fields = ['display', 'group', 'id', 'interface_id', 'interface_type', 'priority', 'url']
bulk_update_data = { bulk_update_data = {
'priority': 100, 'priority': 100,
} }

View File

@ -1,6 +1,6 @@
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from drf_spectacular.utils import extend_schema_field
from drf_spectacular.types import OpenApiTypes from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field
from netaddr import IPNetwork from netaddr import IPNetwork
from rest_framework import serializers from rest_framework import serializers
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
@ -10,6 +10,7 @@ __all__ = (
'ChoiceField', 'ChoiceField',
'ContentTypeField', 'ContentTypeField',
'IPNetworkSerializer', 'IPNetworkSerializer',
'RelatedObjectCountField',
'SerializedPKRelatedField', 'SerializedPKRelatedField',
) )
@ -135,3 +136,16 @@ class SerializedPKRelatedField(PrimaryKeyRelatedField):
def to_representation(self, value): def to_representation(self, value):
return self.serializer(value, context={'request': self.context['request']}).data return self.serializer(value, context={'request': self.context['request']}).data
@extend_schema_field(OpenApiTypes.INT64)
class RelatedObjectCountField(serializers.ReadOnlyField):
"""
Represents a read-only integer count of related objects (e.g. the number of racks assigned to a site). This field
is detected by get_annotations_for_serializer() when determining the annotations to be added to a queryset
depending on the serializer fields selected for inclusion in the response.
"""
def __init__(self, relation, **kwargs):
self.relation = relation
super().__init__(**kwargs)

View File

@ -12,6 +12,15 @@ __all__ = (
class BaseModelSerializer(serializers.ModelSerializer): class BaseModelSerializer(serializers.ModelSerializer):
display = serializers.SerializerMethodField(read_only=True) display = serializers.SerializerMethodField(read_only=True)
def __init__(self, *args, requested_fields=None, **kwargs):
super().__init__(*args, **kwargs)
# If specific fields have been requested, omit the others
if requested_fields:
for field in list(self.fields.keys()):
if field not in requested_fields:
self.fields.pop(field)
@extend_schema_field(OpenApiTypes.STR) @extend_schema_field(OpenApiTypes.STR)
def get_display(self, obj): def get_display(self, obj):
return str(obj) return str(obj)

View File

@ -1,4 +1,5 @@
import logging import logging
from functools import cached_property
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.db import transaction from django.db import transaction
@ -9,6 +10,7 @@ from rest_framework import mixins as drf_mixins
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet from rest_framework.viewsets import GenericViewSet
from utilities.api import get_annotations_for_serializer, get_prefetches_for_serializer
from utilities.exceptions import AbortRequest from utilities.exceptions import AbortRequest
from . import mixins from . import mixins
@ -40,6 +42,33 @@ class BaseViewSet(GenericViewSet):
if action := HTTP_ACTIONS[request.method]: if action := HTTP_ACTIONS[request.method]:
self.queryset = self.queryset.restrict(request.user, action) self.queryset = self.queryset.restrict(request.user, action)
def get_queryset(self):
qs = super().get_queryset()
serializer_class = self.get_serializer_class()
# Dynamically resolve prefetches for included serializer fields and attach them to the queryset
if prefetch := get_prefetches_for_serializer(serializer_class, fields_to_include=self.requested_fields):
qs = qs.prefetch_related(*prefetch)
# Dynamically resolve annotations for RelatedObjectCountFields on the serializer and attach them to the queryset
if annotations := get_annotations_for_serializer(serializer_class, fields_to_include=self.requested_fields):
qs = qs.annotate(**annotations)
return qs
def get_serializer(self, *args, **kwargs):
# If specific fields have been requested, pass them to the serializer
if self.requested_fields:
kwargs['requested_fields'] = self.requested_fields
return super().get_serializer(*args, **kwargs)
@cached_property
def requested_fields(self):
requested_fields = self.request.query_params.get('fields')
return requested_fields.split(',') if requested_fields else []
class NetBoxReadOnlyModelViewSet( class NetBoxReadOnlyModelViewSet(
mixins.BriefModeMixin, mixins.BriefModeMixin,

View File

@ -30,7 +30,6 @@ class BriefModeMixin:
GET /api/dcim/sites/?brief=True GET /api/dcim/sites/?brief=True
""" """
brief = False brief = False
brief_prefetch_fields = []
def initialize_request(self, request, *args, **kwargs): def initialize_request(self, request, *args, **kwargs):
# Annotate whether brief mode is active # Annotate whether brief mode is active
@ -53,22 +52,6 @@ class BriefModeMixin:
return self.serializer_class return self.serializer_class
def get_queryset(self):
qs = super().get_queryset()
if self.brief:
serializer_class = self.get_serializer_class()
# Clear any annotations for fields not present on the nested serializer
for annotation in list(qs.query.annotations.keys()):
if annotation not in serializer_class().fields:
qs.query.annotations.pop(annotation)
# Clear any prefetches from the queryset and append only brief_prefetch_fields (if any)
return qs.prefetch_related(None).prefetch_related(*self.brief_prefetch_fields)
return qs
class CustomFieldsMixin: class CustomFieldsMixin:
""" """

Binary file not shown.

Binary file not shown.

View File

@ -31,6 +31,15 @@ export class DynamicTomSelect extends TomSelect {
// Glean the REST API endpoint URL from the <select> element // Glean the REST API endpoint URL from the <select> element
this.api_url = this.input.getAttribute('data-url') as string; this.api_url = this.input.getAttribute('data-url') as string;
// Override any field names set as widget attributes
this.valueField = this.input.getAttribute('ts-value-field') || this.settings.valueField;
this.labelField = this.input.getAttribute('ts-label-field') || this.settings.labelField;
this.disabledField = this.input.getAttribute('ts-disabled-field') || this.settings.disabledField;
this.descriptionField = this.input.getAttribute('ts-description-field') || 'description';
this.depthField = this.input.getAttribute('ts-depth-field') || '_depth';
this.parentField = this.input.getAttribute('ts-parent-field') || null;
this.countField = this.input.getAttribute('ts-count-field') || null;
// Set the null option (if any) // Set the null option (if any)
const nullOption = this.input.getAttribute('data-null-option'); const nullOption = this.input.getAttribute('data-null-option');
if (nullOption) { if (nullOption) {
@ -82,8 +91,18 @@ export class DynamicTomSelect extends TomSelect {
// Make the API request // Make the API request
fetch(url) fetch(url)
.then(response => response.json()) .then(response => response.json())
.then(json => { .then(apiData => {
self.loadCallback(json.results, []); const results: Dict[] = apiData.results;
let options: Dict[] = []
for (let result of results) {
const option = self.getOptionFromData(result);
options.push(option);
}
return options;
})
// Pass the options to the callback function
.then(options => {
self.loadCallback(options, []);
}).catch(()=>{ }).catch(()=>{
self.loadCallback([], []); self.loadCallback([], []);
}); });
@ -126,6 +145,27 @@ export class DynamicTomSelect extends TomSelect {
return queryString.stringifyUrl({ url, query }); return queryString.stringifyUrl({ url, query });
} }
// Compile TomOption data from an API result
getOptionFromData(data: Dict) {
let option: Dict = {
id: data[this.valueField],
display: data[this.labelField],
depth: data[this.depthField] || null,
description: data[this.descriptionField] || null,
};
if (data[this.parentField]) {
let parent: Dict = data[this.parentField] as Dict;
option['parent'] = parent[this.labelField];
}
if (data[this.countField]) {
option['count'] = data[this.countField];
}
if (data[this.disabledField]) {
option['disabled'] = data[this.disabledField];
}
return option
}
/** /**
* Transitional methods * Transitional methods
*/ */

View File

@ -10,12 +10,34 @@ const MAX_OPTIONS = 100;
// Render the HTML for a dropdown option // Render the HTML for a dropdown option
function renderOption(data: TomOption, escape: typeof escape_html) { function renderOption(data: TomOption, escape: typeof escape_html) {
// If the option has a `_depth` property, indent its label let html = '<div>';
if (typeof data._depth === 'number' && data._depth > 0) {
return `<div>${'─'.repeat(data._depth)} ${escape(data[LABEL_FIELD])}</div>`; // If the option has a `depth` property, indent its label
if (typeof data.depth === 'number' && data.depth > 0) {
html = `${html}${'─'.repeat(data.depth)} `;
} }
return `<div>${escape(data[LABEL_FIELD])}</div>`; html = `${html}${escape(data[LABEL_FIELD])}`;
if (data['parent']) {
html = `${html} <span class="text-secondary">${escape(data['parent'])}</span>`;
}
if (data['count']) {
html = `${html} <span class="badge">${escape(data['count'])}</span>`;
}
if (data['description']) {
html = `${html}<br /><small class="text-secondary">${escape(data['description'])}</small>`;
}
html = `${html}</div>`;
return html;
}
// Render the HTML for a selected item
function renderItem(data: TomOption, escape: typeof escape_html) {
if (data['parent']) {
return `<div>${escape(data['parent'])} > ${escape(data[LABEL_FIELD])}</div>`;
}
return `<div>${escape(data[LABEL_FIELD])}<div>`;
} }
// Initialize <select> elements which are populated via a REST API call // Initialize <select> elements which are populated via a REST API call
@ -30,16 +52,13 @@ export function initDynamicSelects(): void {
// Disable local search (search is performed on the backend) // Disable local search (search is performed on the backend)
searchField: [], searchField: [],
// Reference the disabled-indicator attr on the <select> element to determine
// the name of the attribute which indicates whether an option should be disabled
disabledField: select.getAttribute('disabled-indicator') || undefined,
// Load options from API immediately on focus // Load options from API immediately on focus
preload: 'focus', preload: 'focus',
// Define custom rendering functions // Define custom rendering functions
render: { render: {
option: renderOption, option: renderOption,
item: renderItem,
}, },
// By default, load() will be called only if query.length > 0 // By default, load() will be called only if query.length > 0

View File

@ -17,13 +17,18 @@ export function initStaticSelects(): void {
// Initialize color selection fields // Initialize color selection fields
export function initColorSelects(): void { export function initColorSelects(): void {
function renderColor(item: TomOption, escape: typeof escape_html) {
return `<div><span class="dropdown-item-indicator color-label" style="background-color: #${escape(
item.value,
)}"></span> ${escape(item.text)}</div>`;
}
for (const select of getElements<HTMLSelectElement>('select.color-select')) { for (const select of getElements<HTMLSelectElement>('select.color-select')) {
new TomSelect(select, { new TomSelect(select, {
...config, ...config,
render: { render: {
option: function (item: TomOption, escape: typeof escape_html) { option: renderColor,
return `<div style="background-color: #${escape(item.value)}">${escape(item.text)}</div>`; item: renderColor,
},
}, },
}); });
} }

View File

@ -3,7 +3,7 @@ from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers from rest_framework import serializers
from netbox.api.fields import ChoiceField, ContentTypeField from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField
from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer
from netbox.constants import NESTED_SERIALIZER_PREFIX from netbox.constants import NESTED_SERIALIZER_PREFIX
from tenancy.choices import ContactPriorityChoices from tenancy.choices import ContactPriorityChoices
@ -32,16 +32,18 @@ class TenantGroupSerializer(NestedGroupModelSerializer):
class TenantSerializer(NetBoxModelSerializer): class TenantSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenant-detail') url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenant-detail')
group = NestedTenantGroupSerializer(required=False, allow_null=True) group = NestedTenantGroupSerializer(required=False, allow_null=True)
circuit_count = serializers.IntegerField(read_only=True)
device_count = serializers.IntegerField(read_only=True) # Related object counts
ipaddress_count = serializers.IntegerField(read_only=True) circuit_count = RelatedObjectCountField('circuits')
prefix_count = serializers.IntegerField(read_only=True) device_count = RelatedObjectCountField('devices')
rack_count = serializers.IntegerField(read_only=True) rack_count = RelatedObjectCountField('racks')
site_count = serializers.IntegerField(read_only=True) site_count = RelatedObjectCountField('sites')
virtualmachine_count = serializers.IntegerField(read_only=True) ipaddress_count = RelatedObjectCountField('ip_addresses')
vlan_count = serializers.IntegerField(read_only=True) prefix_count = RelatedObjectCountField('prefixes')
vrf_count = serializers.IntegerField(read_only=True) vlan_count = RelatedObjectCountField('vlans')
cluster_count = serializers.IntegerField(read_only=True) vrf_count = RelatedObjectCountField('vrfs')
virtualmachine_count = RelatedObjectCountField('virtual_machines')
cluster_count = RelatedObjectCountField('clusters')
class Meta: class Meta:
model = Tenant model = Tenant

View File

@ -1,13 +1,8 @@
from rest_framework.routers import APIRootView from rest_framework.routers import APIRootView
from circuits.models import Circuit
from dcim.models import Device, Rack, Site
from ipam.models import IPAddress, Prefix, VLAN, VRF
from netbox.api.viewsets import NetBoxModelViewSet, MPTTLockedMixin from netbox.api.viewsets import NetBoxModelViewSet, MPTTLockedMixin
from tenancy import filtersets from tenancy import filtersets
from tenancy.models import * from tenancy.models import *
from utilities.utils import count_related
from virtualization.models import VirtualMachine, Cluster
from . import serializers from . import serializers
@ -30,26 +25,13 @@ class TenantGroupViewSet(MPTTLockedMixin, NetBoxModelViewSet):
'group', 'group',
'tenant_count', 'tenant_count',
cumulative=True cumulative=True
).prefetch_related('tags') )
serializer_class = serializers.TenantGroupSerializer serializer_class = serializers.TenantGroupSerializer
filterset_class = filtersets.TenantGroupFilterSet filterset_class = filtersets.TenantGroupFilterSet
class TenantViewSet(NetBoxModelViewSet): class TenantViewSet(NetBoxModelViewSet):
queryset = Tenant.objects.prefetch_related( queryset = Tenant.objects.all()
'group', 'tags'
).annotate(
circuit_count=count_related(Circuit, 'tenant'),
device_count=count_related(Device, 'tenant'),
ipaddress_count=count_related(IPAddress, 'tenant'),
prefix_count=count_related(Prefix, 'tenant'),
rack_count=count_related(Rack, 'tenant'),
site_count=count_related(Site, 'tenant'),
virtualmachine_count=count_related(VirtualMachine, 'tenant'),
vlan_count=count_related(VLAN, 'tenant'),
vrf_count=count_related(VRF, 'tenant'),
cluster_count=count_related(Cluster, 'tenant')
)
serializer_class = serializers.TenantSerializer serializer_class = serializers.TenantSerializer
filterset_class = filtersets.TenantFilterSet filterset_class = filtersets.TenantFilterSet
@ -65,24 +47,24 @@ class ContactGroupViewSet(MPTTLockedMixin, NetBoxModelViewSet):
'group', 'group',
'contact_count', 'contact_count',
cumulative=True cumulative=True
).prefetch_related('tags') )
serializer_class = serializers.ContactGroupSerializer serializer_class = serializers.ContactGroupSerializer
filterset_class = filtersets.ContactGroupFilterSet filterset_class = filtersets.ContactGroupFilterSet
class ContactRoleViewSet(NetBoxModelViewSet): class ContactRoleViewSet(NetBoxModelViewSet):
queryset = ContactRole.objects.prefetch_related('tags') queryset = ContactRole.objects.all()
serializer_class = serializers.ContactRoleSerializer serializer_class = serializers.ContactRoleSerializer
filterset_class = filtersets.ContactRoleFilterSet filterset_class = filtersets.ContactRoleFilterSet
class ContactViewSet(NetBoxModelViewSet): class ContactViewSet(NetBoxModelViewSet):
queryset = Contact.objects.prefetch_related('group', 'tags') queryset = Contact.objects.all()
serializer_class = serializers.ContactSerializer serializer_class = serializers.ContactSerializer
filterset_class = filtersets.ContactFilterSet filterset_class = filtersets.ContactFilterSet
class ContactAssignmentViewSet(NetBoxModelViewSet): class ContactAssignmentViewSet(NetBoxModelViewSet):
queryset = ContactAssignment.objects.prefetch_related('content_type', 'object', 'contact', 'role', 'tags') queryset = ContactAssignment.objects.all()
serializer_class = serializers.ContactAssignmentSerializer serializer_class = serializers.ContactAssignmentSerializer
filterset_class = filtersets.ContactAssignmentFilterSet filterset_class = filtersets.ContactAssignmentFilterSet

View File

@ -34,7 +34,7 @@ class UsersRootView(APIRootView):
# #
class UserViewSet(NetBoxModelViewSet): class UserViewSet(NetBoxModelViewSet):
queryset = RestrictedQuerySet(model=get_user_model()).prefetch_related('groups').order_by('username') queryset = RestrictedQuerySet(model=get_user_model()).order_by('username')
serializer_class = serializers.UserSerializer serializer_class = serializers.UserSerializer
filterset_class = filtersets.UserFilterSet filterset_class = filtersets.UserFilterSet
@ -50,7 +50,7 @@ class GroupViewSet(NetBoxModelViewSet):
# #
class TokenViewSet(NetBoxModelViewSet): class TokenViewSet(NetBoxModelViewSet):
queryset = Token.objects.prefetch_related('user') queryset = Token.objects.all()
serializer_class = serializers.TokenSerializer serializer_class = serializers.TokenSerializer
filterset_class = filtersets.TokenFilterSet filterset_class = filtersets.TokenFilterSet
@ -86,7 +86,7 @@ class TokenProvisionView(APIView):
# #
class ObjectPermissionViewSet(NetBoxModelViewSet): class ObjectPermissionViewSet(NetBoxModelViewSet):
queryset = ObjectPermission.objects.prefetch_related('object_types', 'groups', 'users') queryset = ObjectPermission.objects.all()
serializer_class = serializers.ObjectPermissionSerializer serializer_class = serializers.ObjectPermissionSerializer
filterset_class = filtersets.ObjectPermissionFilterSet filterset_class = filtersets.ObjectPermissionFilterSet

View File

@ -2,16 +2,24 @@ import platform
import sys import sys
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey
from django.core.exceptions import FieldDoesNotExist
from django.db.models.fields.related import ManyToOneRel, RelatedField
from django.http import JsonResponse from django.http import JsonResponse
from django.urls import reverse from django.urls import reverse
from rest_framework import status from rest_framework import status
from rest_framework.serializers import Serializer
from rest_framework.utils import formatting from rest_framework.utils import formatting
from netbox.api.fields import RelatedObjectCountField
from netbox.api.exceptions import GraphQLTypeNotFound, SerializerNotFound from netbox.api.exceptions import GraphQLTypeNotFound, SerializerNotFound
from utilities.utils import count_related
from .utils import dynamic_import from .utils import dynamic_import
__all__ = ( __all__ = (
'get_annotations_for_serializer',
'get_graphql_type_for_model', 'get_graphql_type_for_model',
'get_prefetches_for_serializer',
'get_serializer_for_model', 'get_serializer_for_model',
'get_view_name', 'get_view_name',
'is_api_request', 'is_api_request',
@ -89,6 +97,63 @@ def get_view_name(view, suffix=None):
return name return name
def get_prefetches_for_serializer(serializer_class, fields_to_include=None):
"""
Compile and return a list of fields which should be prefetched on the queryset for a serializer.
"""
model = serializer_class.Meta.model
# If specific fields are not specified, default to all
if not fields_to_include:
fields_to_include = serializer_class.Meta.fields
prefetch_fields = []
for field_name in fields_to_include:
serializer_field = serializer_class._declared_fields.get(field_name)
# Determine the name of the model field referenced by the serializer field
model_field_name = field_name
if serializer_field and serializer_field.source:
model_field_name = serializer_field.source
# If the serializer field does not map to a discrete model field, skip it.
try:
field = model._meta.get_field(model_field_name)
if isinstance(field, (RelatedField, ManyToOneRel, GenericForeignKey)):
prefetch_fields.append(field.name)
except FieldDoesNotExist:
continue
# If this field is represented by a nested serializer, recurse to resolve prefetches
# for the related object.
if serializer_field:
if issubclass(type(serializer_field), Serializer):
for subfield in get_prefetches_for_serializer(type(serializer_field)):
prefetch_fields.append(f'{field_name}__{subfield}')
return prefetch_fields
def get_annotations_for_serializer(serializer_class, fields_to_include=None):
"""
Return a mapping of field names to annotations to be applied to the queryset for a serializer.
"""
annotations = {}
# If specific fields are not specified, default to all
if not fields_to_include:
fields_to_include = serializer_class.Meta.fields
model = serializer_class.Meta.model
for field_name, field in serializer_class._declared_fields.items():
if field_name in fields_to_include and type(field) is RelatedObjectCountField:
related_field = model._meta.get_field(field.relation).field
annotations[field_name] = count_related(related_field.model, related_field.name)
return annotations
def rest_api_server_error(request, *args, **kwargs): def rest_api_server_error(request, *args, **kwargs):
""" """
Handle exceptions and return a useful error message for REST API requests. Handle exceptions and return a useful error message for REST API requests.

View File

@ -63,8 +63,19 @@ class DynamicModelChoiceMixin:
initial_params: A dictionary of child field references to use for selecting a parent field's initial value initial_params: A dictionary of child field references to use for selecting a parent field's initial value
null_option: The string used to represent a null selection (if any) null_option: The string used to represent a null selection (if any)
disabled_indicator: The name of the field which, if populated, will disable selection of the disabled_indicator: The name of the field which, if populated, will disable selection of the
choice (optional) choice (DEPRECATED: pass `context={'disabled': '$fieldname'}` instead)
context: A mapping of <option> template variables to their API data keys (optional; see below)
selector: Include an advanced object selection widget to assist the user in identifying the desired object selector: Include an advanced object selection widget to assist the user in identifying the desired object
Context keys:
value: The name of the attribute which contains the option's value (default: 'id')
label: The name of the attribute used as the option's human-friendly label (default: 'display')
description: The name of the attribute to use as a description (default: 'description')
depth: The name of the attribute which indicates an object's depth within a recursive hierarchy; must be a
positive integer (default: '_depth')
disabled: The name of the attribute which, if true, signifies that the option should be disabled
parent: The name of the attribute which represents the object's parent object (e.g. device for an interface)
count: The name of the attribute which contains a numeric count of related objects
""" """
filter = django_filters.ModelChoiceFilter filter = django_filters.ModelChoiceFilter
widget = widgets.APISelect widget = widgets.APISelect
@ -77,6 +88,7 @@ class DynamicModelChoiceMixin:
initial_params=None, initial_params=None,
null_option=None, null_option=None,
disabled_indicator=None, disabled_indicator=None,
context=None,
selector=False, selector=False,
**kwargs **kwargs
): ):
@ -85,6 +97,7 @@ class DynamicModelChoiceMixin:
self.initial_params = initial_params or {} self.initial_params = initial_params or {}
self.null_option = null_option self.null_option = null_option
self.disabled_indicator = disabled_indicator self.disabled_indicator = disabled_indicator
self.context = context or {}
self.selector = selector self.selector = selector
super().__init__(queryset, **kwargs) super().__init__(queryset, **kwargs)
@ -96,12 +109,17 @@ class DynamicModelChoiceMixin:
if self.null_option is not None: if self.null_option is not None:
attrs['data-null-option'] = self.null_option attrs['data-null-option'] = self.null_option
# Set the disabled indicator, if any # Set any custom template attributes for TomSelect
for var, accessor in self.context.items():
attrs[f'ts-{var}-field'] = accessor
# TODO: Remove in v4.1
# Legacy means of specifying the disabled indicator
if self.disabled_indicator is not None: if self.disabled_indicator is not None:
attrs['disabled-indicator'] = self.disabled_indicator attrs['ts-disabled-field'] = self.disabled_indicator
# Attach any static query parameters # Attach any static query parameters
if (len(self.query_params) > 0): if len(self.query_params) > 0:
widget.add_query_params(self.query_params) widget.add_query_params(self.query_params)
# Include object selector? # Include object selector?

View File

@ -1,6 +1,7 @@
from drf_spectacular.utils import extend_schema_serializer from drf_spectacular.utils import extend_schema_serializer
from rest_framework import serializers from rest_framework import serializers
from netbox.api.fields import RelatedObjectCountField
from netbox.api.serializers import WritableNestedSerializer from netbox.api.serializers import WritableNestedSerializer
from virtualization.models import * from virtualization.models import *
@ -23,7 +24,7 @@ __all__ = [
) )
class NestedClusterTypeSerializer(WritableNestedSerializer): class NestedClusterTypeSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustertype-detail') url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustertype-detail')
cluster_count = serializers.IntegerField(read_only=True) cluster_count = RelatedObjectCountField('clusters')
class Meta: class Meta:
model = ClusterType model = ClusterType
@ -35,7 +36,7 @@ class NestedClusterTypeSerializer(WritableNestedSerializer):
) )
class NestedClusterGroupSerializer(WritableNestedSerializer): class NestedClusterGroupSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustergroup-detail') url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustergroup-detail')
cluster_count = serializers.IntegerField(read_only=True) cluster_count = RelatedObjectCountField('clusters')
class Meta: class Meta:
model = ClusterGroup model = ClusterGroup
@ -47,7 +48,7 @@ class NestedClusterGroupSerializer(WritableNestedSerializer):
) )
class NestedClusterSerializer(WritableNestedSerializer): class NestedClusterSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail') url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail')
virtualmachine_count = serializers.IntegerField(read_only=True) virtualmachine_count = RelatedObjectCountField('virtual_machines')
class Meta: class Meta:
model = Cluster model = Cluster

View File

@ -8,7 +8,7 @@ from dcim.choices import InterfaceModeChoices
from extras.api.nested_serializers import NestedConfigTemplateSerializer from extras.api.nested_serializers import NestedConfigTemplateSerializer
from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer, NestedVRFSerializer from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer, NestedVRFSerializer
from ipam.models import VLAN from ipam.models import VLAN
from netbox.api.fields import ChoiceField, SerializedPKRelatedField from netbox.api.fields import ChoiceField, RelatedObjectCountField, SerializedPKRelatedField
from netbox.api.serializers import NetBoxModelSerializer from netbox.api.serializers import NetBoxModelSerializer
from tenancy.api.nested_serializers import NestedTenantSerializer from tenancy.api.nested_serializers import NestedTenantSerializer
from virtualization.choices import * from virtualization.choices import *
@ -23,7 +23,9 @@ from .nested_serializers import *
class ClusterTypeSerializer(NetBoxModelSerializer): class ClusterTypeSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustertype-detail') url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustertype-detail')
cluster_count = serializers.IntegerField(read_only=True)
# Related object counts
cluster_count = RelatedObjectCountField('clusters')
class Meta: class Meta:
model = ClusterType model = ClusterType
@ -35,7 +37,9 @@ class ClusterTypeSerializer(NetBoxModelSerializer):
class ClusterGroupSerializer(NetBoxModelSerializer): class ClusterGroupSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustergroup-detail') url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustergroup-detail')
cluster_count = serializers.IntegerField(read_only=True)
# Related object counts
cluster_count = RelatedObjectCountField('clusters')
class Meta: class Meta:
model = ClusterGroup model = ClusterGroup
@ -52,8 +56,10 @@ class ClusterSerializer(NetBoxModelSerializer):
status = ChoiceField(choices=ClusterStatusChoices, required=False) status = ChoiceField(choices=ClusterStatusChoices, required=False)
tenant = NestedTenantSerializer(required=False, allow_null=True) tenant = NestedTenantSerializer(required=False, allow_null=True)
site = NestedSiteSerializer(required=False, allow_null=True, default=None) site = NestedSiteSerializer(required=False, allow_null=True, default=None)
device_count = serializers.IntegerField(read_only=True)
virtualmachine_count = serializers.IntegerField(read_only=True) # Related object counts
device_count = RelatedObjectCountField('devices')
virtualmachine_count = RelatedObjectCountField('virtual_machines')
class Meta: class Meta:
model = Cluster model = Cluster

View File

@ -1,10 +1,8 @@
from rest_framework.routers import APIRootView from rest_framework.routers import APIRootView
from dcim.models import Device
from extras.api.mixins import ConfigContextQuerySetMixin, RenderConfigMixin from extras.api.mixins import ConfigContextQuerySetMixin, RenderConfigMixin
from netbox.api.viewsets import NetBoxModelViewSet from netbox.api.viewsets import NetBoxModelViewSet
from utilities.query_functions import CollateAsChar from utilities.query_functions import CollateAsChar
from utilities.utils import count_related
from virtualization import filtersets from virtualization import filtersets
from virtualization.models import * from virtualization.models import *
from . import serializers from . import serializers
@ -23,28 +21,19 @@ class VirtualizationRootView(APIRootView):
# #
class ClusterTypeViewSet(NetBoxModelViewSet): class ClusterTypeViewSet(NetBoxModelViewSet):
queryset = ClusterType.objects.annotate( queryset = ClusterType.objects.all()
cluster_count=count_related(Cluster, 'type')
).prefetch_related('tags')
serializer_class = serializers.ClusterTypeSerializer serializer_class = serializers.ClusterTypeSerializer
filterset_class = filtersets.ClusterTypeFilterSet filterset_class = filtersets.ClusterTypeFilterSet
class ClusterGroupViewSet(NetBoxModelViewSet): class ClusterGroupViewSet(NetBoxModelViewSet):
queryset = ClusterGroup.objects.annotate( queryset = ClusterGroup.objects.all()
cluster_count=count_related(Cluster, 'group')
).prefetch_related('tags')
serializer_class = serializers.ClusterGroupSerializer serializer_class = serializers.ClusterGroupSerializer
filterset_class = filtersets.ClusterGroupFilterSet filterset_class = filtersets.ClusterGroupFilterSet
class ClusterViewSet(NetBoxModelViewSet): class ClusterViewSet(NetBoxModelViewSet):
queryset = Cluster.objects.prefetch_related( queryset = Cluster.objects.all()
'type', 'group', 'tenant', 'site', 'tags'
).annotate(
device_count=count_related(Device, 'cluster'),
virtualmachine_count=count_related(VirtualMachine, 'cluster')
)
serializer_class = serializers.ClusterSerializer serializer_class = serializers.ClusterSerializer
filterset_class = filtersets.ClusterFilterSet filterset_class = filtersets.ClusterFilterSet
@ -54,10 +43,7 @@ class ClusterViewSet(NetBoxModelViewSet):
# #
class VirtualMachineViewSet(ConfigContextQuerySetMixin, RenderConfigMixin, NetBoxModelViewSet): class VirtualMachineViewSet(ConfigContextQuerySetMixin, RenderConfigMixin, NetBoxModelViewSet):
queryset = VirtualMachine.objects.prefetch_related( queryset = VirtualMachine.objects.all()
'site', 'cluster', 'device', 'role', 'tenant', 'platform', 'primary_ip4', 'primary_ip6', 'config_template',
'tags', 'virtualdisks',
)
filterset_class = filtersets.VirtualMachineFilterSet filterset_class = filtersets.VirtualMachineFilterSet
def get_serializer_class(self): def get_serializer_class(self):
@ -83,12 +69,12 @@ class VirtualMachineViewSet(ConfigContextQuerySetMixin, RenderConfigMixin, NetBo
class VMInterfaceViewSet(NetBoxModelViewSet): class VMInterfaceViewSet(NetBoxModelViewSet):
queryset = VMInterface.objects.prefetch_related( queryset = VMInterface.objects.prefetch_related(
'virtual_machine', 'parent', 'tags', 'untagged_vlan', 'tagged_vlans', 'vrf', 'ip_addresses', 'l2vpn_terminations', # Referenced by VMInterfaceSerializer.l2vpn_termination
'fhrp_group_assignments', 'ip_addresses', # Referenced by VMInterface.count_ipaddresses()
'fhrp_group_assignments', # Referenced by VMInterface.count_fhrp_groups()
) )
serializer_class = serializers.VMInterfaceSerializer serializer_class = serializers.VMInterfaceSerializer
filterset_class = filtersets.VMInterfaceFilterSet filterset_class = filtersets.VMInterfaceFilterSet
brief_prefetch_fields = ['virtual_machine']
def get_bulk_destroy_queryset(self): def get_bulk_destroy_queryset(self):
# Ensure child interfaces are deleted prior to their parents # Ensure child interfaces are deleted prior to their parents
@ -96,9 +82,6 @@ class VMInterfaceViewSet(NetBoxModelViewSet):
class VirtualDiskViewSet(NetBoxModelViewSet): class VirtualDiskViewSet(NetBoxModelViewSet):
queryset = VirtualDisk.objects.prefetch_related( queryset = VirtualDisk.objects.all()
'virtual_machine', 'tags',
)
serializer_class = serializers.VirtualDiskSerializer serializer_class = serializers.VirtualDiskSerializer
filterset_class = filtersets.VirtualDiskFilterSet filterset_class = filtersets.VirtualDiskFilterSet
brief_prefetch_fields = ['virtual_machine']

View File

@ -1,6 +1,7 @@
from drf_spectacular.utils import extend_schema_serializer from drf_spectacular.utils import extend_schema_serializer
from rest_framework import serializers from rest_framework import serializers
from netbox.api.fields import RelatedObjectCountField
from netbox.api.serializers import WritableNestedSerializer from netbox.api.serializers import WritableNestedSerializer
from vpn import models from vpn import models
@ -23,7 +24,7 @@ __all__ = (
) )
class NestedTunnelGroupSerializer(WritableNestedSerializer): class NestedTunnelGroupSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='vpn-api:tunnelgroup-detail') url = serializers.HyperlinkedIdentityField(view_name='vpn-api:tunnelgroup-detail')
tunnel_count = serializers.IntegerField(read_only=True) tunnel_count = RelatedObjectCountField('tunnels')
class Meta: class Meta:
model = models.TunnelGroup model = models.TunnelGroup

View File

@ -4,7 +4,7 @@ from rest_framework import serializers
from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedRouteTargetSerializer from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedRouteTargetSerializer
from ipam.models import RouteTarget from ipam.models import RouteTarget
from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField, SerializedPKRelatedField
from netbox.api.serializers import NetBoxModelSerializer from netbox.api.serializers import NetBoxModelSerializer
from netbox.constants import NESTED_SERIALIZER_PREFIX from netbox.constants import NESTED_SERIALIZER_PREFIX
from tenancy.api.nested_serializers import NestedTenantSerializer from tenancy.api.nested_serializers import NestedTenantSerializer
@ -29,7 +29,9 @@ __all__ = (
class TunnelGroupSerializer(NetBoxModelSerializer): class TunnelGroupSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='vpn-api:tunnelgroup-detail') url = serializers.HyperlinkedIdentityField(view_name='vpn-api:tunnelgroup-detail')
tunnel_count = serializers.IntegerField(read_only=True)
# Related object counts
tunnel_count = RelatedObjectCountField('tunnels')
class Meta: class Meta:
model = TunnelGroup model = TunnelGroup
@ -59,11 +61,14 @@ class TunnelSerializer(NetBoxModelSerializer):
allow_null=True allow_null=True
) )
# Related object counts
terminations_count = RelatedObjectCountField('terminations')
class Meta: class Meta:
model = Tunnel model = Tunnel
fields = ( fields = (
'id', 'url', 'display', 'name', 'status', 'group', 'encapsulation', 'ipsec_profile', 'tenant', 'tunnel_id', 'id', 'url', 'display', 'name', 'status', 'group', 'encapsulation', 'ipsec_profile', 'tenant', 'tunnel_id',
'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'terminations_count',
) )

View File

@ -1,7 +1,6 @@
from rest_framework.routers import APIRootView from rest_framework.routers import APIRootView
from netbox.api.viewsets import NetBoxModelViewSet from netbox.api.viewsets import NetBoxModelViewSet
from utilities.utils import count_related
from vpn import filtersets from vpn import filtersets
from vpn.models import * from vpn.models import *
from . import serializers from . import serializers
@ -34,23 +33,19 @@ class VPNRootView(APIRootView):
# #
class TunnelGroupViewSet(NetBoxModelViewSet): class TunnelGroupViewSet(NetBoxModelViewSet):
queryset = TunnelGroup.objects.annotate( queryset = TunnelGroup.objects.all()
tunnel_count=count_related(Tunnel, 'group')
)
serializer_class = serializers.TunnelGroupSerializer serializer_class = serializers.TunnelGroupSerializer
filterset_class = filtersets.TunnelGroupFilterSet filterset_class = filtersets.TunnelGroupFilterSet
class TunnelViewSet(NetBoxModelViewSet): class TunnelViewSet(NetBoxModelViewSet):
queryset = Tunnel.objects.prefetch_related('ipsec_profile', 'tenant').annotate( queryset = Tunnel.objects.all()
terminations_count=count_related(TunnelTermination, 'tunnel')
)
serializer_class = serializers.TunnelSerializer serializer_class = serializers.TunnelSerializer
filterset_class = filtersets.TunnelFilterSet filterset_class = filtersets.TunnelFilterSet
class TunnelTerminationViewSet(NetBoxModelViewSet): class TunnelTerminationViewSet(NetBoxModelViewSet):
queryset = TunnelTermination.objects.prefetch_related('tunnel') queryset = TunnelTermination.objects.all()
serializer_class = serializers.TunnelTerminationSerializer serializer_class = serializers.TunnelTerminationSerializer
filterset_class = filtersets.TunnelTerminationFilterSet filterset_class = filtersets.TunnelTerminationFilterSet
@ -86,12 +81,12 @@ class IPSecProfileViewSet(NetBoxModelViewSet):
class L2VPNViewSet(NetBoxModelViewSet): class L2VPNViewSet(NetBoxModelViewSet):
queryset = L2VPN.objects.prefetch_related('import_targets', 'export_targets', 'tenant', 'tags') queryset = L2VPN.objects.all()
serializer_class = serializers.L2VPNSerializer serializer_class = serializers.L2VPNSerializer
filterset_class = filtersets.L2VPNFilterSet filterset_class = filtersets.L2VPNFilterSet
class L2VPNTerminationViewSet(NetBoxModelViewSet): class L2VPNTerminationViewSet(NetBoxModelViewSet):
queryset = L2VPNTermination.objects.prefetch_related('assigned_object') queryset = L2VPNTermination.objects.all()
serializer_class = serializers.L2VPNTerminationSerializer serializer_class = serializers.L2VPNTerminationSerializer
filterset_class = filtersets.L2VPNTerminationFilterSet filterset_class = filtersets.L2VPNTerminationFilterSet

View File

@ -27,12 +27,12 @@ class WirelessLANGroupViewSet(MPTTLockedMixin, NetBoxModelViewSet):
class WirelessLANViewSet(NetBoxModelViewSet): class WirelessLANViewSet(NetBoxModelViewSet):
queryset = WirelessLAN.objects.prefetch_related('vlan', 'tenant', 'tags') queryset = WirelessLAN.objects.all()
serializer_class = serializers.WirelessLANSerializer serializer_class = serializers.WirelessLANSerializer
filterset_class = filtersets.WirelessLANFilterSet filterset_class = filtersets.WirelessLANFilterSet
class WirelessLinkViewSet(NetBoxModelViewSet): class WirelessLinkViewSet(NetBoxModelViewSet):
queryset = WirelessLink.objects.prefetch_related('interface_a', 'interface_b', 'tenant', 'tags') queryset = WirelessLink.objects.all()
serializer_class = serializers.WirelessLinkSerializer serializer_class = serializers.WirelessLinkSerializer
filterset_class = filtersets.WirelessLinkFilterSet filterset_class = filtersets.WirelessLinkFilterSet

View File

@ -108,7 +108,9 @@ class WirelessLinkForm(TenancyForm, NetBoxModelForm):
'kind': 'wireless', 'kind': 'wireless',
'device_id': '$device_a', 'device_id': '$device_a',
}, },
disabled_indicator='_occupied', context={
'disabled': '_occupied',
},
label=_('Interface') label=_('Interface')
) )
site_b = DynamicModelChoiceField( site_b = DynamicModelChoiceField(
@ -148,7 +150,9 @@ class WirelessLinkForm(TenancyForm, NetBoxModelForm):
'kind': 'wireless', 'kind': 'wireless',
'device_id': '$device_b', 'device_id': '$device_b',
}, },
disabled_indicator='_occupied', context={
'disabled': '_occupied',
},
label=_('Interface') label=_('Interface')
) )
comments = CommentField() comments = CommentField()