mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-17 04:58:16 -06:00
Merge branch 'feature' into 14438-script-model
This commit is contained in:
commit
3182977a88
@ -304,6 +304,7 @@ A particular object within NetBox. Each ObjectVar must specify a particular mode
|
||||
|
||||
* `model` - The model class
|
||||
* `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)
|
||||
|
||||
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
|
||||
|
||||
Similar to `ObjectVar`, but allows for the selection of multiple objects.
|
||||
|
@ -5,6 +5,7 @@
|
||||
### Breaking Changes
|
||||
|
||||
* 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
|
||||
|
||||
@ -15,6 +16,8 @@ The NetBox user interface has been completely refreshed and updated.
|
||||
### Enhancements
|
||||
|
||||
* [#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
|
||||
* [#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
|
||||
@ -23,6 +26,7 @@ The NetBox user interface has been completely refreshed and updated.
|
||||
### 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)
|
||||
* [#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
|
||||
* [#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`)
|
||||
|
@ -292,6 +292,7 @@ nav:
|
||||
- git Cheat Sheet: 'development/git-cheat-sheet.md'
|
||||
- Release Notes:
|
||||
- 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.6: 'release-notes/version-3.6.md'
|
||||
- Version 3.5: 'release-notes/version-3.5.md'
|
||||
|
@ -1,8 +1,8 @@
|
||||
from drf_spectacular.utils import extend_schema_field, extend_schema_serializer
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import extend_schema_serializer
|
||||
from rest_framework import serializers
|
||||
|
||||
from circuits.models import *
|
||||
from netbox.api.fields import RelatedObjectCountField
|
||||
from netbox.api.serializers import WritableNestedSerializer
|
||||
|
||||
__all__ = [
|
||||
@ -36,7 +36,7 @@ class NestedProviderNetworkSerializer(WritableNestedSerializer):
|
||||
)
|
||||
class NestedProviderSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
|
||||
circuit_count = serializers.IntegerField(read_only=True)
|
||||
circuit_count = RelatedObjectCountField('circuits')
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
@ -64,7 +64,7 @@ class NestedProviderAccountSerializer(WritableNestedSerializer):
|
||||
)
|
||||
class NestedCircuitTypeSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
|
||||
circuit_count = serializers.IntegerField(read_only=True)
|
||||
circuit_count = RelatedObjectCountField('circuits')
|
||||
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
|
@ -6,7 +6,7 @@ from dcim.api.nested_serializers import NestedSiteSerializer
|
||||
from dcim.api.serializers import CabledObjectSerializer
|
||||
from ipam.models import ASN
|
||||
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 tenancy.api.nested_serializers import NestedTenantSerializer
|
||||
from .nested_serializers import *
|
||||
@ -32,7 +32,7 @@ class ProviderSerializer(NetBoxModelSerializer):
|
||||
)
|
||||
|
||||
# Related object counts
|
||||
circuit_count = serializers.IntegerField(read_only=True)
|
||||
circuit_count = RelatedObjectCountField('circuits')
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
@ -80,13 +80,15 @@ class ProviderNetworkSerializer(NetBoxModelSerializer):
|
||||
|
||||
class CircuitTypeSerializer(NetBoxModelSerializer):
|
||||
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:
|
||||
model = CircuitType
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'circuit_count',
|
||||
'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created',
|
||||
'last_updated', 'circuit_count',
|
||||
]
|
||||
|
||||
|
||||
|
@ -4,7 +4,6 @@ from circuits import filtersets
|
||||
from circuits.models import *
|
||||
from dcim.api.views import PassThroughPortMixin
|
||||
from netbox.api.viewsets import NetBoxModelViewSet
|
||||
from utilities.utils import count_related
|
||||
from . import serializers
|
||||
|
||||
|
||||
@ -21,9 +20,7 @@ class CircuitsRootView(APIRootView):
|
||||
#
|
||||
|
||||
class ProviderViewSet(NetBoxModelViewSet):
|
||||
queryset = Provider.objects.prefetch_related('asns', 'tags').annotate(
|
||||
circuit_count=count_related(Circuit, 'provider')
|
||||
)
|
||||
queryset = Provider.objects.all()
|
||||
serializer_class = serializers.ProviderSerializer
|
||||
filterset_class = filtersets.ProviderFilterSet
|
||||
|
||||
@ -33,9 +30,7 @@ class ProviderViewSet(NetBoxModelViewSet):
|
||||
#
|
||||
|
||||
class CircuitTypeViewSet(NetBoxModelViewSet):
|
||||
queryset = CircuitType.objects.prefetch_related('tags').annotate(
|
||||
circuit_count=count_related(Circuit, 'type')
|
||||
)
|
||||
queryset = CircuitType.objects.all()
|
||||
serializer_class = serializers.CircuitTypeSerializer
|
||||
filterset_class = filtersets.CircuitTypeFilterSet
|
||||
|
||||
@ -45,9 +40,7 @@ class CircuitTypeViewSet(NetBoxModelViewSet):
|
||||
#
|
||||
|
||||
class CircuitViewSet(NetBoxModelViewSet):
|
||||
queryset = Circuit.objects.prefetch_related(
|
||||
'type', 'tenant', 'provider', 'provider_account', 'termination_a', 'termination_z'
|
||||
).prefetch_related('tags')
|
||||
queryset = Circuit.objects.all()
|
||||
serializer_class = serializers.CircuitSerializer
|
||||
filterset_class = filtersets.CircuitFilterSet
|
||||
|
||||
@ -57,12 +50,9 @@ class CircuitViewSet(NetBoxModelViewSet):
|
||||
#
|
||||
|
||||
class CircuitTerminationViewSet(PassThroughPortMixin, NetBoxModelViewSet):
|
||||
queryset = CircuitTermination.objects.prefetch_related(
|
||||
'circuit', 'site', 'provider_network', 'cable__terminations'
|
||||
)
|
||||
queryset = CircuitTermination.objects.all()
|
||||
serializer_class = serializers.CircuitTerminationSerializer
|
||||
filterset_class = filtersets.CircuitTerminationFilterSet
|
||||
brief_prefetch_fields = ['circuit']
|
||||
|
||||
|
||||
#
|
||||
@ -70,7 +60,7 @@ class CircuitTerminationViewSet(PassThroughPortMixin, NetBoxModelViewSet):
|
||||
#
|
||||
|
||||
class ProviderAccountViewSet(NetBoxModelViewSet):
|
||||
queryset = ProviderAccount.objects.prefetch_related('provider', 'tags')
|
||||
queryset = ProviderAccount.objects.all()
|
||||
serializer_class = serializers.ProviderAccountSerializer
|
||||
filterset_class = filtersets.ProviderAccountFilterSet
|
||||
|
||||
@ -80,6 +70,6 @@ class ProviderAccountViewSet(NetBoxModelViewSet):
|
||||
#
|
||||
|
||||
class ProviderNetworkViewSet(NetBoxModelViewSet):
|
||||
queryset = ProviderNetwork.objects.prefetch_related('tags')
|
||||
queryset = ProviderNetwork.objects.all()
|
||||
serializer_class = serializers.ProviderNetworkSerializer
|
||||
filterset_class = filtersets.ProviderNetworkFilterSet
|
||||
|
@ -2,7 +2,7 @@ from rest_framework import serializers
|
||||
|
||||
from core.choices 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.utils import get_data_backend_choices
|
||||
from users.api.nested_serializers import NestedUserSerializer
|
||||
@ -28,9 +28,7 @@ class DataSourceSerializer(NetBoxModelSerializer):
|
||||
)
|
||||
|
||||
# Related object counts
|
||||
file_count = serializers.IntegerField(
|
||||
read_only=True
|
||||
)
|
||||
file_count = RelatedObjectCountField('datafiles')
|
||||
|
||||
class Meta:
|
||||
model = DataSource
|
||||
|
@ -9,7 +9,6 @@ from rest_framework.viewsets import ReadOnlyModelViewSet
|
||||
from core import filtersets
|
||||
from core.models import *
|
||||
from netbox.api.viewsets import NetBoxModelViewSet, NetBoxReadOnlyModelViewSet
|
||||
from utilities.utils import count_related
|
||||
from . import serializers
|
||||
|
||||
|
||||
@ -22,9 +21,7 @@ class CoreRootView(APIRootView):
|
||||
|
||||
|
||||
class DataSourceViewSet(NetBoxModelViewSet):
|
||||
queryset = DataSource.objects.annotate(
|
||||
file_count=count_related(DataFile, 'source')
|
||||
)
|
||||
queryset = DataSource.objects.all()
|
||||
serializer_class = serializers.DataSourceSerializer
|
||||
filterset_class = filtersets.DataSourceFilterSet
|
||||
|
||||
@ -44,7 +41,7 @@ class DataSourceViewSet(NetBoxModelViewSet):
|
||||
|
||||
|
||||
class DataFileViewSet(NetBoxReadOnlyModelViewSet):
|
||||
queryset = DataFile.objects.defer('data').prefetch_related('source')
|
||||
queryset = DataFile.objects.defer('data')
|
||||
serializer_class = serializers.DataFileSerializer
|
||||
filterset_class = filtersets.DataFileFilterSet
|
||||
|
||||
@ -53,6 +50,6 @@ class JobViewSet(ReadOnlyModelViewSet):
|
||||
"""
|
||||
Retrieve a list of job results
|
||||
"""
|
||||
queryset = Job.objects.prefetch_related('user')
|
||||
queryset = Job.objects.all()
|
||||
serializer_class = serializers.JobSerializer
|
||||
filterset_class = filtersets.JobFilterSet
|
||||
|
@ -2,7 +2,8 @@ from drf_spectacular.utils import extend_schema_serializer
|
||||
from rest_framework import serializers
|
||||
|
||||
from dcim import models
|
||||
from netbox.api.serializers import BaseModelSerializer, WritableNestedSerializer
|
||||
from netbox.api.fields import RelatedObjectCountField
|
||||
from netbox.api.serializers import WritableNestedSerializer
|
||||
|
||||
__all__ = [
|
||||
'ComponentNestedModuleSerializer',
|
||||
@ -110,7 +111,7 @@ class NestedLocationSerializer(WritableNestedSerializer):
|
||||
)
|
||||
class NestedRackRoleSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
|
||||
rack_count = serializers.IntegerField(read_only=True)
|
||||
rack_count = RelatedObjectCountField('racks')
|
||||
|
||||
class Meta:
|
||||
model = models.RackRole
|
||||
@ -122,7 +123,7 @@ class NestedRackRoleSerializer(WritableNestedSerializer):
|
||||
)
|
||||
class NestedRackSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail')
|
||||
device_count = serializers.IntegerField(read_only=True)
|
||||
device_count = RelatedObjectCountField('devices')
|
||||
|
||||
class Meta:
|
||||
model = models.Rack
|
||||
@ -150,7 +151,7 @@ class NestedRackReservationSerializer(WritableNestedSerializer):
|
||||
)
|
||||
class NestedManufacturerSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
|
||||
devicetype_count = serializers.IntegerField(read_only=True)
|
||||
devicetype_count = RelatedObjectCountField('device_types')
|
||||
|
||||
class Meta:
|
||||
model = models.Manufacturer
|
||||
@ -163,7 +164,7 @@ class NestedManufacturerSerializer(WritableNestedSerializer):
|
||||
class NestedDeviceTypeSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
|
||||
manufacturer = NestedManufacturerSerializer(read_only=True)
|
||||
device_count = serializers.IntegerField(read_only=True)
|
||||
device_count = RelatedObjectCountField('instances')
|
||||
|
||||
class Meta:
|
||||
model = models.DeviceType
|
||||
@ -173,7 +174,6 @@ class NestedDeviceTypeSerializer(WritableNestedSerializer):
|
||||
class NestedModuleTypeSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:moduletype-detail')
|
||||
manufacturer = NestedManufacturerSerializer(read_only=True)
|
||||
# module_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.ModuleType
|
||||
@ -274,8 +274,8 @@ class NestedInventoryItemTemplateSerializer(WritableNestedSerializer):
|
||||
)
|
||||
class NestedDeviceRoleSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
|
||||
device_count = serializers.IntegerField(read_only=True)
|
||||
virtualmachine_count = serializers.IntegerField(read_only=True)
|
||||
device_count = RelatedObjectCountField('devices')
|
||||
virtualmachine_count = RelatedObjectCountField('virtual_machines')
|
||||
|
||||
class Meta:
|
||||
model = models.DeviceRole
|
||||
@ -287,8 +287,8 @@ class NestedDeviceRoleSerializer(WritableNestedSerializer):
|
||||
)
|
||||
class NestedPlatformSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
|
||||
device_count = serializers.IntegerField(read_only=True)
|
||||
virtualmachine_count = serializers.IntegerField(read_only=True)
|
||||
device_count = RelatedObjectCountField('devices')
|
||||
virtualmachine_count = RelatedObjectCountField('virtual_machines')
|
||||
|
||||
class Meta:
|
||||
model = models.Platform
|
||||
@ -445,7 +445,7 @@ class NestedInventoryItemSerializer(WritableNestedSerializer):
|
||||
)
|
||||
class NestedInventoryItemRoleSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemrole-detail')
|
||||
inventoryitem_count = serializers.IntegerField(read_only=True)
|
||||
inventoryitem_count = RelatedObjectCountField('inventory_items')
|
||||
|
||||
class Meta:
|
||||
model = models.InventoryItemRole
|
||||
@ -490,7 +490,7 @@ class NestedVirtualChassisSerializer(WritableNestedSerializer):
|
||||
)
|
||||
class NestedPowerPanelSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail')
|
||||
powerfeed_count = serializers.IntegerField(read_only=True)
|
||||
powerfeed_count = RelatedObjectCountField('powerfeeds')
|
||||
|
||||
class Meta:
|
||||
model = models.PowerPanel
|
||||
|
@ -15,7 +15,7 @@ from ipam.api.nested_serializers import (
|
||||
NestedASNSerializer, NestedIPAddressSerializer, NestedVLANSerializer, NestedVRFSerializer,
|
||||
)
|
||||
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 (
|
||||
GenericObjectSerializer, NestedGroupModelSerializer, NetBoxModelSerializer, ValidatedModelSerializer,
|
||||
WritableNestedSerializer,
|
||||
@ -144,12 +144,12 @@ class SiteSerializer(NetBoxModelSerializer):
|
||||
)
|
||||
|
||||
# Related object counts
|
||||
circuit_count = serializers.IntegerField(read_only=True)
|
||||
device_count = serializers.IntegerField(read_only=True)
|
||||
prefix_count = serializers.IntegerField(read_only=True)
|
||||
rack_count = serializers.IntegerField(read_only=True)
|
||||
virtualmachine_count = serializers.IntegerField(read_only=True)
|
||||
vlan_count = serializers.IntegerField(read_only=True)
|
||||
circuit_count = RelatedObjectCountField('circuit_terminations')
|
||||
device_count = RelatedObjectCountField('devices')
|
||||
prefix_count = RelatedObjectCountField('prefixes')
|
||||
rack_count = RelatedObjectCountField('racks')
|
||||
vlan_count = RelatedObjectCountField('vlans')
|
||||
virtualmachine_count = RelatedObjectCountField('virtual_machines')
|
||||
|
||||
class Meta:
|
||||
model = Site
|
||||
@ -184,7 +184,9 @@ class LocationSerializer(NestedGroupModelSerializer):
|
||||
|
||||
class RackRoleSerializer(NetBoxModelSerializer):
|
||||
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:
|
||||
model = RackRole
|
||||
@ -207,8 +209,10 @@ class RackSerializer(NetBoxModelSerializer):
|
||||
width = ChoiceField(choices=RackWidthChoices, required=False)
|
||||
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)
|
||||
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:
|
||||
model = Rack
|
||||
@ -299,9 +303,11 @@ class RackElevationDetailFilterSerializer(serializers.Serializer):
|
||||
|
||||
class ManufacturerSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
|
||||
devicetype_count = serializers.IntegerField(read_only=True)
|
||||
inventoryitem_count = serializers.IntegerField(read_only=True)
|
||||
platform_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
# Related object counts
|
||||
devicetype_count = RelatedObjectCountField('device_types')
|
||||
inventoryitem_count = RelatedObjectCountField('inventory_items')
|
||||
platform_count = RelatedObjectCountField('platforms')
|
||||
|
||||
class Meta:
|
||||
model = Manufacturer
|
||||
@ -325,7 +331,6 @@ class DeviceTypeSerializer(NetBoxModelSerializer):
|
||||
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)
|
||||
weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True)
|
||||
device_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
# Counter fields
|
||||
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)
|
||||
inventory_item_template_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
# Related object counts
|
||||
device_count = RelatedObjectCountField('instances')
|
||||
|
||||
class Meta:
|
||||
model = DeviceType
|
||||
fields = [
|
||||
@ -636,8 +644,10 @@ class InventoryItemTemplateSerializer(ValidatedModelSerializer):
|
||||
class DeviceRoleSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
|
||||
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:
|
||||
model = DeviceRole
|
||||
@ -651,8 +661,10 @@ class PlatformSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
|
||||
manufacturer = NestedManufacturerSerializer(required=False, allow_null=True)
|
||||
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:
|
||||
model = Platform
|
||||
@ -761,7 +773,7 @@ class VirtualDeviceContextSerializer(NetBoxModelSerializer):
|
||||
status = ChoiceField(choices=VirtualDeviceContextStatusChoices)
|
||||
|
||||
# Related object counts
|
||||
interface_count = serializers.IntegerField(read_only=True)
|
||||
interface_count = RelatedObjectCountField('interfaces')
|
||||
|
||||
class Meta:
|
||||
model = VirtualDeviceContext
|
||||
@ -1092,7 +1104,9 @@ class InventoryItemSerializer(NetBoxModelSerializer):
|
||||
|
||||
class InventoryItemRoleSerializer(NetBoxModelSerializer):
|
||||
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:
|
||||
model = InventoryItemRole
|
||||
@ -1204,7 +1218,9 @@ class PowerPanelSerializer(NetBoxModelSerializer):
|
||||
allow_null=True,
|
||||
default=None
|
||||
)
|
||||
powerfeed_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
# Related object counts
|
||||
powerfeed_count = RelatedObjectCountField('powerfeeds')
|
||||
|
||||
class Meta:
|
||||
model = PowerPanel
|
||||
|
@ -13,7 +13,6 @@ from dcim.constants import CABLE_TRACE_SVG_DEFAULT_WIDTH
|
||||
from dcim.models import *
|
||||
from dcim.svg import CableTraceSVG
|
||||
from extras.api.mixins import ConfigContextQuerySetMixin, RenderConfigMixin
|
||||
from ipam.models import Prefix, VLAN
|
||||
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
|
||||
from netbox.api.metadata import ContentTypeMetadata
|
||||
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.query_functions import CollateAsChar
|
||||
from utilities.utils import count_related
|
||||
from virtualization.models import VirtualMachine
|
||||
from . import serializers
|
||||
from .exceptions import MissingFilterException
|
||||
|
||||
@ -103,7 +101,7 @@ class RegionViewSet(MPTTLockedMixin, NetBoxModelViewSet):
|
||||
'region',
|
||||
'site_count',
|
||||
cumulative=True
|
||||
).prefetch_related('tags')
|
||||
)
|
||||
serializer_class = serializers.RegionSerializer
|
||||
filterset_class = filtersets.RegionFilterSet
|
||||
|
||||
@ -119,7 +117,7 @@ class SiteGroupViewSet(MPTTLockedMixin, NetBoxModelViewSet):
|
||||
'group',
|
||||
'site_count',
|
||||
cumulative=True
|
||||
).prefetch_related('tags')
|
||||
)
|
||||
serializer_class = serializers.SiteGroupSerializer
|
||||
filterset_class = filtersets.SiteGroupFilterSet
|
||||
|
||||
@ -129,16 +127,7 @@ class SiteGroupViewSet(MPTTLockedMixin, NetBoxModelViewSet):
|
||||
#
|
||||
|
||||
class SiteViewSet(NetBoxModelViewSet):
|
||||
queryset = Site.objects.prefetch_related(
|
||||
'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')
|
||||
)
|
||||
queryset = Site.objects.all()
|
||||
serializer_class = serializers.SiteSerializer
|
||||
filterset_class = filtersets.SiteFilterSet
|
||||
|
||||
@ -160,7 +149,7 @@ class LocationViewSet(MPTTLockedMixin, NetBoxModelViewSet):
|
||||
'location',
|
||||
'rack_count',
|
||||
cumulative=True
|
||||
).prefetch_related('site', 'tags')
|
||||
)
|
||||
serializer_class = serializers.LocationSerializer
|
||||
filterset_class = filtersets.LocationFilterSet
|
||||
|
||||
@ -170,9 +159,7 @@ class LocationViewSet(MPTTLockedMixin, NetBoxModelViewSet):
|
||||
#
|
||||
|
||||
class RackRoleViewSet(NetBoxModelViewSet):
|
||||
queryset = RackRole.objects.prefetch_related('tags').annotate(
|
||||
rack_count=count_related(Rack, 'role')
|
||||
)
|
||||
queryset = RackRole.objects.all()
|
||||
serializer_class = serializers.RackRoleSerializer
|
||||
filterset_class = filtersets.RackRoleFilterSet
|
||||
|
||||
@ -182,12 +169,7 @@ class RackRoleViewSet(NetBoxModelViewSet):
|
||||
#
|
||||
|
||||
class RackViewSet(NetBoxModelViewSet):
|
||||
queryset = Rack.objects.prefetch_related(
|
||||
'site', 'location', 'role', 'tenant', 'tags'
|
||||
).annotate(
|
||||
device_count=count_related(Device, 'rack'),
|
||||
powerfeed_count=count_related(PowerFeed, 'rack')
|
||||
)
|
||||
queryset = Rack.objects.all()
|
||||
serializer_class = serializers.RackSerializer
|
||||
filterset_class = filtersets.RackFilterSet
|
||||
|
||||
@ -249,7 +231,7 @@ class RackViewSet(NetBoxModelViewSet):
|
||||
#
|
||||
|
||||
class RackReservationViewSet(NetBoxModelViewSet):
|
||||
queryset = RackReservation.objects.prefetch_related('rack', 'user', 'tenant')
|
||||
queryset = RackReservation.objects.all()
|
||||
serializer_class = serializers.RackReservationSerializer
|
||||
filterset_class = filtersets.RackReservationFilterSet
|
||||
|
||||
@ -259,11 +241,7 @@ class RackReservationViewSet(NetBoxModelViewSet):
|
||||
#
|
||||
|
||||
class ManufacturerViewSet(NetBoxModelViewSet):
|
||||
queryset = Manufacturer.objects.prefetch_related('tags').annotate(
|
||||
devicetype_count=count_related(DeviceType, 'manufacturer'),
|
||||
inventoryitem_count=count_related(InventoryItem, 'manufacturer'),
|
||||
platform_count=count_related(Platform, 'manufacturer')
|
||||
)
|
||||
queryset = Manufacturer.objects.all()
|
||||
serializer_class = serializers.ManufacturerSerializer
|
||||
filterset_class = filtersets.ManufacturerFilterSet
|
||||
|
||||
@ -273,21 +251,15 @@ class ManufacturerViewSet(NetBoxModelViewSet):
|
||||
#
|
||||
|
||||
class DeviceTypeViewSet(NetBoxModelViewSet):
|
||||
queryset = DeviceType.objects.prefetch_related('manufacturer', 'default_platform', 'tags').annotate(
|
||||
device_count=count_related(Device, 'device_type')
|
||||
)
|
||||
queryset = DeviceType.objects.all()
|
||||
serializer_class = serializers.DeviceTypeSerializer
|
||||
filterset_class = filtersets.DeviceTypeFilterSet
|
||||
brief_prefetch_fields = ['manufacturer']
|
||||
|
||||
|
||||
class ModuleTypeViewSet(NetBoxModelViewSet):
|
||||
queryset = ModuleType.objects.prefetch_related('manufacturer', 'tags').annotate(
|
||||
# module_count=count_related(Module, 'module_type')
|
||||
)
|
||||
queryset = ModuleType.objects.all()
|
||||
serializer_class = serializers.ModuleTypeSerializer
|
||||
filterset_class = filtersets.ModuleTypeFilterSet
|
||||
brief_prefetch_fields = ['manufacturer']
|
||||
|
||||
|
||||
#
|
||||
@ -295,61 +267,61 @@ class ModuleTypeViewSet(NetBoxModelViewSet):
|
||||
#
|
||||
|
||||
class ConsolePortTemplateViewSet(NetBoxModelViewSet):
|
||||
queryset = ConsolePortTemplate.objects.prefetch_related('device_type__manufacturer')
|
||||
queryset = ConsolePortTemplate.objects.all()
|
||||
serializer_class = serializers.ConsolePortTemplateSerializer
|
||||
filterset_class = filtersets.ConsolePortTemplateFilterSet
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateViewSet(NetBoxModelViewSet):
|
||||
queryset = ConsoleServerPortTemplate.objects.prefetch_related('device_type__manufacturer')
|
||||
queryset = ConsoleServerPortTemplate.objects.all()
|
||||
serializer_class = serializers.ConsoleServerPortTemplateSerializer
|
||||
filterset_class = filtersets.ConsoleServerPortTemplateFilterSet
|
||||
|
||||
|
||||
class PowerPortTemplateViewSet(NetBoxModelViewSet):
|
||||
queryset = PowerPortTemplate.objects.prefetch_related('device_type__manufacturer')
|
||||
queryset = PowerPortTemplate.objects.all()
|
||||
serializer_class = serializers.PowerPortTemplateSerializer
|
||||
filterset_class = filtersets.PowerPortTemplateFilterSet
|
||||
|
||||
|
||||
class PowerOutletTemplateViewSet(NetBoxModelViewSet):
|
||||
queryset = PowerOutletTemplate.objects.prefetch_related('device_type__manufacturer')
|
||||
queryset = PowerOutletTemplate.objects.all()
|
||||
serializer_class = serializers.PowerOutletTemplateSerializer
|
||||
filterset_class = filtersets.PowerOutletTemplateFilterSet
|
||||
|
||||
|
||||
class InterfaceTemplateViewSet(NetBoxModelViewSet):
|
||||
queryset = InterfaceTemplate.objects.prefetch_related('device_type__manufacturer')
|
||||
queryset = InterfaceTemplate.objects.all()
|
||||
serializer_class = serializers.InterfaceTemplateSerializer
|
||||
filterset_class = filtersets.InterfaceTemplateFilterSet
|
||||
|
||||
|
||||
class FrontPortTemplateViewSet(NetBoxModelViewSet):
|
||||
queryset = FrontPortTemplate.objects.prefetch_related('device_type__manufacturer')
|
||||
queryset = FrontPortTemplate.objects.all()
|
||||
serializer_class = serializers.FrontPortTemplateSerializer
|
||||
filterset_class = filtersets.FrontPortTemplateFilterSet
|
||||
|
||||
|
||||
class RearPortTemplateViewSet(NetBoxModelViewSet):
|
||||
queryset = RearPortTemplate.objects.prefetch_related('device_type__manufacturer')
|
||||
queryset = RearPortTemplate.objects.all()
|
||||
serializer_class = serializers.RearPortTemplateSerializer
|
||||
filterset_class = filtersets.RearPortTemplateFilterSet
|
||||
|
||||
|
||||
class ModuleBayTemplateViewSet(NetBoxModelViewSet):
|
||||
queryset = ModuleBayTemplate.objects.prefetch_related('device_type__manufacturer')
|
||||
queryset = ModuleBayTemplate.objects.all()
|
||||
serializer_class = serializers.ModuleBayTemplateSerializer
|
||||
filterset_class = filtersets.ModuleBayTemplateFilterSet
|
||||
|
||||
|
||||
class DeviceBayTemplateViewSet(NetBoxModelViewSet):
|
||||
queryset = DeviceBayTemplate.objects.prefetch_related('device_type__manufacturer')
|
||||
queryset = DeviceBayTemplate.objects.all()
|
||||
serializer_class = serializers.DeviceBayTemplateSerializer
|
||||
filterset_class = filtersets.DeviceBayTemplateFilterSet
|
||||
|
||||
|
||||
class InventoryItemTemplateViewSet(MPTTLockedMixin, NetBoxModelViewSet):
|
||||
queryset = InventoryItemTemplate.objects.prefetch_related('device_type__manufacturer', 'role')
|
||||
queryset = InventoryItemTemplate.objects.all()
|
||||
serializer_class = serializers.InventoryItemTemplateSerializer
|
||||
filterset_class = filtersets.InventoryItemTemplateFilterSet
|
||||
|
||||
@ -359,10 +331,7 @@ class InventoryItemTemplateViewSet(MPTTLockedMixin, NetBoxModelViewSet):
|
||||
#
|
||||
|
||||
class DeviceRoleViewSet(NetBoxModelViewSet):
|
||||
queryset = DeviceRole.objects.prefetch_related('config_template', 'tags').annotate(
|
||||
device_count=count_related(Device, 'role'),
|
||||
virtualmachine_count=count_related(VirtualMachine, 'role')
|
||||
)
|
||||
queryset = DeviceRole.objects.all()
|
||||
serializer_class = serializers.DeviceRoleSerializer
|
||||
filterset_class = filtersets.DeviceRoleFilterSet
|
||||
|
||||
@ -372,10 +341,7 @@ class DeviceRoleViewSet(NetBoxModelViewSet):
|
||||
#
|
||||
|
||||
class PlatformViewSet(NetBoxModelViewSet):
|
||||
queryset = Platform.objects.prefetch_related('config_template', 'tags').annotate(
|
||||
device_count=count_related(Device, 'platform'),
|
||||
virtualmachine_count=count_related(VirtualMachine, 'platform')
|
||||
)
|
||||
queryset = Platform.objects.all()
|
||||
serializer_class = serializers.PlatformSerializer
|
||||
filterset_class = filtersets.PlatformFilterSet
|
||||
|
||||
@ -391,8 +357,7 @@ class DeviceViewSet(
|
||||
NetBoxModelViewSet
|
||||
):
|
||||
queryset = Device.objects.prefetch_related(
|
||||
'device_type__manufacturer', 'role', 'tenant', 'platform', 'site', 'location', 'rack', 'parent_bay',
|
||||
'virtual_chassis__master', 'primary_ip4__nat_outside', 'primary_ip6__nat_outside', 'config_template', 'tags',
|
||||
'parent_bay', # Referenced by DeviceSerializer.get_parent_device()
|
||||
)
|
||||
filterset_class = filtersets.DeviceFilterSet
|
||||
pagination_class = StripCountAnnotationsPaginator
|
||||
@ -419,19 +384,13 @@ class DeviceViewSet(
|
||||
|
||||
|
||||
class VirtualDeviceContextViewSet(NetBoxModelViewSet):
|
||||
queryset = VirtualDeviceContext.objects.prefetch_related(
|
||||
'device__device_type', 'device', 'tenant', 'tags',
|
||||
).annotate(
|
||||
interface_count=count_related(Interface, 'vdcs'),
|
||||
)
|
||||
queryset = VirtualDeviceContext.objects.all()
|
||||
serializer_class = serializers.VirtualDeviceContextSerializer
|
||||
filterset_class = filtersets.VirtualDeviceContextFilterSet
|
||||
|
||||
|
||||
class ModuleViewSet(NetBoxModelViewSet):
|
||||
queryset = Module.objects.prefetch_related(
|
||||
'device', 'module_bay', 'module_type__manufacturer', 'tags',
|
||||
)
|
||||
queryset = Module.objects.all()
|
||||
serializer_class = serializers.ModuleSerializer
|
||||
filterset_class = filtersets.ModuleFilterSet
|
||||
|
||||
@ -442,49 +401,45 @@ class ModuleViewSet(NetBoxModelViewSet):
|
||||
|
||||
class ConsolePortViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
||||
queryset = ConsolePort.objects.prefetch_related(
|
||||
'device', 'module__module_bay', '_path', 'cable__terminations', 'tags'
|
||||
'_path', 'cable__terminations',
|
||||
)
|
||||
serializer_class = serializers.ConsolePortSerializer
|
||||
filterset_class = filtersets.ConsolePortFilterSet
|
||||
brief_prefetch_fields = ['device']
|
||||
|
||||
|
||||
class ConsoleServerPortViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
||||
queryset = ConsoleServerPort.objects.prefetch_related(
|
||||
'device', 'module__module_bay', '_path', 'cable__terminations', 'tags'
|
||||
'_path', 'cable__terminations',
|
||||
)
|
||||
serializer_class = serializers.ConsoleServerPortSerializer
|
||||
filterset_class = filtersets.ConsoleServerPortFilterSet
|
||||
brief_prefetch_fields = ['device']
|
||||
|
||||
|
||||
class PowerPortViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
||||
queryset = PowerPort.objects.prefetch_related(
|
||||
'device', 'module__module_bay', '_path', 'cable__terminations', 'tags'
|
||||
'_path', 'cable__terminations',
|
||||
)
|
||||
serializer_class = serializers.PowerPortSerializer
|
||||
filterset_class = filtersets.PowerPortFilterSet
|
||||
brief_prefetch_fields = ['device']
|
||||
|
||||
|
||||
class PowerOutletViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
||||
queryset = PowerOutlet.objects.prefetch_related(
|
||||
'device', 'module__module_bay', '_path', 'cable__terminations', 'tags'
|
||||
'_path', 'cable__terminations',
|
||||
)
|
||||
serializer_class = serializers.PowerOutletSerializer
|
||||
filterset_class = filtersets.PowerOutletFilterSet
|
||||
brief_prefetch_fields = ['device']
|
||||
|
||||
|
||||
class InterfaceViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
||||
queryset = Interface.objects.prefetch_related(
|
||||
'device', 'module__module_bay', 'parent', 'bridge', 'lag', '_path', 'cable__terminations', 'wireless_lans',
|
||||
'untagged_vlan', 'tagged_vlans', 'vrf', 'ip_addresses', 'fhrp_group_assignments', 'tags', 'l2vpn_terminations',
|
||||
'vdcs',
|
||||
'_path', 'cable__terminations',
|
||||
'l2vpn_terminations', # Referenced by InterfaceSerializer.l2vpn_termination
|
||||
'ip_addresses', # Referenced by Interface.count_ipaddresses()
|
||||
'fhrp_group_assignments', # Referenced by Interface.count_fhrp_groups()
|
||||
)
|
||||
serializer_class = serializers.InterfaceSerializer
|
||||
filterset_class = filtersets.InterfaceFilterSet
|
||||
brief_prefetch_fields = ['device']
|
||||
|
||||
def get_bulk_destroy_queryset(self):
|
||||
# Ensure child interfaces are deleted prior to their parents
|
||||
@ -493,41 +448,36 @@ class InterfaceViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
||||
|
||||
class FrontPortViewSet(PassThroughPortMixin, NetBoxModelViewSet):
|
||||
queryset = FrontPort.objects.prefetch_related(
|
||||
'device__device_type__manufacturer', 'module__module_bay', 'rear_port', 'cable__terminations', 'tags'
|
||||
'cable__terminations',
|
||||
)
|
||||
serializer_class = serializers.FrontPortSerializer
|
||||
filterset_class = filtersets.FrontPortFilterSet
|
||||
brief_prefetch_fields = ['device']
|
||||
|
||||
|
||||
class RearPortViewSet(PassThroughPortMixin, NetBoxModelViewSet):
|
||||
queryset = RearPort.objects.prefetch_related(
|
||||
'device__device_type__manufacturer', 'module__module_bay', 'cable__terminations', 'tags'
|
||||
'cable__terminations',
|
||||
)
|
||||
serializer_class = serializers.RearPortSerializer
|
||||
filterset_class = filtersets.RearPortFilterSet
|
||||
brief_prefetch_fields = ['device']
|
||||
|
||||
|
||||
class ModuleBayViewSet(NetBoxModelViewSet):
|
||||
queryset = ModuleBay.objects.prefetch_related('tags', 'installed_module')
|
||||
queryset = ModuleBay.objects.all()
|
||||
serializer_class = serializers.ModuleBaySerializer
|
||||
filterset_class = filtersets.ModuleBayFilterSet
|
||||
brief_prefetch_fields = ['device']
|
||||
|
||||
|
||||
class DeviceBayViewSet(NetBoxModelViewSet):
|
||||
queryset = DeviceBay.objects.prefetch_related('installed_device', 'tags')
|
||||
queryset = DeviceBay.objects.all()
|
||||
serializer_class = serializers.DeviceBaySerializer
|
||||
filterset_class = filtersets.DeviceBayFilterSet
|
||||
brief_prefetch_fields = ['device']
|
||||
|
||||
|
||||
class InventoryItemViewSet(MPTTLockedMixin, NetBoxModelViewSet):
|
||||
queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer', 'tags')
|
||||
queryset = InventoryItem.objects.all()
|
||||
serializer_class = serializers.InventoryItemSerializer
|
||||
filterset_class = filtersets.InventoryItemFilterSet
|
||||
brief_prefetch_fields = ['device']
|
||||
|
||||
|
||||
#
|
||||
@ -535,9 +485,7 @@ class InventoryItemViewSet(MPTTLockedMixin, NetBoxModelViewSet):
|
||||
#
|
||||
|
||||
class InventoryItemRoleViewSet(NetBoxModelViewSet):
|
||||
queryset = InventoryItemRole.objects.prefetch_related('tags').annotate(
|
||||
inventoryitem_count=count_related(InventoryItem, 'role')
|
||||
)
|
||||
queryset = InventoryItemRole.objects.all()
|
||||
serializer_class = serializers.InventoryItemRoleSerializer
|
||||
filterset_class = filtersets.InventoryItemRoleFilterSet
|
||||
|
||||
@ -554,7 +502,7 @@ class CableViewSet(NetBoxModelViewSet):
|
||||
|
||||
class CableTerminationViewSet(NetBoxModelViewSet):
|
||||
metadata_class = ContentTypeMetadata
|
||||
queryset = CableTermination.objects.prefetch_related('cable', 'termination')
|
||||
queryset = CableTermination.objects.all()
|
||||
serializer_class = serializers.CableTerminationSerializer
|
||||
filterset_class = filtersets.CableTerminationFilterSet
|
||||
|
||||
@ -564,10 +512,9 @@ class CableTerminationViewSet(NetBoxModelViewSet):
|
||||
#
|
||||
|
||||
class VirtualChassisViewSet(NetBoxModelViewSet):
|
||||
queryset = VirtualChassis.objects.prefetch_related('tags')
|
||||
queryset = VirtualChassis.objects.all()
|
||||
serializer_class = serializers.VirtualChassisSerializer
|
||||
filterset_class = filtersets.VirtualChassisFilterSet
|
||||
brief_prefetch_fields = ['master']
|
||||
|
||||
|
||||
#
|
||||
@ -575,11 +522,7 @@ class VirtualChassisViewSet(NetBoxModelViewSet):
|
||||
#
|
||||
|
||||
class PowerPanelViewSet(NetBoxModelViewSet):
|
||||
queryset = PowerPanel.objects.prefetch_related(
|
||||
'site', 'location'
|
||||
).annotate(
|
||||
powerfeed_count=count_related(PowerFeed, 'power_panel')
|
||||
)
|
||||
queryset = PowerPanel.objects.all()
|
||||
serializer_class = serializers.PowerPanelSerializer
|
||||
filterset_class = filtersets.PowerPanelFilterSet
|
||||
|
||||
@ -590,7 +533,7 @@ class PowerPanelViewSet(NetBoxModelViewSet):
|
||||
|
||||
class PowerFeedViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
||||
queryset = PowerFeed.objects.prefetch_related(
|
||||
'power_panel', 'rack', '_path', 'cable__terminations', 'tags'
|
||||
'_path', 'cable__terminations',
|
||||
)
|
||||
serializer_class = serializers.PowerFeedSerializer
|
||||
filterset_class = filtersets.PowerFeedFilterSet
|
||||
|
@ -557,6 +557,9 @@ class DeviceBulkEditForm(NetBoxModelBulkEditForm):
|
||||
label=_('Device type'),
|
||||
queryset=DeviceType.objects.all(),
|
||||
required=False,
|
||||
context={
|
||||
'parent': 'manufacturer',
|
||||
},
|
||||
query_params={
|
||||
'manufacturer_id': '$manufacturer'
|
||||
}
|
||||
@ -640,6 +643,9 @@ class ModuleBulkEditForm(NetBoxModelBulkEditForm):
|
||||
required=False,
|
||||
query_params={
|
||||
'manufacturer_id': '$manufacturer'
|
||||
},
|
||||
context={
|
||||
'parent': 'manufacturer',
|
||||
}
|
||||
)
|
||||
status = forms.ChoiceField(
|
||||
|
@ -30,7 +30,9 @@ def get_cable_form(a_type, b_type):
|
||||
attrs[f'{cable_end}_terminations'] = DynamicModelMultipleChoiceField(
|
||||
queryset=term_cls.objects.all(),
|
||||
label=term_cls._meta.verbose_name.title(),
|
||||
disabled_indicator='_occupied',
|
||||
context={
|
||||
'disabled': '_occupied',
|
||||
},
|
||||
query_params={
|
||||
'device_id': f'$termination_{cable_end}_device',
|
||||
'kind': 'physical', # Exclude virtual interfaces
|
||||
@ -52,7 +54,9 @@ def get_cable_form(a_type, b_type):
|
||||
attrs[f'{cable_end}_terminations'] = DynamicModelMultipleChoiceField(
|
||||
queryset=term_cls.objects.all(),
|
||||
label=_('Power Feed'),
|
||||
disabled_indicator='_occupied',
|
||||
context={
|
||||
'disabled': '_occupied',
|
||||
},
|
||||
query_params={
|
||||
'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(
|
||||
queryset=term_cls.objects.all(),
|
||||
label=_('Side'),
|
||||
disabled_indicator='_occupied',
|
||||
context={
|
||||
'disabled': '_occupied',
|
||||
},
|
||||
query_params={
|
||||
'circuit_id': f'$termination_{cable_end}_circuit',
|
||||
}
|
||||
|
@ -426,7 +426,7 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
|
||||
widget=APISelect(
|
||||
api_url='/api/dcim/racks/{{rack}}/elevation/',
|
||||
attrs={
|
||||
'disabled-indicator': 'device',
|
||||
'ts-disabled-field': 'device',
|
||||
'data-dynamic-params': '[{"fieldName":"face","queryParam":"face"}]'
|
||||
},
|
||||
)
|
||||
@ -434,6 +434,9 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
|
||||
device_type = DynamicModelChoiceField(
|
||||
label=_('Device type'),
|
||||
queryset=DeviceType.objects.all(),
|
||||
context={
|
||||
'parent': 'manufacturer',
|
||||
},
|
||||
selector=True
|
||||
)
|
||||
role = DynamicModelChoiceField(
|
||||
@ -461,6 +464,9 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
|
||||
label=_('Virtual chassis'),
|
||||
queryset=VirtualChassis.objects.all(),
|
||||
required=False,
|
||||
context={
|
||||
'parent': 'master',
|
||||
},
|
||||
selector=True
|
||||
)
|
||||
vc_position = forms.IntegerField(
|
||||
@ -568,6 +574,9 @@ class ModuleForm(ModuleCommonForm, NetBoxModelForm):
|
||||
module_type = DynamicModelChoiceField(
|
||||
label=_('Module type'),
|
||||
queryset=ModuleType.objects.all(),
|
||||
context={
|
||||
'parent': 'manufacturer',
|
||||
},
|
||||
selector=True
|
||||
)
|
||||
comments = CommentField()
|
||||
@ -774,7 +783,10 @@ class VCMemberSelectForm(forms.Form):
|
||||
class ComponentTemplateForm(forms.ModelForm):
|
||||
device_type = DynamicModelChoiceField(
|
||||
label=_('Device type'),
|
||||
queryset=DeviceType.objects.all()
|
||||
queryset=DeviceType.objects.all(),
|
||||
context={
|
||||
'parent': 'manufacturer',
|
||||
}
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@ -789,12 +801,18 @@ class ModularComponentTemplateForm(ComponentTemplateForm):
|
||||
device_type = DynamicModelChoiceField(
|
||||
label=_('Device type'),
|
||||
queryset=DeviceType.objects.all().all(),
|
||||
required=False
|
||||
required=False,
|
||||
context={
|
||||
'parent': 'manufacturer',
|
||||
}
|
||||
)
|
||||
module_type = DynamicModelChoiceField(
|
||||
label=_('Module type'),
|
||||
queryset=ModuleType.objects.all(),
|
||||
required=False
|
||||
required=False,
|
||||
context={
|
||||
'parent': 'manufacturer',
|
||||
}
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
@ -233,7 +233,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='powerfeed',
|
||||
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(
|
||||
model_name='powerfeed',
|
||||
|
@ -84,6 +84,7 @@ class PowerFeed(PrimaryModel, PathEndpoint, CabledObjectModel):
|
||||
rack = models.ForeignKey(
|
||||
to='Rack',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='powerfeeds',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
|
@ -3,7 +3,6 @@ from django.core.exceptions import ObjectDoesNotExist
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from rest_framework import serializers
|
||||
from rest_framework.fields import ListField
|
||||
|
||||
from core.api.nested_serializers import NestedDataSourceSerializer, NestedDataFileSerializer, NestedJobSerializer
|
||||
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.models import *
|
||||
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.features import TaggableModelSerializer
|
||||
from netbox.constants import NESTED_SERIALIZER_PREFIX
|
||||
@ -285,7 +284,9 @@ class TagSerializer(ValidatedModelSerializer):
|
||||
many=True,
|
||||
required=False
|
||||
)
|
||||
tagged_items = serializers.IntegerField(read_only=True)
|
||||
|
||||
# Related object counts
|
||||
tagged_items = RelatedObjectCountField('extras_taggeditem_items')
|
||||
|
||||
class Meta:
|
||||
model = Tag
|
||||
|
@ -22,7 +22,7 @@ from netbox.api.metadata import ContentTypeMetadata
|
||||
from netbox.api.renderers import TextRenderer
|
||||
from netbox.api.viewsets import NetBoxModelViewSet
|
||||
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 .mixins import ConfigTemplateRenderMixin
|
||||
|
||||
@ -114,7 +114,7 @@ class CustomLinkViewSet(NetBoxModelViewSet):
|
||||
|
||||
class ExportTemplateViewSet(SyncedDataMixin, NetBoxModelViewSet):
|
||||
metadata_class = ContentTypeMetadata
|
||||
queryset = ExportTemplate.objects.prefetch_related('data_source', 'data_file')
|
||||
queryset = ExportTemplate.objects.all()
|
||||
serializer_class = serializers.ExportTemplateSerializer
|
||||
filterset_class = filtersets.ExportTemplateFilterSet
|
||||
|
||||
@ -146,9 +146,7 @@ class BookmarkViewSet(NetBoxModelViewSet):
|
||||
#
|
||||
|
||||
class TagViewSet(NetBoxModelViewSet):
|
||||
queryset = Tag.objects.annotate(
|
||||
tagged_items=count_related(TaggedItem, 'tag')
|
||||
)
|
||||
queryset = Tag.objects.all()
|
||||
serializer_class = serializers.TagSerializer
|
||||
filterset_class = filtersets.TagFilterSet
|
||||
|
||||
@ -180,10 +178,7 @@ class JournalEntryViewSet(NetBoxModelViewSet):
|
||||
#
|
||||
|
||||
class ConfigContextViewSet(SyncedDataMixin, NetBoxModelViewSet):
|
||||
queryset = ConfigContext.objects.prefetch_related(
|
||||
'regions', 'site_groups', 'sites', 'locations', 'roles', 'platforms', 'tenant_groups', 'tenants', 'data_source',
|
||||
'data_file',
|
||||
)
|
||||
queryset = ConfigContext.objects.all()
|
||||
serializer_class = serializers.ConfigContextSerializer
|
||||
filterset_class = filtersets.ConfigContextFilterSet
|
||||
|
||||
@ -193,7 +188,7 @@ class ConfigContextViewSet(SyncedDataMixin, NetBoxModelViewSet):
|
||||
#
|
||||
|
||||
class ConfigTemplateViewSet(SyncedDataMixin, ConfigTemplateRenderMixin, NetBoxModelViewSet):
|
||||
queryset = ConfigTemplate.objects.prefetch_related('data_source', 'data_file')
|
||||
queryset = ConfigTemplate.objects.all()
|
||||
serializer_class = serializers.ConfigTemplateSerializer
|
||||
filterset_class = filtersets.ConfigTemplateFilterSet
|
||||
|
||||
@ -283,7 +278,7 @@ class ObjectChangeViewSet(ReadOnlyModelViewSet):
|
||||
Retrieve a list of recent changes.
|
||||
"""
|
||||
metadata_class = ContentTypeMetadata
|
||||
queryset = ObjectChange.objects.valid_models().prefetch_related('user')
|
||||
queryset = ObjectChange.objects.valid_models()
|
||||
serializer_class = serializers.ObjectChangeSerializer
|
||||
filterset_class = filtersets.ObjectChangeFilterSet
|
||||
|
||||
|
@ -193,16 +193,19 @@ class ObjectVar(ScriptVariable):
|
||||
|
||||
: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 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)
|
||||
"""
|
||||
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)
|
||||
|
||||
self.field_attrs.update({
|
||||
'queryset': model.objects.all(),
|
||||
'query_params': query_params,
|
||||
'context': context,
|
||||
'null_option': null_option,
|
||||
})
|
||||
|
||||
|
@ -2,6 +2,7 @@ from drf_spectacular.utils import extend_schema_serializer
|
||||
from rest_framework import serializers
|
||||
|
||||
from ipam import models
|
||||
from netbox.api.fields import RelatedObjectCountField
|
||||
from netbox.api.serializers import WritableNestedSerializer
|
||||
from .field_serializers import IPAddressField
|
||||
|
||||
@ -58,7 +59,7 @@ class NestedASNSerializer(WritableNestedSerializer):
|
||||
)
|
||||
class NestedVRFSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vrf-detail')
|
||||
prefix_count = serializers.IntegerField(read_only=True)
|
||||
prefix_count = RelatedObjectCountField('prefixes')
|
||||
|
||||
class Meta:
|
||||
model = models.VRF
|
||||
@ -86,7 +87,7 @@ class NestedRouteTargetSerializer(WritableNestedSerializer):
|
||||
)
|
||||
class NestedRIRSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail')
|
||||
aggregate_count = serializers.IntegerField(read_only=True)
|
||||
aggregate_count = RelatedObjectCountField('aggregates')
|
||||
|
||||
class Meta:
|
||||
model = models.RIR
|
||||
@ -116,10 +117,11 @@ class NestedFHRPGroupSerializer(WritableNestedSerializer):
|
||||
|
||||
class NestedFHRPGroupAssignmentSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:fhrpgroupassignment-detail')
|
||||
group = NestedFHRPGroupSerializer()
|
||||
|
||||
class Meta:
|
||||
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):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:role-detail')
|
||||
prefix_count = serializers.IntegerField(read_only=True)
|
||||
vlan_count = serializers.IntegerField(read_only=True)
|
||||
prefix_count = RelatedObjectCountField('prefixes')
|
||||
vlan_count = RelatedObjectCountField('vlans')
|
||||
|
||||
class Meta:
|
||||
model = models.Role
|
||||
@ -144,7 +146,7 @@ class NestedRoleSerializer(WritableNestedSerializer):
|
||||
)
|
||||
class NestedVLANGroupSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail')
|
||||
vlan_count = serializers.IntegerField(read_only=True)
|
||||
vlan_count = RelatedObjectCountField('vlans')
|
||||
|
||||
class Meta:
|
||||
model = models.VLANGroup
|
||||
|
@ -6,7 +6,7 @@ from dcim.api.nested_serializers import NestedDeviceSerializer, NestedSiteSerial
|
||||
from ipam.choices import *
|
||||
from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS, VLANGROUP_SCOPE_TYPES
|
||||
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.constants import NESTED_SERIALIZER_PREFIX
|
||||
from tenancy.api.nested_serializers import NestedTenantSerializer
|
||||
@ -43,8 +43,10 @@ class ASNSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asn-detail')
|
||||
rir = NestedRIRSerializer(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:
|
||||
model = ASN
|
||||
@ -90,8 +92,10 @@ class VRFSerializer(NetBoxModelSerializer):
|
||||
required=False,
|
||||
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:
|
||||
model = VRF
|
||||
@ -124,7 +128,9 @@ class RouteTargetSerializer(NetBoxModelSerializer):
|
||||
|
||||
class RIRSerializer(NetBoxModelSerializer):
|
||||
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:
|
||||
model = RIR
|
||||
@ -195,8 +201,10 @@ class FHRPGroupAssignmentSerializer(NetBoxModelSerializer):
|
||||
|
||||
class RoleSerializer(NetBoxModelSerializer):
|
||||
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:
|
||||
model = Role
|
||||
@ -218,9 +226,11 @@ class VLANGroupSerializer(NetBoxModelSerializer):
|
||||
)
|
||||
scope_id = serializers.IntegerField(allow_null=True, required=False, default=None)
|
||||
scope = serializers.SerializerMethodField(read_only=True)
|
||||
vlan_count = serializers.IntegerField(read_only=True)
|
||||
utilization = serializers.CharField(read_only=True)
|
||||
|
||||
# Related object counts
|
||||
vlan_count = RelatedObjectCountField('vlans')
|
||||
|
||||
class Meta:
|
||||
model = VLANGroup
|
||||
fields = [
|
||||
@ -247,7 +257,9 @@ class VLANSerializer(NetBoxModelSerializer):
|
||||
status = ChoiceField(choices=VLANStatusChoices, required=False)
|
||||
role = NestedRoleSerializer(required=False, 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:
|
||||
model = VLAN
|
||||
|
@ -12,8 +12,6 @@ from rest_framework.response import Response
|
||||
from rest_framework.routers import APIRootView
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from circuits.models import Provider
|
||||
from dcim.models import Site
|
||||
from ipam import filtersets
|
||||
from ipam.models import *
|
||||
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.constants import ADVISORY_LOCK_KEYS
|
||||
from utilities.api import get_serializer_for_model
|
||||
from utilities.utils import count_related
|
||||
from . import serializers
|
||||
|
||||
|
||||
@ -39,64 +36,49 @@ class IPAMRootView(APIRootView):
|
||||
#
|
||||
|
||||
class ASNRangeViewSet(NetBoxModelViewSet):
|
||||
queryset = ASNRange.objects.prefetch_related('tenant', 'rir').all()
|
||||
queryset = ASNRange.objects.all()
|
||||
serializer_class = serializers.ASNRangeSerializer
|
||||
filterset_class = filtersets.ASNRangeFilterSet
|
||||
|
||||
|
||||
class ASNViewSet(NetBoxModelViewSet):
|
||||
queryset = ASN.objects.prefetch_related('tenant', 'rir').annotate(
|
||||
site_count=count_related(Site, 'asns'),
|
||||
provider_count=count_related(Provider, 'asns')
|
||||
)
|
||||
queryset = ASN.objects.all()
|
||||
serializer_class = serializers.ASNSerializer
|
||||
filterset_class = filtersets.ASNFilterSet
|
||||
|
||||
|
||||
class VRFViewSet(NetBoxModelViewSet):
|
||||
queryset = VRF.objects.prefetch_related('tenant').prefetch_related(
|
||||
'import_targets', 'export_targets', 'tags'
|
||||
).annotate(
|
||||
ipaddress_count=count_related(IPAddress, 'vrf'),
|
||||
prefix_count=count_related(Prefix, 'vrf')
|
||||
)
|
||||
queryset = VRF.objects.all()
|
||||
serializer_class = serializers.VRFSerializer
|
||||
filterset_class = filtersets.VRFFilterSet
|
||||
|
||||
|
||||
class RouteTargetViewSet(NetBoxModelViewSet):
|
||||
queryset = RouteTarget.objects.prefetch_related('tenant').prefetch_related('tags')
|
||||
queryset = RouteTarget.objects.all()
|
||||
serializer_class = serializers.RouteTargetSerializer
|
||||
filterset_class = filtersets.RouteTargetFilterSet
|
||||
|
||||
|
||||
class RIRViewSet(NetBoxModelViewSet):
|
||||
queryset = RIR.objects.annotate(
|
||||
aggregate_count=count_related(Aggregate, 'rir')
|
||||
).prefetch_related('tags')
|
||||
queryset = RIR.objects.all()
|
||||
serializer_class = serializers.RIRSerializer
|
||||
filterset_class = filtersets.RIRFilterSet
|
||||
|
||||
|
||||
class AggregateViewSet(NetBoxModelViewSet):
|
||||
queryset = Aggregate.objects.prefetch_related('rir').prefetch_related('tags')
|
||||
queryset = Aggregate.objects.all()
|
||||
serializer_class = serializers.AggregateSerializer
|
||||
filterset_class = filtersets.AggregateFilterSet
|
||||
|
||||
|
||||
class RoleViewSet(NetBoxModelViewSet):
|
||||
queryset = Role.objects.annotate(
|
||||
prefix_count=count_related(Prefix, 'role'),
|
||||
vlan_count=count_related(VLAN, 'role')
|
||||
).prefetch_related('tags')
|
||||
queryset = Role.objects.all()
|
||||
serializer_class = serializers.RoleSerializer
|
||||
filterset_class = filtersets.RoleFilterSet
|
||||
|
||||
|
||||
class PrefixViewSet(NetBoxModelViewSet):
|
||||
queryset = Prefix.objects.prefetch_related(
|
||||
'site', 'vrf__tenant', 'tenant', 'vlan', 'role', 'tags'
|
||||
)
|
||||
queryset = Prefix.objects.all()
|
||||
serializer_class = serializers.PrefixSerializer
|
||||
filterset_class = filtersets.PrefixFilterSet
|
||||
|
||||
@ -109,7 +91,7 @@ class PrefixViewSet(NetBoxModelViewSet):
|
||||
|
||||
|
||||
class IPRangeViewSet(NetBoxModelViewSet):
|
||||
queryset = IPRange.objects.prefetch_related('vrf', 'role', 'tenant', 'tags')
|
||||
queryset = IPRange.objects.all()
|
||||
serializer_class = serializers.IPRangeSerializer
|
||||
filterset_class = filtersets.IPRangeFilterSet
|
||||
|
||||
@ -117,9 +99,7 @@ class IPRangeViewSet(NetBoxModelViewSet):
|
||||
|
||||
|
||||
class IPAddressViewSet(NetBoxModelViewSet):
|
||||
queryset = IPAddress.objects.prefetch_related(
|
||||
'vrf__tenant', 'tenant', 'nat_inside', 'nat_outside', 'tags', 'assigned_object'
|
||||
)
|
||||
queryset = IPAddress.objects.all()
|
||||
serializer_class = serializers.IPAddressSerializer
|
||||
filterset_class = filtersets.IPAddressFilterSet
|
||||
|
||||
@ -137,44 +117,39 @@ class IPAddressViewSet(NetBoxModelViewSet):
|
||||
|
||||
|
||||
class FHRPGroupViewSet(NetBoxModelViewSet):
|
||||
queryset = FHRPGroup.objects.prefetch_related('ip_addresses', 'tags')
|
||||
queryset = FHRPGroup.objects.all()
|
||||
serializer_class = serializers.FHRPGroupSerializer
|
||||
filterset_class = filtersets.FHRPGroupFilterSet
|
||||
brief_prefetch_fields = ('ip_addresses',)
|
||||
|
||||
|
||||
class FHRPGroupAssignmentViewSet(NetBoxModelViewSet):
|
||||
queryset = FHRPGroupAssignment.objects.prefetch_related('group', 'interface')
|
||||
queryset = FHRPGroupAssignment.objects.all()
|
||||
serializer_class = serializers.FHRPGroupAssignmentSerializer
|
||||
filterset_class = filtersets.FHRPGroupAssignmentFilterSet
|
||||
|
||||
|
||||
class VLANGroupViewSet(NetBoxModelViewSet):
|
||||
queryset = VLANGroup.objects.annotate_utilization().prefetch_related('tags')
|
||||
queryset = VLANGroup.objects.annotate_utilization()
|
||||
serializer_class = serializers.VLANGroupSerializer
|
||||
filterset_class = filtersets.VLANGroupFilterSet
|
||||
|
||||
|
||||
class VLANViewSet(NetBoxModelViewSet):
|
||||
queryset = VLAN.objects.prefetch_related(
|
||||
'site', 'group', 'tenant', 'role', 'tags'
|
||||
).annotate(
|
||||
prefix_count=count_related(Prefix, 'vlan')
|
||||
'l2vpn_terminations', # Referenced by VLANSerializer.l2vpn_termination
|
||||
)
|
||||
serializer_class = serializers.VLANSerializer
|
||||
filterset_class = filtersets.VLANFilterSet
|
||||
|
||||
|
||||
class ServiceTemplateViewSet(NetBoxModelViewSet):
|
||||
queryset = ServiceTemplate.objects.prefetch_related('tags')
|
||||
queryset = ServiceTemplate.objects.all()
|
||||
serializer_class = serializers.ServiceTemplateSerializer
|
||||
filterset_class = filtersets.ServiceTemplateFilterSet
|
||||
|
||||
|
||||
class ServiceViewSet(NetBoxModelViewSet):
|
||||
queryset = Service.objects.prefetch_related(
|
||||
'device', 'virtual_machine', 'tags', 'ipaddresses'
|
||||
)
|
||||
queryset = Service.objects.all()
|
||||
serializer_class = serializers.ServiceSerializer
|
||||
filterset_class = filtersets.ServiceFilterSet
|
||||
|
||||
|
@ -267,14 +267,20 @@ class IPRangeForm(TenancyForm, NetBoxModelForm):
|
||||
|
||||
class IPAddressForm(TenancyForm, NetBoxModelForm):
|
||||
interface = DynamicModelChoiceField(
|
||||
label=_('Interface'),
|
||||
queryset=Interface.objects.all(),
|
||||
required=False,
|
||||
context={
|
||||
'parent': 'device',
|
||||
},
|
||||
selector=True,
|
||||
label=_('Interface'),
|
||||
)
|
||||
vminterface = DynamicModelChoiceField(
|
||||
queryset=VMInterface.objects.all(),
|
||||
required=False,
|
||||
context={
|
||||
'parent': 'virtual_machine',
|
||||
},
|
||||
selector=True,
|
||||
label=_('Interface'),
|
||||
)
|
||||
|
@ -760,7 +760,7 @@ class FHRPGroupTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class FHRPGroupAssignmentTest(APIViewTestCases.APIViewTestCase):
|
||||
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 = {
|
||||
'priority': 100,
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from netaddr import IPNetwork
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
@ -10,6 +10,7 @@ __all__ = (
|
||||
'ChoiceField',
|
||||
'ContentTypeField',
|
||||
'IPNetworkSerializer',
|
||||
'RelatedObjectCountField',
|
||||
'SerializedPKRelatedField',
|
||||
)
|
||||
|
||||
@ -135,3 +136,16 @@ class SerializedPKRelatedField(PrimaryKeyRelatedField):
|
||||
|
||||
def to_representation(self, value):
|
||||
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)
|
||||
|
@ -12,6 +12,15 @@ __all__ = (
|
||||
class BaseModelSerializer(serializers.ModelSerializer):
|
||||
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)
|
||||
def get_display(self, obj):
|
||||
return str(obj)
|
||||
|
@ -1,4 +1,5 @@
|
||||
import logging
|
||||
from functools import cached_property
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
|
||||
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.viewsets import GenericViewSet
|
||||
|
||||
from utilities.api import get_annotations_for_serializer, get_prefetches_for_serializer
|
||||
from utilities.exceptions import AbortRequest
|
||||
from . import mixins
|
||||
|
||||
@ -40,6 +42,33 @@ class BaseViewSet(GenericViewSet):
|
||||
if action := HTTP_ACTIONS[request.method]:
|
||||
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(
|
||||
mixins.BriefModeMixin,
|
||||
|
@ -30,7 +30,6 @@ class BriefModeMixin:
|
||||
GET /api/dcim/sites/?brief=True
|
||||
"""
|
||||
brief = False
|
||||
brief_prefetch_fields = []
|
||||
|
||||
def initialize_request(self, request, *args, **kwargs):
|
||||
# Annotate whether brief mode is active
|
||||
@ -53,22 +52,6 @@ class BriefModeMixin:
|
||||
|
||||
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:
|
||||
"""
|
||||
|
BIN
netbox/project-static/dist/netbox.js
vendored
BIN
netbox/project-static/dist/netbox.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js.map
vendored
BIN
netbox/project-static/dist/netbox.js.map
vendored
Binary file not shown.
@ -31,6 +31,15 @@ export class DynamicTomSelect extends TomSelect {
|
||||
// Glean the REST API endpoint URL from the <select> element
|
||||
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)
|
||||
const nullOption = this.input.getAttribute('data-null-option');
|
||||
if (nullOption) {
|
||||
@ -82,10 +91,20 @@ export class DynamicTomSelect extends TomSelect {
|
||||
// Make the API request
|
||||
fetch(url)
|
||||
.then(response => response.json())
|
||||
.then(json => {
|
||||
self.loadCallback(json.results, []);
|
||||
.then(apiData => {
|
||||
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(()=>{
|
||||
self.loadCallback([], []);
|
||||
self.loadCallback([], []);
|
||||
});
|
||||
|
||||
}
|
||||
@ -126,6 +145,27 @@ export class DynamicTomSelect extends TomSelect {
|
||||
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
|
||||
*/
|
||||
|
@ -10,12 +10,34 @@ const MAX_OPTIONS = 100;
|
||||
|
||||
// Render the HTML for a dropdown option
|
||||
function renderOption(data: TomOption, escape: typeof escape_html) {
|
||||
// If the option has a `_depth` property, indent its label
|
||||
if (typeof data._depth === 'number' && data._depth > 0) {
|
||||
return `<div>${'─'.repeat(data._depth)} ${escape(data[LABEL_FIELD])}</div>`;
|
||||
let html = '<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
|
||||
@ -30,16 +52,13 @@ export function initDynamicSelects(): void {
|
||||
// Disable local search (search is performed on the backend)
|
||||
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
|
||||
preload: 'focus',
|
||||
|
||||
// Define custom rendering functions
|
||||
render: {
|
||||
option: renderOption,
|
||||
item: renderItem,
|
||||
},
|
||||
|
||||
// By default, load() will be called only if query.length > 0
|
||||
|
@ -17,13 +17,18 @@ export function initStaticSelects(): void {
|
||||
|
||||
// Initialize color selection fields
|
||||
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')) {
|
||||
new TomSelect(select, {
|
||||
...config,
|
||||
render: {
|
||||
option: function (item: TomOption, escape: typeof escape_html) {
|
||||
return `<div style="background-color: #${escape(item.value)}">${escape(item.text)}</div>`;
|
||||
},
|
||||
option: renderColor,
|
||||
item: renderColor,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
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.constants import NESTED_SERIALIZER_PREFIX
|
||||
from tenancy.choices import ContactPriorityChoices
|
||||
@ -32,16 +32,18 @@ class TenantGroupSerializer(NestedGroupModelSerializer):
|
||||
class TenantSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenant-detail')
|
||||
group = NestedTenantGroupSerializer(required=False, allow_null=True)
|
||||
circuit_count = serializers.IntegerField(read_only=True)
|
||||
device_count = serializers.IntegerField(read_only=True)
|
||||
ipaddress_count = serializers.IntegerField(read_only=True)
|
||||
prefix_count = serializers.IntegerField(read_only=True)
|
||||
rack_count = serializers.IntegerField(read_only=True)
|
||||
site_count = serializers.IntegerField(read_only=True)
|
||||
virtualmachine_count = serializers.IntegerField(read_only=True)
|
||||
vlan_count = serializers.IntegerField(read_only=True)
|
||||
vrf_count = serializers.IntegerField(read_only=True)
|
||||
cluster_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
# Related object counts
|
||||
circuit_count = RelatedObjectCountField('circuits')
|
||||
device_count = RelatedObjectCountField('devices')
|
||||
rack_count = RelatedObjectCountField('racks')
|
||||
site_count = RelatedObjectCountField('sites')
|
||||
ipaddress_count = RelatedObjectCountField('ip_addresses')
|
||||
prefix_count = RelatedObjectCountField('prefixes')
|
||||
vlan_count = RelatedObjectCountField('vlans')
|
||||
vrf_count = RelatedObjectCountField('vrfs')
|
||||
virtualmachine_count = RelatedObjectCountField('virtual_machines')
|
||||
cluster_count = RelatedObjectCountField('clusters')
|
||||
|
||||
class Meta:
|
||||
model = Tenant
|
||||
|
@ -1,13 +1,8 @@
|
||||
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 tenancy import filtersets
|
||||
from tenancy.models import *
|
||||
from utilities.utils import count_related
|
||||
from virtualization.models import VirtualMachine, Cluster
|
||||
from . import serializers
|
||||
|
||||
|
||||
@ -30,26 +25,13 @@ class TenantGroupViewSet(MPTTLockedMixin, NetBoxModelViewSet):
|
||||
'group',
|
||||
'tenant_count',
|
||||
cumulative=True
|
||||
).prefetch_related('tags')
|
||||
)
|
||||
serializer_class = serializers.TenantGroupSerializer
|
||||
filterset_class = filtersets.TenantGroupFilterSet
|
||||
|
||||
|
||||
class TenantViewSet(NetBoxModelViewSet):
|
||||
queryset = Tenant.objects.prefetch_related(
|
||||
'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')
|
||||
)
|
||||
queryset = Tenant.objects.all()
|
||||
serializer_class = serializers.TenantSerializer
|
||||
filterset_class = filtersets.TenantFilterSet
|
||||
|
||||
@ -65,24 +47,24 @@ class ContactGroupViewSet(MPTTLockedMixin, NetBoxModelViewSet):
|
||||
'group',
|
||||
'contact_count',
|
||||
cumulative=True
|
||||
).prefetch_related('tags')
|
||||
)
|
||||
serializer_class = serializers.ContactGroupSerializer
|
||||
filterset_class = filtersets.ContactGroupFilterSet
|
||||
|
||||
|
||||
class ContactRoleViewSet(NetBoxModelViewSet):
|
||||
queryset = ContactRole.objects.prefetch_related('tags')
|
||||
queryset = ContactRole.objects.all()
|
||||
serializer_class = serializers.ContactRoleSerializer
|
||||
filterset_class = filtersets.ContactRoleFilterSet
|
||||
|
||||
|
||||
class ContactViewSet(NetBoxModelViewSet):
|
||||
queryset = Contact.objects.prefetch_related('group', 'tags')
|
||||
queryset = Contact.objects.all()
|
||||
serializer_class = serializers.ContactSerializer
|
||||
filterset_class = filtersets.ContactFilterSet
|
||||
|
||||
|
||||
class ContactAssignmentViewSet(NetBoxModelViewSet):
|
||||
queryset = ContactAssignment.objects.prefetch_related('content_type', 'object', 'contact', 'role', 'tags')
|
||||
queryset = ContactAssignment.objects.all()
|
||||
serializer_class = serializers.ContactAssignmentSerializer
|
||||
filterset_class = filtersets.ContactAssignmentFilterSet
|
||||
|
@ -34,7 +34,7 @@ class UsersRootView(APIRootView):
|
||||
#
|
||||
|
||||
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
|
||||
filterset_class = filtersets.UserFilterSet
|
||||
|
||||
@ -50,7 +50,7 @@ class GroupViewSet(NetBoxModelViewSet):
|
||||
#
|
||||
|
||||
class TokenViewSet(NetBoxModelViewSet):
|
||||
queryset = Token.objects.prefetch_related('user')
|
||||
queryset = Token.objects.all()
|
||||
serializer_class = serializers.TokenSerializer
|
||||
filterset_class = filtersets.TokenFilterSet
|
||||
|
||||
@ -86,7 +86,7 @@ class TokenProvisionView(APIView):
|
||||
#
|
||||
|
||||
class ObjectPermissionViewSet(NetBoxModelViewSet):
|
||||
queryset = ObjectPermission.objects.prefetch_related('object_types', 'groups', 'users')
|
||||
queryset = ObjectPermission.objects.all()
|
||||
serializer_class = serializers.ObjectPermissionSerializer
|
||||
filterset_class = filtersets.ObjectPermissionFilterSet
|
||||
|
||||
|
@ -2,16 +2,24 @@ import platform
|
||||
import sys
|
||||
|
||||
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.urls import reverse
|
||||
from rest_framework import status
|
||||
from rest_framework.serializers import Serializer
|
||||
from rest_framework.utils import formatting
|
||||
|
||||
from netbox.api.fields import RelatedObjectCountField
|
||||
from netbox.api.exceptions import GraphQLTypeNotFound, SerializerNotFound
|
||||
from utilities.utils import count_related
|
||||
from .utils import dynamic_import
|
||||
|
||||
__all__ = (
|
||||
'get_annotations_for_serializer',
|
||||
'get_graphql_type_for_model',
|
||||
'get_prefetches_for_serializer',
|
||||
'get_serializer_for_model',
|
||||
'get_view_name',
|
||||
'is_api_request',
|
||||
@ -89,6 +97,63 @@ def get_view_name(view, suffix=None):
|
||||
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):
|
||||
"""
|
||||
Handle exceptions and return a useful error message for REST API requests.
|
||||
|
@ -63,8 +63,19 @@ class DynamicModelChoiceMixin:
|
||||
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)
|
||||
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
|
||||
|
||||
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
|
||||
widget = widgets.APISelect
|
||||
@ -77,6 +88,7 @@ class DynamicModelChoiceMixin:
|
||||
initial_params=None,
|
||||
null_option=None,
|
||||
disabled_indicator=None,
|
||||
context=None,
|
||||
selector=False,
|
||||
**kwargs
|
||||
):
|
||||
@ -85,6 +97,7 @@ class DynamicModelChoiceMixin:
|
||||
self.initial_params = initial_params or {}
|
||||
self.null_option = null_option
|
||||
self.disabled_indicator = disabled_indicator
|
||||
self.context = context or {}
|
||||
self.selector = selector
|
||||
|
||||
super().__init__(queryset, **kwargs)
|
||||
@ -96,12 +109,17 @@ class DynamicModelChoiceMixin:
|
||||
if self.null_option is not None:
|
||||
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:
|
||||
attrs['disabled-indicator'] = self.disabled_indicator
|
||||
attrs['ts-disabled-field'] = self.disabled_indicator
|
||||
|
||||
# Attach any static query parameters
|
||||
if (len(self.query_params) > 0):
|
||||
if len(self.query_params) > 0:
|
||||
widget.add_query_params(self.query_params)
|
||||
|
||||
# Include object selector?
|
||||
|
@ -1,6 +1,7 @@
|
||||
from drf_spectacular.utils import extend_schema_serializer
|
||||
from rest_framework import serializers
|
||||
|
||||
from netbox.api.fields import RelatedObjectCountField
|
||||
from netbox.api.serializers import WritableNestedSerializer
|
||||
from virtualization.models import *
|
||||
|
||||
@ -23,7 +24,7 @@ __all__ = [
|
||||
)
|
||||
class NestedClusterTypeSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustertype-detail')
|
||||
cluster_count = serializers.IntegerField(read_only=True)
|
||||
cluster_count = RelatedObjectCountField('clusters')
|
||||
|
||||
class Meta:
|
||||
model = ClusterType
|
||||
@ -35,7 +36,7 @@ class NestedClusterTypeSerializer(WritableNestedSerializer):
|
||||
)
|
||||
class NestedClusterGroupSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustergroup-detail')
|
||||
cluster_count = serializers.IntegerField(read_only=True)
|
||||
cluster_count = RelatedObjectCountField('clusters')
|
||||
|
||||
class Meta:
|
||||
model = ClusterGroup
|
||||
@ -47,7 +48,7 @@ class NestedClusterGroupSerializer(WritableNestedSerializer):
|
||||
)
|
||||
class NestedClusterSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail')
|
||||
virtualmachine_count = serializers.IntegerField(read_only=True)
|
||||
virtualmachine_count = RelatedObjectCountField('virtual_machines')
|
||||
|
||||
class Meta:
|
||||
model = Cluster
|
||||
|
@ -8,7 +8,7 @@ from dcim.choices import InterfaceModeChoices
|
||||
from extras.api.nested_serializers import NestedConfigTemplateSerializer
|
||||
from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer, NestedVRFSerializer
|
||||
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 tenancy.api.nested_serializers import NestedTenantSerializer
|
||||
from virtualization.choices import *
|
||||
@ -23,7 +23,9 @@ from .nested_serializers import *
|
||||
|
||||
class ClusterTypeSerializer(NetBoxModelSerializer):
|
||||
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:
|
||||
model = ClusterType
|
||||
@ -35,7 +37,9 @@ class ClusterTypeSerializer(NetBoxModelSerializer):
|
||||
|
||||
class ClusterGroupSerializer(NetBoxModelSerializer):
|
||||
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:
|
||||
model = ClusterGroup
|
||||
@ -52,8 +56,10 @@ class ClusterSerializer(NetBoxModelSerializer):
|
||||
status = ChoiceField(choices=ClusterStatusChoices, required=False)
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
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:
|
||||
model = Cluster
|
||||
|
@ -1,10 +1,8 @@
|
||||
from rest_framework.routers import APIRootView
|
||||
|
||||
from dcim.models import Device
|
||||
from extras.api.mixins import ConfigContextQuerySetMixin, RenderConfigMixin
|
||||
from netbox.api.viewsets import NetBoxModelViewSet
|
||||
from utilities.query_functions import CollateAsChar
|
||||
from utilities.utils import count_related
|
||||
from virtualization import filtersets
|
||||
from virtualization.models import *
|
||||
from . import serializers
|
||||
@ -23,28 +21,19 @@ class VirtualizationRootView(APIRootView):
|
||||
#
|
||||
|
||||
class ClusterTypeViewSet(NetBoxModelViewSet):
|
||||
queryset = ClusterType.objects.annotate(
|
||||
cluster_count=count_related(Cluster, 'type')
|
||||
).prefetch_related('tags')
|
||||
queryset = ClusterType.objects.all()
|
||||
serializer_class = serializers.ClusterTypeSerializer
|
||||
filterset_class = filtersets.ClusterTypeFilterSet
|
||||
|
||||
|
||||
class ClusterGroupViewSet(NetBoxModelViewSet):
|
||||
queryset = ClusterGroup.objects.annotate(
|
||||
cluster_count=count_related(Cluster, 'group')
|
||||
).prefetch_related('tags')
|
||||
queryset = ClusterGroup.objects.all()
|
||||
serializer_class = serializers.ClusterGroupSerializer
|
||||
filterset_class = filtersets.ClusterGroupFilterSet
|
||||
|
||||
|
||||
class ClusterViewSet(NetBoxModelViewSet):
|
||||
queryset = Cluster.objects.prefetch_related(
|
||||
'type', 'group', 'tenant', 'site', 'tags'
|
||||
).annotate(
|
||||
device_count=count_related(Device, 'cluster'),
|
||||
virtualmachine_count=count_related(VirtualMachine, 'cluster')
|
||||
)
|
||||
queryset = Cluster.objects.all()
|
||||
serializer_class = serializers.ClusterSerializer
|
||||
filterset_class = filtersets.ClusterFilterSet
|
||||
|
||||
@ -54,10 +43,7 @@ class ClusterViewSet(NetBoxModelViewSet):
|
||||
#
|
||||
|
||||
class VirtualMachineViewSet(ConfigContextQuerySetMixin, RenderConfigMixin, NetBoxModelViewSet):
|
||||
queryset = VirtualMachine.objects.prefetch_related(
|
||||
'site', 'cluster', 'device', 'role', 'tenant', 'platform', 'primary_ip4', 'primary_ip6', 'config_template',
|
||||
'tags', 'virtualdisks',
|
||||
)
|
||||
queryset = VirtualMachine.objects.all()
|
||||
filterset_class = filtersets.VirtualMachineFilterSet
|
||||
|
||||
def get_serializer_class(self):
|
||||
@ -83,12 +69,12 @@ class VirtualMachineViewSet(ConfigContextQuerySetMixin, RenderConfigMixin, NetBo
|
||||
|
||||
class VMInterfaceViewSet(NetBoxModelViewSet):
|
||||
queryset = VMInterface.objects.prefetch_related(
|
||||
'virtual_machine', 'parent', 'tags', 'untagged_vlan', 'tagged_vlans', 'vrf', 'ip_addresses',
|
||||
'fhrp_group_assignments',
|
||||
'l2vpn_terminations', # Referenced by VMInterfaceSerializer.l2vpn_termination
|
||||
'ip_addresses', # Referenced by VMInterface.count_ipaddresses()
|
||||
'fhrp_group_assignments', # Referenced by VMInterface.count_fhrp_groups()
|
||||
)
|
||||
serializer_class = serializers.VMInterfaceSerializer
|
||||
filterset_class = filtersets.VMInterfaceFilterSet
|
||||
brief_prefetch_fields = ['virtual_machine']
|
||||
|
||||
def get_bulk_destroy_queryset(self):
|
||||
# Ensure child interfaces are deleted prior to their parents
|
||||
@ -96,9 +82,6 @@ class VMInterfaceViewSet(NetBoxModelViewSet):
|
||||
|
||||
|
||||
class VirtualDiskViewSet(NetBoxModelViewSet):
|
||||
queryset = VirtualDisk.objects.prefetch_related(
|
||||
'virtual_machine', 'tags',
|
||||
)
|
||||
queryset = VirtualDisk.objects.all()
|
||||
serializer_class = serializers.VirtualDiskSerializer
|
||||
filterset_class = filtersets.VirtualDiskFilterSet
|
||||
brief_prefetch_fields = ['virtual_machine']
|
||||
|
@ -1,6 +1,7 @@
|
||||
from drf_spectacular.utils import extend_schema_serializer
|
||||
from rest_framework import serializers
|
||||
|
||||
from netbox.api.fields import RelatedObjectCountField
|
||||
from netbox.api.serializers import WritableNestedSerializer
|
||||
from vpn import models
|
||||
|
||||
@ -23,7 +24,7 @@ __all__ = (
|
||||
)
|
||||
class NestedTunnelGroupSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='vpn-api:tunnelgroup-detail')
|
||||
tunnel_count = serializers.IntegerField(read_only=True)
|
||||
tunnel_count = RelatedObjectCountField('tunnels')
|
||||
|
||||
class Meta:
|
||||
model = models.TunnelGroup
|
||||
|
@ -4,7 +4,7 @@ from rest_framework import serializers
|
||||
|
||||
from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedRouteTargetSerializer
|
||||
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.constants import NESTED_SERIALIZER_PREFIX
|
||||
from tenancy.api.nested_serializers import NestedTenantSerializer
|
||||
@ -29,7 +29,9 @@ __all__ = (
|
||||
|
||||
class TunnelGroupSerializer(NetBoxModelSerializer):
|
||||
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:
|
||||
model = TunnelGroup
|
||||
@ -59,11 +61,14 @@ class TunnelSerializer(NetBoxModelSerializer):
|
||||
allow_null=True
|
||||
)
|
||||
|
||||
# Related object counts
|
||||
terminations_count = RelatedObjectCountField('terminations')
|
||||
|
||||
class Meta:
|
||||
model = Tunnel
|
||||
fields = (
|
||||
'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',
|
||||
)
|
||||
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
from rest_framework.routers import APIRootView
|
||||
|
||||
from netbox.api.viewsets import NetBoxModelViewSet
|
||||
from utilities.utils import count_related
|
||||
from vpn import filtersets
|
||||
from vpn.models import *
|
||||
from . import serializers
|
||||
@ -34,23 +33,19 @@ class VPNRootView(APIRootView):
|
||||
#
|
||||
|
||||
class TunnelGroupViewSet(NetBoxModelViewSet):
|
||||
queryset = TunnelGroup.objects.annotate(
|
||||
tunnel_count=count_related(Tunnel, 'group')
|
||||
)
|
||||
queryset = TunnelGroup.objects.all()
|
||||
serializer_class = serializers.TunnelGroupSerializer
|
||||
filterset_class = filtersets.TunnelGroupFilterSet
|
||||
|
||||
|
||||
class TunnelViewSet(NetBoxModelViewSet):
|
||||
queryset = Tunnel.objects.prefetch_related('ipsec_profile', 'tenant').annotate(
|
||||
terminations_count=count_related(TunnelTermination, 'tunnel')
|
||||
)
|
||||
queryset = Tunnel.objects.all()
|
||||
serializer_class = serializers.TunnelSerializer
|
||||
filterset_class = filtersets.TunnelFilterSet
|
||||
|
||||
|
||||
class TunnelTerminationViewSet(NetBoxModelViewSet):
|
||||
queryset = TunnelTermination.objects.prefetch_related('tunnel')
|
||||
queryset = TunnelTermination.objects.all()
|
||||
serializer_class = serializers.TunnelTerminationSerializer
|
||||
filterset_class = filtersets.TunnelTerminationFilterSet
|
||||
|
||||
@ -86,12 +81,12 @@ class IPSecProfileViewSet(NetBoxModelViewSet):
|
||||
|
||||
|
||||
class L2VPNViewSet(NetBoxModelViewSet):
|
||||
queryset = L2VPN.objects.prefetch_related('import_targets', 'export_targets', 'tenant', 'tags')
|
||||
queryset = L2VPN.objects.all()
|
||||
serializer_class = serializers.L2VPNSerializer
|
||||
filterset_class = filtersets.L2VPNFilterSet
|
||||
|
||||
|
||||
class L2VPNTerminationViewSet(NetBoxModelViewSet):
|
||||
queryset = L2VPNTermination.objects.prefetch_related('assigned_object')
|
||||
queryset = L2VPNTermination.objects.all()
|
||||
serializer_class = serializers.L2VPNTerminationSerializer
|
||||
filterset_class = filtersets.L2VPNTerminationFilterSet
|
||||
|
@ -27,12 +27,12 @@ class WirelessLANGroupViewSet(MPTTLockedMixin, NetBoxModelViewSet):
|
||||
|
||||
|
||||
class WirelessLANViewSet(NetBoxModelViewSet):
|
||||
queryset = WirelessLAN.objects.prefetch_related('vlan', 'tenant', 'tags')
|
||||
queryset = WirelessLAN.objects.all()
|
||||
serializer_class = serializers.WirelessLANSerializer
|
||||
filterset_class = filtersets.WirelessLANFilterSet
|
||||
|
||||
|
||||
class WirelessLinkViewSet(NetBoxModelViewSet):
|
||||
queryset = WirelessLink.objects.prefetch_related('interface_a', 'interface_b', 'tenant', 'tags')
|
||||
queryset = WirelessLink.objects.all()
|
||||
serializer_class = serializers.WirelessLinkSerializer
|
||||
filterset_class = filtersets.WirelessLinkFilterSet
|
||||
|
@ -108,7 +108,9 @@ class WirelessLinkForm(TenancyForm, NetBoxModelForm):
|
||||
'kind': 'wireless',
|
||||
'device_id': '$device_a',
|
||||
},
|
||||
disabled_indicator='_occupied',
|
||||
context={
|
||||
'disabled': '_occupied',
|
||||
},
|
||||
label=_('Interface')
|
||||
)
|
||||
site_b = DynamicModelChoiceField(
|
||||
@ -148,7 +150,9 @@ class WirelessLinkForm(TenancyForm, NetBoxModelForm):
|
||||
'kind': 'wireless',
|
||||
'device_id': '$device_b',
|
||||
},
|
||||
disabled_indicator='_occupied',
|
||||
context={
|
||||
'disabled': '_occupied',
|
||||
},
|
||||
label=_('Interface')
|
||||
)
|
||||
comments = CommentField()
|
||||
|
Loading…
Reference in New Issue
Block a user