From c146f5e1b56f7c0e2eecd932d94ba21e43936392 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 26 Feb 2024 16:49:30 -0500 Subject: [PATCH 01/47] Replace nested serializers with primary serializers where possible --- netbox/circuits/api/serializers.py | 27 +- netbox/core/api/nested_serializers.py | 5 +- netbox/core/api/serializers.py | 9 +- netbox/dcim/api/nested_serializers.py | 22 - netbox/dcim/api/serializers.py | 2313 +++++++++++----------- netbox/extras/api/mixins.py | 4 +- netbox/extras/api/serializers.py | 77 +- netbox/ipam/api/serializers.py | 117 +- netbox/netbox/api/serializers/base.py | 25 +- netbox/netbox/api/serializers/nested.py | 44 +- netbox/tenancy/api/serializers.py | 8 +- netbox/users/api/serializers.py | 5 +- netbox/utilities/api.py | 57 +- netbox/virtualization/api/serializers.py | 34 +- netbox/vpn/api/serializers.py | 197 +- netbox/wireless/api/serializers.py | 18 +- 16 files changed, 1525 insertions(+), 1437 deletions(-) diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py index 574924c4a..c9341853b 100644 --- a/netbox/circuits/api/serializers.py +++ b/netbox/circuits/api/serializers.py @@ -2,13 +2,12 @@ from rest_framework import serializers from circuits.choices import CircuitStatusChoices from circuits.models import * -from dcim.api.nested_serializers import NestedSiteSerializer -from dcim.api.serializers import CabledObjectSerializer +from dcim.api.serializers import CabledObjectSerializer, SiteSerializer from ipam.api.nested_serializers import NestedASNSerializer from ipam.models import ASN from netbox.api.fields import ChoiceField, RelatedObjectCountField, SerializedPKRelatedField from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer -from tenancy.api.nested_serializers import NestedTenantSerializer +from tenancy.api.serializers import TenantSerializer from .nested_serializers import * @@ -49,7 +48,7 @@ class ProviderSerializer(NetBoxModelSerializer): class ProviderAccountSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provideraccount-detail') - provider = NestedProviderSerializer() + provider = ProviderSerializer(nested=True) class Meta: model = ProviderAccount @@ -66,7 +65,7 @@ class ProviderAccountSerializer(NetBoxModelSerializer): class ProviderNetworkSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='circuits-api:providernetwork-detail') - provider = NestedProviderSerializer() + provider = ProviderSerializer(nested=True) class Meta: model = ProviderNetwork @@ -98,8 +97,8 @@ class CircuitTypeSerializer(NetBoxModelSerializer): class CircuitCircuitTerminationSerializer(WritableNestedSerializer): url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail') - site = NestedSiteSerializer(allow_null=True) - provider_network = NestedProviderNetworkSerializer(allow_null=True) + site = SiteSerializer(nested=True, allow_null=True) + provider_network = ProviderNetworkSerializer(nested=True, allow_null=True) class Meta: model = CircuitTermination @@ -111,11 +110,11 @@ class CircuitCircuitTerminationSerializer(WritableNestedSerializer): class CircuitSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail') - provider = NestedProviderSerializer() - provider_account = NestedProviderAccountSerializer(required=False, allow_null=True) + provider = ProviderSerializer(nested=True) + provider_account = ProviderAccountSerializer(nested=True, required=False, allow_null=True) status = ChoiceField(choices=CircuitStatusChoices, required=False) - type = NestedCircuitTypeSerializer() - tenant = NestedTenantSerializer(required=False, allow_null=True) + type = CircuitTypeSerializer(nested=True) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) termination_a = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True) termination_z = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True) @@ -131,9 +130,9 @@ class CircuitSerializer(NetBoxModelSerializer): class CircuitTerminationSerializer(NetBoxModelSerializer, CabledObjectSerializer): url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail') - circuit = NestedCircuitSerializer() - site = NestedSiteSerializer(required=False, allow_null=True) - provider_network = NestedProviderNetworkSerializer(required=False, allow_null=True) + circuit = CircuitSerializer(nested=True) + site = SiteSerializer(nested=True, required=False, allow_null=True) + provider_network = ProviderNetworkSerializer(nested=True, required=False, allow_null=True) class Meta: model = CircuitTermination diff --git a/netbox/core/api/nested_serializers.py b/netbox/core/api/nested_serializers.py index d99738cbe..efb748ee0 100644 --- a/netbox/core/api/nested_serializers.py +++ b/netbox/core/api/nested_serializers.py @@ -4,7 +4,7 @@ from core.choices import JobStatusChoices from core.models import * from netbox.api.fields import ChoiceField from netbox.api.serializers import WritableNestedSerializer -from users.api.nested_serializers import NestedUserSerializer +from users.api.serializers import UserSerializer __all__ = ( 'NestedDataFileSerializer', @@ -32,7 +32,8 @@ class NestedDataFileSerializer(WritableNestedSerializer): class NestedJobSerializer(serializers.ModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='core-api:job-detail') status = ChoiceField(choices=JobStatusChoices) - user = NestedUserSerializer( + user = UserSerializer( + nested=True, read_only=True ) diff --git a/netbox/core/api/serializers.py b/netbox/core/api/serializers.py index be3e9ff4a..3a2794304 100644 --- a/netbox/core/api/serializers.py +++ b/netbox/core/api/serializers.py @@ -5,8 +5,7 @@ from core.models import * 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 -from .nested_serializers import * +from users.api.serializers import UserSerializer __all__ = ( 'DataFileSerializer', @@ -43,7 +42,8 @@ class DataFileSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField( view_name='core-api:datafile-detail' ) - source = NestedDataSourceSerializer( + source = DataSourceSerializer( + nested=True, read_only=True ) @@ -57,7 +57,8 @@ class DataFileSerializer(NetBoxModelSerializer): class JobSerializer(BaseModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='core-api:job-detail') - user = NestedUserSerializer( + user = UserSerializer( + nested=True, read_only=True ) status = ChoiceField(choices=JobStatusChoices, read_only=True) diff --git a/netbox/dcim/api/nested_serializers.py b/netbox/dcim/api/nested_serializers.py index 9a59af8e2..1d9828ee3 100644 --- a/netbox/dcim/api/nested_serializers.py +++ b/netbox/dcim/api/nested_serializers.py @@ -6,8 +6,6 @@ from netbox.api.fields import RelatedObjectCountField from netbox.api.serializers import WritableNestedSerializer __all__ = [ - 'ComponentNestedModuleSerializer', - 'ModuleBayNestedModuleSerializer', 'NestedCableSerializer', 'NestedConsolePortSerializer', 'NestedConsolePortTemplateSerializer', @@ -311,26 +309,6 @@ class ModuleNestedModuleBaySerializer(WritableNestedSerializer): fields = ['id', 'url', 'display', 'name'] -class ModuleBayNestedModuleSerializer(WritableNestedSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail') - - class Meta: - model = models.Module - fields = ['id', 'url', 'display', 'serial'] - - -class ComponentNestedModuleSerializer(WritableNestedSerializer): - """ - Used by device component serializers. - """ - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail') - module_bay = ModuleNestedModuleBaySerializer(read_only=True) - - class Meta: - model = models.Module - fields = ['id', 'url', 'display', 'device', 'module_bay'] - - class NestedModuleSerializer(WritableNestedSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail') device = NestedDeviceSerializer(read_only=True) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 1bf4969e2..af0e4b2d6 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -10,7 +10,7 @@ from timezone_field.rest_framework import TimeZoneSerializerField from dcim.choices import * from dcim.constants import * from dcim.models import * -from extras.api.nested_serializers import NestedConfigTemplateSerializer +from extras.api.serializers import ConfigTemplateSerializer from ipam.api.nested_serializers import ( NestedASNSerializer, NestedIPAddressSerializer, NestedVLANSerializer, NestedVRFSerializer, ) @@ -22,8 +22,8 @@ from netbox.api.serializers import ( ) from netbox.config import ConfigItem from netbox.constants import NESTED_SERIALIZER_PREFIX -from tenancy.api.nested_serializers import NestedTenantSerializer -from users.api.nested_serializers import NestedUserSerializer +from tenancy.api.serializers import TenantSerializer +from users.api.serializers import UserSerializer from utilities.api import get_serializer_for_model from virtualization.api.nested_serializers import NestedClusterSerializer from vpn.api.nested_serializers import NestedL2VPNTerminationSerializer @@ -33,1126 +33,6 @@ from wireless.models import WirelessLAN from .nested_serializers import * -class CabledObjectSerializer(serializers.ModelSerializer): - cable = NestedCableSerializer(read_only=True, allow_null=True) - cable_end = serializers.CharField(read_only=True) - link_peers_type = serializers.SerializerMethodField(read_only=True) - link_peers = serializers.SerializerMethodField(read_only=True) - _occupied = serializers.SerializerMethodField(read_only=True) - - @extend_schema_field(OpenApiTypes.STR) - def get_link_peers_type(self, obj): - """ - Return the type of the peer link terminations, or None. - """ - if not obj.cable: - return None - - if obj.link_peers: - return f'{obj.link_peers[0]._meta.app_label}.{obj.link_peers[0]._meta.model_name}' - - return None - - @extend_schema_field(serializers.ListField) - def get_link_peers(self, obj): - """ - Return the appropriate serializer for the link termination model. - """ - if not obj.link_peers: - return [] - - # Return serialized peer termination objects - serializer = get_serializer_for_model(obj.link_peers[0], prefix=NESTED_SERIALIZER_PREFIX) - context = {'request': self.context['request']} - return serializer(obj.link_peers, context=context, many=True).data - - @extend_schema_field(serializers.BooleanField) - def get__occupied(self, obj): - return obj._occupied - - -class ConnectedEndpointsSerializer(serializers.ModelSerializer): - """ - Legacy serializer for pre-v3.3 connections - """ - connected_endpoints_type = serializers.SerializerMethodField(read_only=True) - connected_endpoints = serializers.SerializerMethodField(read_only=True) - connected_endpoints_reachable = serializers.SerializerMethodField(read_only=True) - - @extend_schema_field(OpenApiTypes.STR) - def get_connected_endpoints_type(self, obj): - if endpoints := obj.connected_endpoints: - return f'{endpoints[0]._meta.app_label}.{endpoints[0]._meta.model_name}' - - @extend_schema_field(serializers.ListField) - def get_connected_endpoints(self, obj): - """ - Return the appropriate serializer for the type of connected object. - """ - if endpoints := obj.connected_endpoints: - serializer = get_serializer_for_model(endpoints[0], prefix=NESTED_SERIALIZER_PREFIX) - context = {'request': self.context['request']} - return serializer(endpoints, many=True, context=context).data - - @extend_schema_field(serializers.BooleanField) - def get_connected_endpoints_reachable(self, obj): - return obj._path and obj._path.is_complete and obj._path.is_active - - -# -# Regions/sites -# - -class RegionSerializer(NestedGroupModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail') - parent = NestedRegionSerializer(required=False, allow_null=True, default=None) - site_count = serializers.IntegerField(read_only=True) - - class Meta: - model = Region - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created', - 'last_updated', 'site_count', '_depth', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'site_count', '_depth') - - -class SiteGroupSerializer(NestedGroupModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:sitegroup-detail') - parent = NestedSiteGroupSerializer(required=False, allow_null=True, default=None) - site_count = serializers.IntegerField(read_only=True) - - class Meta: - model = SiteGroup - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created', - 'last_updated', 'site_count', '_depth', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'site_count', '_depth') - - -class SiteSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:site-detail') - status = ChoiceField(choices=SiteStatusChoices, required=False) - region = NestedRegionSerializer(required=False, allow_null=True) - group = NestedSiteGroupSerializer(required=False, allow_null=True) - tenant = NestedTenantSerializer(required=False, allow_null=True) - time_zone = TimeZoneSerializerField(required=False, allow_null=True) - asns = SerializedPKRelatedField( - queryset=ASN.objects.all(), - serializer=NestedASNSerializer, - required=False, - many=True - ) - - # Related object counts - 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 - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'time_zone', - 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments', 'asns', 'tags', - 'custom_fields', 'created', 'last_updated', 'circuit_count', 'device_count', 'prefix_count', 'rack_count', - 'virtualmachine_count', 'vlan_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description', 'slug') - - -# -# Racks -# - -class LocationSerializer(NestedGroupModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:location-detail') - site = NestedSiteSerializer() - parent = NestedLocationSerializer(required=False, allow_null=True) - status = ChoiceField(choices=LocationStatusChoices, required=False) - tenant = NestedTenantSerializer(required=False, allow_null=True) - rack_count = serializers.IntegerField(read_only=True) - device_count = serializers.IntegerField(read_only=True) - - class Meta: - model = Location - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'site', 'parent', 'status', 'tenant', 'description', 'tags', - 'custom_fields', 'created', 'last_updated', 'rack_count', 'device_count', '_depth', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'rack_count', '_depth') - - -class RackRoleSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail') - - # Related object counts - rack_count = RelatedObjectCountField('racks') - - class Meta: - model = RackRole - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created', - 'last_updated', 'rack_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'rack_count') - - -class RackSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail') - site = NestedSiteSerializer() - location = NestedLocationSerializer(required=False, allow_null=True, default=None) - tenant = NestedTenantSerializer(required=False, allow_null=True) - status = ChoiceField(choices=RackStatusChoices, required=False) - role = NestedRackRoleSerializer(required=False, allow_null=True) - type = ChoiceField(choices=RackTypeChoices, allow_blank=True, required=False, allow_null=True) - facility_id = serializers.CharField(max_length=50, allow_blank=True, allow_null=True, label=_('Facility ID'), - default=None) - 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) - - # Related object counts - device_count = RelatedObjectCountField('devices') - powerfeed_count = RelatedObjectCountField('powerfeeds') - - class Meta: - model = Rack - fields = [ - 'id', 'url', 'display', 'name', 'facility_id', 'site', 'location', 'tenant', 'status', 'role', 'serial', - 'asset_tag', 'type', 'width', 'u_height', 'starting_unit', 'weight', 'max_weight', 'weight_unit', - 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'description', 'comments', - 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'powerfeed_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description', 'device_count') - - -class RackUnitSerializer(serializers.Serializer): - """ - A rack unit is an abstraction formed by the set (rack, position, face); it does not exist as a row in the database. - """ - id = serializers.DecimalField( - max_digits=4, - decimal_places=1, - read_only=True - ) - name = serializers.CharField(read_only=True) - face = ChoiceField(choices=DeviceFaceChoices, read_only=True) - device = NestedDeviceSerializer(read_only=True) - occupied = serializers.BooleanField(read_only=True) - display = serializers.SerializerMethodField(read_only=True) - - @extend_schema_field(OpenApiTypes.STR) - def get_display(self, obj): - return obj['name'] - - -class RackReservationSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackreservation-detail') - rack = NestedRackSerializer() - user = NestedUserSerializer() - tenant = NestedTenantSerializer(required=False, allow_null=True) - - class Meta: - model = RackReservation - fields = [ - 'id', 'url', 'display', 'rack', 'units', 'created', 'last_updated', 'user', 'tenant', 'description', - 'comments', 'tags', 'custom_fields', - ] - brief_fields = ('id', 'url', 'display', 'user', 'description', 'units') - - -class RackElevationDetailFilterSerializer(serializers.Serializer): - q = serializers.CharField( - required=False, - default=None - ) - face = serializers.ChoiceField( - choices=DeviceFaceChoices, - default=DeviceFaceChoices.FACE_FRONT - ) - render = serializers.ChoiceField( - choices=RackElevationDetailRenderChoices, - default=RackElevationDetailRenderChoices.RENDER_JSON - ) - unit_width = serializers.IntegerField( - default=ConfigItem('RACK_ELEVATION_DEFAULT_UNIT_WIDTH') - ) - unit_height = serializers.IntegerField( - default=ConfigItem('RACK_ELEVATION_DEFAULT_UNIT_HEIGHT') - ) - legend_width = serializers.IntegerField( - default=RACK_ELEVATION_DEFAULT_LEGEND_WIDTH - ) - margin_width = serializers.IntegerField( - default=RACK_ELEVATION_DEFAULT_MARGIN_WIDTH - ) - exclude = serializers.IntegerField( - required=False, - default=None - ) - expand_devices = serializers.BooleanField( - required=False, - default=True - ) - include_images = serializers.BooleanField( - required=False, - default=True - ) - - -# -# Device/module types -# - -class ManufacturerSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail') - - # Related object counts - devicetype_count = RelatedObjectCountField('device_types') - inventoryitem_count = RelatedObjectCountField('inventory_items') - platform_count = RelatedObjectCountField('platforms') - - class Meta: - model = Manufacturer - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated', - 'devicetype_count', 'inventoryitem_count', 'platform_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'devicetype_count') - - -class DeviceTypeSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail') - manufacturer = NestedManufacturerSerializer() - default_platform = NestedPlatformSerializer(required=False, allow_null=True) - u_height = serializers.DecimalField( - max_digits=4, - decimal_places=1, - label=_('Position (U)'), - min_value=0, - default=1.0 - ) - 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) - front_image = serializers.URLField(allow_null=True, required=False) - rear_image = serializers.URLField(allow_null=True, required=False) - - # Counter fields - console_port_template_count = serializers.IntegerField(read_only=True) - console_server_port_template_count = serializers.IntegerField(read_only=True) - power_port_template_count = serializers.IntegerField(read_only=True) - power_outlet_template_count = serializers.IntegerField(read_only=True) - interface_template_count = serializers.IntegerField(read_only=True) - front_port_template_count = serializers.IntegerField(read_only=True) - rear_port_template_count = serializers.IntegerField(read_only=True) - device_bay_template_count = serializers.IntegerField(read_only=True) - module_bay_template_count = serializers.IntegerField(read_only=True) - inventory_item_template_count = serializers.IntegerField(read_only=True) - - # Related object counts - device_count = RelatedObjectCountField('instances') - - class Meta: - model = DeviceType - fields = [ - 'id', 'url', 'display', 'manufacturer', 'default_platform', 'model', 'slug', 'part_number', 'u_height', - 'exclude_from_utilization', 'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit', - 'front_image', 'rear_image', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', - 'device_count', 'console_port_template_count', 'console_server_port_template_count', 'power_port_template_count', - 'power_outlet_template_count', 'interface_template_count', 'front_port_template_count', - 'rear_port_template_count', 'device_bay_template_count', 'module_bay_template_count', - 'inventory_item_template_count', - ] - brief_fields = ('id', 'url', 'display', 'manufacturer', 'model', 'slug', 'description', 'device_count') - - -class ModuleTypeSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:moduletype-detail') - manufacturer = NestedManufacturerSerializer() - weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True) - - class Meta: - model = ModuleType - fields = [ - 'id', 'url', 'display', 'manufacturer', 'model', 'part_number', 'weight', 'weight_unit', 'description', - 'comments', 'tags', 'custom_fields', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'manufacturer', 'model', 'description') - - -# -# Component templates -# - -class ConsolePortTemplateSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleporttemplate-detail') - device_type = NestedDeviceTypeSerializer( - required=False, - allow_null=True, - default=None - ) - module_type = NestedModuleTypeSerializer( - required=False, - allow_null=True, - default=None - ) - type = ChoiceField( - choices=ConsolePortTypeChoices, - allow_blank=True, - required=False - ) - - class Meta: - model = ConsolePortTemplate - fields = [ - 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'description', 'created', - 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverporttemplate-detail') - device_type = NestedDeviceTypeSerializer( - required=False, - allow_null=True, - default=None - ) - module_type = NestedModuleTypeSerializer( - required=False, - allow_null=True, - default=None - ) - type = ChoiceField( - choices=ConsolePortTypeChoices, - allow_blank=True, - required=False - ) - - class Meta: - model = ConsoleServerPortTemplate - fields = [ - 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'description', 'created', - 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -class PowerPortTemplateSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerporttemplate-detail') - device_type = NestedDeviceTypeSerializer( - required=False, - allow_null=True, - default=None - ) - module_type = NestedModuleTypeSerializer( - required=False, - allow_null=True, - default=None - ) - type = ChoiceField( - choices=PowerPortTypeChoices, - allow_blank=True, - required=False, - allow_null=True - ) - - class Meta: - model = PowerPortTemplate - fields = [ - 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw', - 'allocated_draw', 'description', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -class PowerOutletTemplateSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlettemplate-detail') - device_type = NestedDeviceTypeSerializer( - required=False, - allow_null=True, - default=None - ) - module_type = NestedModuleTypeSerializer( - required=False, - allow_null=True, - default=None - ) - type = ChoiceField( - choices=PowerOutletTypeChoices, - allow_blank=True, - required=False, - allow_null=True - ) - power_port = NestedPowerPortTemplateSerializer( - required=False, - allow_null=True - ) - feed_leg = ChoiceField( - choices=PowerOutletFeedLegChoices, - allow_blank=True, - required=False, - allow_null=True - ) - - class Meta: - model = PowerOutletTemplate - fields = [ - 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'power_port', 'feed_leg', - 'description', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -class InterfaceTemplateSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interfacetemplate-detail') - device_type = NestedDeviceTypeSerializer( - required=False, - allow_null=True, - default=None - ) - module_type = NestedModuleTypeSerializer( - required=False, - allow_null=True, - default=None - ) - type = ChoiceField(choices=InterfaceTypeChoices) - bridge = NestedInterfaceTemplateSerializer( - required=False, - allow_null=True - ) - poe_mode = ChoiceField( - choices=InterfacePoEModeChoices, - required=False, - allow_blank=True, - allow_null=True - ) - poe_type = ChoiceField( - choices=InterfacePoETypeChoices, - required=False, - allow_blank=True, - allow_null=True - ) - rf_role = ChoiceField( - choices=WirelessRoleChoices, - required=False, - allow_blank=True, - allow_null=True - ) - - class Meta: - model = InterfaceTemplate - fields = [ - 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'enabled', 'mgmt_only', - 'description', 'bridge', 'poe_mode', 'poe_type', 'rf_role', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -class RearPortTemplateSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearporttemplate-detail') - device_type = NestedDeviceTypeSerializer( - required=False, - allow_null=True, - default=None - ) - module_type = NestedModuleTypeSerializer( - required=False, - allow_null=True, - default=None - ) - type = ChoiceField(choices=PortTypeChoices) - - class Meta: - model = RearPortTemplate - fields = [ - 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', - 'description', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -class FrontPortTemplateSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontporttemplate-detail') - device_type = NestedDeviceTypeSerializer( - required=False, - allow_null=True, - default=None - ) - module_type = NestedModuleTypeSerializer( - required=False, - allow_null=True, - default=None - ) - type = ChoiceField(choices=PortTypeChoices) - rear_port = NestedRearPortTemplateSerializer() - - class Meta: - model = FrontPortTemplate - fields = [ - 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port', - 'rear_port_position', 'description', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -class ModuleBayTemplateSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebaytemplate-detail') - device_type = NestedDeviceTypeSerializer() - - class Meta: - model = ModuleBayTemplate - fields = [ - 'id', 'url', 'display', 'device_type', 'name', 'label', 'position', 'description', 'created', - 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -class DeviceBayTemplateSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebaytemplate-detail') - device_type = NestedDeviceTypeSerializer() - - class Meta: - model = DeviceBayTemplate - fields = ['id', 'url', 'display', 'device_type', 'name', 'label', 'description', 'created', 'last_updated'] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -class InventoryItemTemplateSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemtemplate-detail') - device_type = NestedDeviceTypeSerializer() - parent = serializers.PrimaryKeyRelatedField( - queryset=InventoryItemTemplate.objects.all(), - allow_null=True, - default=None - ) - role = NestedInventoryItemRoleSerializer(required=False, allow_null=True) - manufacturer = NestedManufacturerSerializer(required=False, allow_null=True, default=None) - component_type = ContentTypeField( - queryset=ContentType.objects.filter(MODULAR_COMPONENT_TEMPLATE_MODELS), - required=False, - allow_null=True - ) - component = serializers.SerializerMethodField(read_only=True) - _depth = serializers.IntegerField(source='level', read_only=True) - - class Meta: - model = InventoryItemTemplate - fields = [ - 'id', 'url', 'display', 'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', - 'description', 'component_type', 'component_id', 'component', 'created', 'last_updated', '_depth', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description', '_depth') - - @extend_schema_field(serializers.JSONField(allow_null=True)) - def get_component(self, obj): - if obj.component is None: - return None - serializer = get_serializer_for_model(obj.component, prefix=NESTED_SERIALIZER_PREFIX) - context = {'request': self.context['request']} - return serializer(obj.component, context=context).data - - -# -# Devices -# - -class DeviceRoleSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail') - config_template = NestedConfigTemplateSerializer(required=False, allow_null=True, default=None) - - # Related object counts - device_count = RelatedObjectCountField('devices') - virtualmachine_count = RelatedObjectCountField('virtual_machines') - - class Meta: - model = DeviceRole - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'color', 'vm_role', 'config_template', 'description', 'tags', - 'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'device_count', 'virtualmachine_count') - - -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) - - # Related object counts - device_count = RelatedObjectCountField('devices') - virtualmachine_count = RelatedObjectCountField('virtual_machines') - - class Meta: - model = Platform - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'manufacturer', 'config_template', 'description', 'tags', - 'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'device_count', 'virtualmachine_count') - - -class DeviceSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail') - device_type = NestedDeviceTypeSerializer() - role = NestedDeviceRoleSerializer() - device_role = NestedDeviceRoleSerializer(read_only=True, help_text='Deprecated in v3.6 in favor of `role`.') - tenant = NestedTenantSerializer(required=False, allow_null=True, default=None) - platform = NestedPlatformSerializer(required=False, allow_null=True) - site = NestedSiteSerializer() - location = NestedLocationSerializer(required=False, allow_null=True, default=None) - rack = NestedRackSerializer(required=False, allow_null=True, default=None) - face = ChoiceField(choices=DeviceFaceChoices, allow_blank=True, default=lambda: '') - position = serializers.DecimalField( - max_digits=4, - decimal_places=1, - allow_null=True, - label=_('Position (U)'), - min_value=decimal.Decimal(0.5), - default=None - ) - status = ChoiceField(choices=DeviceStatusChoices, required=False) - airflow = ChoiceField(choices=DeviceAirflowChoices, allow_blank=True, required=False) - primary_ip = NestedIPAddressSerializer(read_only=True) - primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True) - primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True) - oob_ip = NestedIPAddressSerializer(required=False, allow_null=True) - parent_device = serializers.SerializerMethodField() - cluster = NestedClusterSerializer(required=False, allow_null=True) - virtual_chassis = NestedVirtualChassisSerializer(required=False, allow_null=True, default=None) - vc_position = serializers.IntegerField(allow_null=True, max_value=255, min_value=0, default=None) - config_template = NestedConfigTemplateSerializer(required=False, allow_null=True, default=None) - - # Counter fields - console_port_count = serializers.IntegerField(read_only=True) - console_server_port_count = serializers.IntegerField(read_only=True) - power_port_count = serializers.IntegerField(read_only=True) - power_outlet_count = serializers.IntegerField(read_only=True) - interface_count = serializers.IntegerField(read_only=True) - front_port_count = serializers.IntegerField(read_only=True) - rear_port_count = serializers.IntegerField(read_only=True) - device_bay_count = serializers.IntegerField(read_only=True) - module_bay_count = serializers.IntegerField(read_only=True) - inventory_item_count = serializers.IntegerField(read_only=True) - - class Meta: - model = Device - fields = [ - 'id', 'url', 'display', 'name', 'device_type', 'role', 'device_role', 'tenant', 'platform', 'serial', - 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device', - 'status', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', - 'vc_position', 'vc_priority', 'description', 'comments', 'config_template', 'local_context_data', 'tags', - 'custom_fields', 'created', 'last_updated', 'console_port_count', 'console_server_port_count', - 'power_port_count', 'power_outlet_count', 'interface_count', 'front_port_count', 'rear_port_count', - 'device_bay_count', 'module_bay_count', 'inventory_item_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - @extend_schema_field(NestedDeviceSerializer) - def get_parent_device(self, obj): - try: - device_bay = obj.parent_bay - except DeviceBay.DoesNotExist: - return None - context = {'request': self.context['request']} - data = NestedDeviceSerializer(instance=device_bay.device, context=context).data - data['device_bay'] = NestedDeviceBaySerializer(instance=device_bay, context=context).data - return data - - def get_device_role(self, obj): - return obj.role - - -class DeviceWithConfigContextSerializer(DeviceSerializer): - config_context = serializers.SerializerMethodField(read_only=True) - - class Meta(DeviceSerializer.Meta): - fields = [ - 'id', 'url', 'display', 'name', 'device_type', 'role', 'device_role', 'tenant', 'platform', 'serial', - 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device', - 'status', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', - 'vc_position', 'vc_priority', 'description', 'comments', 'config_template', 'config_context', - 'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated', 'console_port_count', - 'console_server_port_count', 'power_port_count', 'power_outlet_count', 'interface_count', - 'front_port_count', 'rear_port_count', 'device_bay_count', 'module_bay_count', 'inventory_item_count', - ] - - @extend_schema_field(serializers.JSONField(allow_null=True)) - def get_config_context(self, obj): - return obj.get_config_context() - - -class VirtualDeviceContextSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualdevicecontext-detail') - device = NestedDeviceSerializer() - tenant = NestedTenantSerializer(required=False, allow_null=True, default=None) - primary_ip = NestedIPAddressSerializer(read_only=True, allow_null=True) - primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True) - primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True) - status = ChoiceField(choices=VirtualDeviceContextStatusChoices) - - # Related object counts - interface_count = RelatedObjectCountField('interfaces') - - class Meta: - model = VirtualDeviceContext - fields = [ - 'id', 'url', 'display', 'name', 'device', 'identifier', 'tenant', 'primary_ip', 'primary_ip4', - 'primary_ip6', 'status', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', - 'interface_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'identifier', 'device', 'description') - - -class ModuleSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail') - device = NestedDeviceSerializer() - module_bay = NestedModuleBaySerializer() - module_type = NestedModuleTypeSerializer() - status = ChoiceField(choices=ModuleStatusChoices, required=False) - - class Meta: - model = Module - fields = [ - 'id', 'url', 'display', 'device', 'module_bay', 'module_type', 'status', 'serial', 'asset_tag', - 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'device', 'module_bay', 'module_type', 'description') - - -# -# Device components -# - -class ConsoleServerPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail') - device = NestedDeviceSerializer() - module = ComponentNestedModuleSerializer( - required=False, - allow_null=True - ) - type = ChoiceField( - choices=ConsolePortTypeChoices, - allow_blank=True, - required=False - ) - speed = ChoiceField( - choices=ConsolePortSpeedChoices, - allow_null=True, - required=False - ) - - class Meta: - model = ConsoleServerPort - fields = [ - 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'speed', 'description', - 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'connected_endpoints', - 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created', - 'last_updated', '_occupied', - ] - brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') - - -class ConsolePortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail') - device = NestedDeviceSerializer() - module = ComponentNestedModuleSerializer( - required=False, - allow_null=True - ) - type = ChoiceField( - choices=ConsolePortTypeChoices, - allow_blank=True, - required=False - ) - speed = ChoiceField( - choices=ConsolePortSpeedChoices, - allow_null=True, - required=False - ) - - class Meta: - model = ConsolePort - fields = [ - 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'speed', 'description', - 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'connected_endpoints', - 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created', - 'last_updated', '_occupied', - ] - brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') - - -class PowerOutletSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail') - device = NestedDeviceSerializer() - module = ComponentNestedModuleSerializer( - required=False, - allow_null=True - ) - type = ChoiceField( - choices=PowerOutletTypeChoices, - allow_blank=True, - required=False, - allow_null=True - ) - power_port = NestedPowerPortSerializer( - required=False, - allow_null=True - ) - feed_leg = ChoiceField( - choices=PowerOutletFeedLegChoices, - allow_blank=True, - required=False, - allow_null=True - ) - - class Meta: - model = PowerOutlet - fields = [ - 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'power_port', 'feed_leg', - 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', - 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', - 'created', 'last_updated', '_occupied', - ] - brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') - - -class PowerPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail') - device = NestedDeviceSerializer() - module = ComponentNestedModuleSerializer( - required=False, - allow_null=True - ) - type = ChoiceField( - choices=PowerPortTypeChoices, - allow_blank=True, - required=False, - allow_null=True - ) - - class Meta: - model = PowerPort - fields = [ - 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', - 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', - 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', - 'created', 'last_updated', '_occupied', - ] - brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') - - -class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail') - device = NestedDeviceSerializer() - vdcs = SerializedPKRelatedField( - queryset=VirtualDeviceContext.objects.all(), - serializer=NestedVirtualDeviceContextSerializer, - required=False, - many=True - ) - module = ComponentNestedModuleSerializer( - required=False, - allow_null=True - ) - type = ChoiceField(choices=InterfaceTypeChoices) - parent = NestedInterfaceSerializer(required=False, allow_null=True) - bridge = NestedInterfaceSerializer(required=False, allow_null=True) - lag = NestedInterfaceSerializer(required=False, allow_null=True) - mode = ChoiceField(choices=InterfaceModeChoices, required=False, allow_blank=True) - duplex = ChoiceField(choices=InterfaceDuplexChoices, required=False, allow_blank=True, allow_null=True) - rf_role = ChoiceField(choices=WirelessRoleChoices, required=False, allow_blank=True) - rf_channel = ChoiceField(choices=WirelessChannelChoices, required=False, allow_blank=True) - poe_mode = ChoiceField(choices=InterfacePoEModeChoices, required=False, allow_blank=True) - poe_type = ChoiceField(choices=InterfacePoETypeChoices, required=False, allow_blank=True) - untagged_vlan = NestedVLANSerializer(required=False, allow_null=True) - tagged_vlans = SerializedPKRelatedField( - queryset=VLAN.objects.all(), - serializer=NestedVLANSerializer, - required=False, - many=True - ) - vrf = NestedVRFSerializer(required=False, allow_null=True) - l2vpn_termination = NestedL2VPNTerminationSerializer(read_only=True, allow_null=True) - wireless_link = NestedWirelessLinkSerializer(read_only=True, allow_null=True) - wireless_lans = SerializedPKRelatedField( - queryset=WirelessLAN.objects.all(), - serializer=NestedWirelessLANSerializer, - required=False, - many=True - ) - count_ipaddresses = serializers.IntegerField(read_only=True) - count_fhrp_groups = serializers.IntegerField(read_only=True) - mac_address = serializers.CharField( - required=False, - default=None, - allow_blank=True, - allow_null=True - ) - wwn = serializers.CharField(required=False, default=None, allow_blank=True, allow_null=True) - - class Meta: - model = Interface - fields = [ - 'id', 'url', 'display', 'device', 'vdcs', 'module', 'name', 'label', 'type', 'enabled', 'parent', 'bridge', - 'lag', 'mtu', 'mac_address', 'speed', 'duplex', 'wwn', 'mgmt_only', 'description', 'mode', 'rf_role', - 'rf_channel', 'poe_mode', 'poe_type', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', - 'untagged_vlan', 'tagged_vlans', 'mark_connected', 'cable', 'cable_end', 'wireless_link', 'link_peers', - 'link_peers_type', 'wireless_lans', 'vrf', 'l2vpn_termination', 'connected_endpoints', - 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created', - 'last_updated', 'count_ipaddresses', 'count_fhrp_groups', '_occupied', - ] - brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') - - def validate(self, data): - - # Validate many-to-many VLAN assignments - device = self.instance.device if self.instance else data.get('device') - for vlan in data.get('tagged_vlans', []): - if vlan.site not in [device.site, None]: - raise serializers.ValidationError({ - 'tagged_vlans': f"VLAN {vlan} must belong to the same site as the interface's parent device, or " - f"it must be global." - }) - - return super().validate(data) - - -class RearPortSerializer(NetBoxModelSerializer, CabledObjectSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail') - device = NestedDeviceSerializer() - module = ComponentNestedModuleSerializer( - required=False, - allow_null=True - ) - type = ChoiceField(choices=PortTypeChoices) - - class Meta: - model = RearPort - fields = [ - 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'description', - 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'tags', 'custom_fields', 'created', - 'last_updated', '_occupied', - ] - brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') - - -class FrontPortRearPortSerializer(WritableNestedSerializer): - """ - NestedRearPortSerializer but with parent device omitted (since front and rear ports must belong to same device) - """ - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail') - - class Meta: - model = RearPort - fields = ['id', 'url', 'display', 'name', 'label', 'description'] - - -class FrontPortSerializer(NetBoxModelSerializer, CabledObjectSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail') - device = NestedDeviceSerializer() - module = ComponentNestedModuleSerializer( - required=False, - allow_null=True - ) - type = ChoiceField(choices=PortTypeChoices) - rear_port = FrontPortRearPortSerializer() - - class Meta: - model = FrontPort - fields = [ - 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', - 'rear_port_position', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', - 'link_peers_type', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied', - ] - brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') - - -class ModuleBaySerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail') - device = NestedDeviceSerializer() - installed_module = ModuleBayNestedModuleSerializer(required=False, allow_null=True) - - class Meta: - model = ModuleBay - fields = [ - 'id', 'url', 'display', 'device', 'name', 'installed_module', 'label', 'position', 'description', 'tags', - 'custom_fields', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'installed_module', 'name', 'description') - - -class DeviceBaySerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail') - device = NestedDeviceSerializer() - installed_device = NestedDeviceSerializer(required=False, allow_null=True) - - class Meta: - model = DeviceBay - fields = [ - 'id', 'url', 'display', 'device', 'name', 'label', 'description', 'installed_device', 'tags', - 'custom_fields', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'device', 'name', 'description') - - -class InventoryItemSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitem-detail') - device = NestedDeviceSerializer() - parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None) - role = NestedInventoryItemRoleSerializer(required=False, allow_null=True) - manufacturer = NestedManufacturerSerializer(required=False, allow_null=True, default=None) - component_type = ContentTypeField( - queryset=ContentType.objects.filter(MODULAR_COMPONENT_MODELS), - required=False, - allow_null=True - ) - component = serializers.SerializerMethodField(read_only=True) - _depth = serializers.IntegerField(source='level', read_only=True) - - class Meta: - model = InventoryItem - fields = [ - 'id', 'url', 'display', 'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', - 'asset_tag', 'discovered', 'description', 'component_type', 'component_id', 'component', 'tags', - 'custom_fields', 'created', 'last_updated', '_depth', - ] - brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', '_depth') - - @extend_schema_field(serializers.JSONField(allow_null=True)) - def get_component(self, obj): - if obj.component is None: - return None - serializer = get_serializer_for_model(obj.component, prefix=NESTED_SERIALIZER_PREFIX) - context = {'request': self.context['request']} - return serializer(obj.component, context=context).data - - -# -# Device component roles -# - -class InventoryItemRoleSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemrole-detail') - - # Related object counts - inventoryitem_count = RelatedObjectCountField('inventory_items') - - class Meta: - model = InventoryItemRole - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created', - 'last_updated', 'inventoryitem_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'inventoryitem_count') - - # # Cables # @@ -1162,7 +42,7 @@ class CableSerializer(NetBoxModelSerializer): a_terminations = GenericObjectSerializer(many=True, required=False) b_terminations = GenericObjectSerializer(many=True, required=False) status = ChoiceField(choices=LinkStatusChoices, required=False) - tenant = NestedTenantSerializer(required=False, allow_null=True) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) length_unit = ChoiceField(choices=CableLengthUnitChoices, allow_blank=True, required=False, allow_null=True) class Meta: @@ -1225,6 +105,660 @@ class CablePathSerializer(serializers.ModelSerializer): return ret +class CabledObjectSerializer(serializers.ModelSerializer): + cable = CableSerializer(nested=True, read_only=True, allow_null=True) + cable_end = serializers.CharField(read_only=True) + link_peers_type = serializers.SerializerMethodField(read_only=True) + link_peers = serializers.SerializerMethodField(read_only=True) + _occupied = serializers.SerializerMethodField(read_only=True) + + @extend_schema_field(OpenApiTypes.STR) + def get_link_peers_type(self, obj): + """ + Return the type of the peer link terminations, or None. + """ + if not obj.cable: + return None + + if obj.link_peers: + return f'{obj.link_peers[0]._meta.app_label}.{obj.link_peers[0]._meta.model_name}' + + return None + + @extend_schema_field(serializers.ListField) + def get_link_peers(self, obj): + """ + Return the appropriate serializer for the link termination model. + """ + if not obj.link_peers: + return [] + + # Return serialized peer termination objects + serializer = get_serializer_for_model(obj.link_peers[0], prefix=NESTED_SERIALIZER_PREFIX) + context = {'request': self.context['request']} + return serializer(obj.link_peers, context=context, many=True).data + + @extend_schema_field(serializers.BooleanField) + def get__occupied(self, obj): + return obj._occupied + + +# +# Regions/sites +# + +class RegionSerializer(NestedGroupModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail') + parent = NestedRegionSerializer(required=False, allow_null=True, default=None) + site_count = serializers.IntegerField(read_only=True) + + class Meta: + model = Region + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created', + 'last_updated', 'site_count', '_depth', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'site_count', '_depth') + + +class SiteGroupSerializer(NestedGroupModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:sitegroup-detail') + parent = NestedSiteGroupSerializer(required=False, allow_null=True, default=None) + site_count = serializers.IntegerField(read_only=True) + + class Meta: + model = SiteGroup + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created', + 'last_updated', 'site_count', '_depth', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'site_count', '_depth') + + +class SiteSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:site-detail') + status = ChoiceField(choices=SiteStatusChoices, required=False) + region = RegionSerializer(nested=True, required=False, allow_null=True) + group = SiteGroupSerializer(nested=True, required=False, allow_null=True) + tenant = TenantSerializer(required=False, allow_null=True) + time_zone = TimeZoneSerializerField(required=False, allow_null=True) + asns = SerializedPKRelatedField( + queryset=ASN.objects.all(), + serializer=NestedASNSerializer, + required=False, + many=True + ) + + # Related object counts + 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 + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'time_zone', + 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments', 'asns', 'tags', + 'custom_fields', 'created', 'last_updated', 'circuit_count', 'device_count', 'prefix_count', 'rack_count', + 'virtualmachine_count', 'vlan_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description', 'slug') + + +# +# Racks +# + +class LocationSerializer(NestedGroupModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:location-detail') + site = SiteSerializer(nested=True) + parent = NestedLocationSerializer(required=False, allow_null=True) + status = ChoiceField(choices=LocationStatusChoices, required=False) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) + rack_count = serializers.IntegerField(read_only=True) + device_count = serializers.IntegerField(read_only=True) + + class Meta: + model = Location + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'site', 'parent', 'status', 'tenant', 'description', 'tags', + 'custom_fields', 'created', 'last_updated', 'rack_count', 'device_count', '_depth', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'rack_count', '_depth') + + +class RackRoleSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail') + + # Related object counts + rack_count = RelatedObjectCountField('racks') + + class Meta: + model = RackRole + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created', + 'last_updated', 'rack_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'rack_count') + + +class RackSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail') + site = SiteSerializer(nested=True) + location = LocationSerializer(nested=True, required=False, allow_null=True, default=None) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) + status = ChoiceField(choices=RackStatusChoices, required=False) + role = RackRoleSerializer(nested=True, required=False, allow_null=True) + type = ChoiceField(choices=RackTypeChoices, allow_blank=True, required=False, allow_null=True) + facility_id = serializers.CharField(max_length=50, allow_blank=True, allow_null=True, label=_('Facility ID'), + default=None) + 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) + + # Related object counts + device_count = RelatedObjectCountField('devices') + powerfeed_count = RelatedObjectCountField('powerfeeds') + + class Meta: + model = Rack + fields = [ + 'id', 'url', 'display', 'name', 'facility_id', 'site', 'location', 'tenant', 'status', 'role', 'serial', + 'asset_tag', 'type', 'width', 'u_height', 'starting_unit', 'weight', 'max_weight', 'weight_unit', + 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'description', 'comments', + 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'powerfeed_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description', 'device_count') + + +class RackReservationSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackreservation-detail') + rack = RackSerializer(nested=True) + user = UserSerializer(nested=True) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) + + class Meta: + model = RackReservation + fields = [ + 'id', 'url', 'display', 'rack', 'units', 'created', 'last_updated', 'user', 'tenant', 'description', + 'comments', 'tags', 'custom_fields', + ] + brief_fields = ('id', 'url', 'display', 'user', 'description', 'units') + + +class RackElevationDetailFilterSerializer(serializers.Serializer): + q = serializers.CharField( + required=False, + default=None + ) + face = serializers.ChoiceField( + choices=DeviceFaceChoices, + default=DeviceFaceChoices.FACE_FRONT + ) + render = serializers.ChoiceField( + choices=RackElevationDetailRenderChoices, + default=RackElevationDetailRenderChoices.RENDER_JSON + ) + unit_width = serializers.IntegerField( + default=ConfigItem('RACK_ELEVATION_DEFAULT_UNIT_WIDTH') + ) + unit_height = serializers.IntegerField( + default=ConfigItem('RACK_ELEVATION_DEFAULT_UNIT_HEIGHT') + ) + legend_width = serializers.IntegerField( + default=RACK_ELEVATION_DEFAULT_LEGEND_WIDTH + ) + margin_width = serializers.IntegerField( + default=RACK_ELEVATION_DEFAULT_MARGIN_WIDTH + ) + exclude = serializers.IntegerField( + required=False, + default=None + ) + expand_devices = serializers.BooleanField( + required=False, + default=True + ) + include_images = serializers.BooleanField( + required=False, + default=True + ) + + +class ManufacturerSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail') + + # Related object counts + devicetype_count = RelatedObjectCountField('device_types') + inventoryitem_count = RelatedObjectCountField('inventory_items') + platform_count = RelatedObjectCountField('platforms') + + class Meta: + model = Manufacturer + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated', + 'devicetype_count', 'inventoryitem_count', 'platform_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'devicetype_count') + + +class DeviceRoleSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail') + config_template = ConfigTemplateSerializer(nested=True, required=False, allow_null=True, default=None) + + # Related object counts + device_count = RelatedObjectCountField('devices') + virtualmachine_count = RelatedObjectCountField('virtual_machines') + + class Meta: + model = DeviceRole + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'color', 'vm_role', 'config_template', 'description', 'tags', + 'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'device_count', 'virtualmachine_count') + + +class InventoryItemRoleSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemrole-detail') + + # Related object counts + inventoryitem_count = RelatedObjectCountField('inventory_items') + + class Meta: + model = InventoryItemRole + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created', + 'last_updated', 'inventoryitem_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'inventoryitem_count') + + +class PlatformSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail') + manufacturer = ManufacturerSerializer(nested=True, required=False, allow_null=True) + config_template = ConfigTemplateSerializer(nested=True, required=False, allow_null=True, default=None) + + # Related object counts + device_count = RelatedObjectCountField('devices') + virtualmachine_count = RelatedObjectCountField('virtual_machines') + + class Meta: + model = Platform + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'manufacturer', 'config_template', 'description', 'tags', + 'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'device_count', 'virtualmachine_count') + + +# +# Device/module types +# + +class DeviceTypeSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail') + manufacturer = ManufacturerSerializer(nested=True) + default_platform = PlatformSerializer(nested=True, required=False, allow_null=True) + u_height = serializers.DecimalField( + max_digits=4, + decimal_places=1, + label=_('Position (U)'), + min_value=0, + default=1.0 + ) + 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) + front_image = serializers.URLField(allow_null=True, required=False) + rear_image = serializers.URLField(allow_null=True, required=False) + + # Counter fields + console_port_template_count = serializers.IntegerField(read_only=True) + console_server_port_template_count = serializers.IntegerField(read_only=True) + power_port_template_count = serializers.IntegerField(read_only=True) + power_outlet_template_count = serializers.IntegerField(read_only=True) + interface_template_count = serializers.IntegerField(read_only=True) + front_port_template_count = serializers.IntegerField(read_only=True) + rear_port_template_count = serializers.IntegerField(read_only=True) + device_bay_template_count = serializers.IntegerField(read_only=True) + module_bay_template_count = serializers.IntegerField(read_only=True) + inventory_item_template_count = serializers.IntegerField(read_only=True) + + # Related object counts + device_count = RelatedObjectCountField('instances') + + class Meta: + model = DeviceType + fields = [ + 'id', 'url', 'display', 'manufacturer', 'default_platform', 'model', 'slug', 'part_number', 'u_height', + 'exclude_from_utilization', 'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit', + 'front_image', 'rear_image', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', + 'device_count', 'console_port_template_count', 'console_server_port_template_count', 'power_port_template_count', + 'power_outlet_template_count', 'interface_template_count', 'front_port_template_count', + 'rear_port_template_count', 'device_bay_template_count', 'module_bay_template_count', + 'inventory_item_template_count', + ] + brief_fields = ('id', 'url', 'display', 'manufacturer', 'model', 'slug', 'description', 'device_count') + + +class ModuleTypeSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:moduletype-detail') + manufacturer = ManufacturerSerializer(nested=True) + weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True) + + class Meta: + model = ModuleType + fields = [ + 'id', 'url', 'display', 'manufacturer', 'model', 'part_number', 'weight', 'weight_unit', 'description', + 'comments', 'tags', 'custom_fields', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'manufacturer', 'model', 'description') + + +# +# Component templates +# + +class ConsolePortTemplateSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleporttemplate-detail') + device_type = DeviceTypeSerializer( + nested=True, + required=False, + allow_null=True, + default=None + ) + module_type = ModuleTypeSerializer( + nested=True, + required=False, + allow_null=True, + default=None + ) + type = ChoiceField( + choices=ConsolePortTypeChoices, + allow_blank=True, + required=False + ) + + class Meta: + model = ConsolePortTemplate + fields = [ + 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'description', 'created', + 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') + + +class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverporttemplate-detail') + device_type = DeviceTypeSerializer( + nested=True, + required=False, + allow_null=True, + default=None + ) + module_type = ModuleTypeSerializer( + nested=True, + required=False, + allow_null=True, + default=None + ) + type = ChoiceField( + choices=ConsolePortTypeChoices, + allow_blank=True, + required=False + ) + + class Meta: + model = ConsoleServerPortTemplate + fields = [ + 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'description', 'created', + 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') + + +class PowerPortTemplateSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerporttemplate-detail') + device_type = DeviceTypeSerializer( + nested=True, + required=False, + allow_null=True, + default=None + ) + module_type = ModuleTypeSerializer( + nested=True, + required=False, + allow_null=True, + default=None + ) + type = ChoiceField( + choices=PowerPortTypeChoices, + allow_blank=True, + required=False, + allow_null=True + ) + + class Meta: + model = PowerPortTemplate + fields = [ + 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw', + 'allocated_draw', 'description', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') + + +class PowerOutletTemplateSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlettemplate-detail') + device_type = DeviceTypeSerializer( + nested=True, + required=False, + allow_null=True, + default=None + ) + module_type = ModuleTypeSerializer( + nested=True, + required=False, + allow_null=True, + default=None + ) + type = ChoiceField( + choices=PowerOutletTypeChoices, + allow_blank=True, + required=False, + allow_null=True + ) + power_port = PowerPortTemplateSerializer( + nested=True, + required=False, + allow_null=True + ) + feed_leg = ChoiceField( + choices=PowerOutletFeedLegChoices, + allow_blank=True, + required=False, + allow_null=True + ) + + class Meta: + model = PowerOutletTemplate + fields = [ + 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'power_port', 'feed_leg', + 'description', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') + + +class InterfaceTemplateSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interfacetemplate-detail') + device_type = DeviceTypeSerializer( + nested=True, + required=False, + allow_null=True, + default=None + ) + module_type = ModuleTypeSerializer( + nested=True, + required=False, + allow_null=True, + default=None + ) + type = ChoiceField(choices=InterfaceTypeChoices) + bridge = NestedInterfaceTemplateSerializer( + required=False, + allow_null=True + ) + poe_mode = ChoiceField( + choices=InterfacePoEModeChoices, + required=False, + allow_blank=True, + allow_null=True + ) + poe_type = ChoiceField( + choices=InterfacePoETypeChoices, + required=False, + allow_blank=True, + allow_null=True + ) + rf_role = ChoiceField( + choices=WirelessRoleChoices, + required=False, + allow_blank=True, + allow_null=True + ) + + class Meta: + model = InterfaceTemplate + fields = [ + 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'enabled', 'mgmt_only', + 'description', 'bridge', 'poe_mode', 'poe_type', 'rf_role', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') + + +class RearPortTemplateSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearporttemplate-detail') + device_type = DeviceTypeSerializer( + required=False, + nested=True, + allow_null=True, + default=None + ) + module_type = ModuleTypeSerializer( + nested=True, + required=False, + allow_null=True, + default=None + ) + type = ChoiceField(choices=PortTypeChoices) + + class Meta: + model = RearPortTemplate + fields = [ + 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', + 'description', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') + + +class FrontPortTemplateSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontporttemplate-detail') + device_type = DeviceTypeSerializer( + nested=True, + required=False, + allow_null=True, + default=None + ) + module_type = ModuleTypeSerializer( + nested=True, + required=False, + allow_null=True, + default=None + ) + type = ChoiceField(choices=PortTypeChoices) + rear_port = RearPortTemplateSerializer(nested=True) + + class Meta: + model = FrontPortTemplate + fields = [ + 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port', + 'rear_port_position', 'description', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') + + +class ModuleBayTemplateSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebaytemplate-detail') + device_type = DeviceTypeSerializer( + nested=True + ) + + class Meta: + model = ModuleBayTemplate + fields = [ + 'id', 'url', 'display', 'device_type', 'name', 'label', 'position', 'description', 'created', + 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') + + +class DeviceBayTemplateSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebaytemplate-detail') + device_type = DeviceTypeSerializer( + nested=True + ) + + class Meta: + model = DeviceBayTemplate + fields = ['id', 'url', 'display', 'device_type', 'name', 'label', 'description', 'created', 'last_updated'] + brief_fields = ('id', 'url', 'display', 'name', 'description') + + +class InventoryItemTemplateSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemtemplate-detail') + device_type = DeviceTypeSerializer( + nested=True + ) + parent = serializers.PrimaryKeyRelatedField( + queryset=InventoryItemTemplate.objects.all(), + allow_null=True, + default=None + ) + role = InventoryItemRoleSerializer(nested=True, required=False, allow_null=True) + manufacturer = ManufacturerSerializer( + nested=True, + required=False, + allow_null=True, + default=None + ) + component_type = ContentTypeField( + queryset=ContentType.objects.filter(MODULAR_COMPONENT_TEMPLATE_MODELS), + required=False, + allow_null=True + ) + component = serializers.SerializerMethodField(read_only=True) + _depth = serializers.IntegerField(source='level', read_only=True) + + class Meta: + model = InventoryItemTemplate + fields = [ + 'id', 'url', 'display', 'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', + 'description', 'component_type', 'component_id', 'component', 'created', 'last_updated', '_depth', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description', '_depth') + + @extend_schema_field(serializers.JSONField(allow_null=True)) + def get_component(self, obj): + if obj.component is None: + return None + serializer = get_serializer_for_model(obj.component, prefix=NESTED_SERIALIZER_PREFIX) + context = {'request': self.context['request']} + return serializer(obj.component, context=context).data + + # # Virtual chassis # @@ -1245,14 +779,513 @@ class VirtualChassisSerializer(NetBoxModelSerializer): brief_fields = ('id', 'url', 'display', 'name', 'master', 'description', 'member_count') +# +# Devices +# + +class DeviceSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail') + device_type = DeviceTypeSerializer(nested=True) + role = DeviceRoleSerializer(nested=True) + device_role = DeviceRoleSerializer( + nested=True, + read_only=True, + help_text='Deprecated in v3.6 in favor of `role`.' + ) + tenant = TenantSerializer( + nested=True, + required=False, + allow_null=True, + default=None + ) + platform = PlatformSerializer(nested=True, required=False, allow_null=True) + site = SiteSerializer(nested=True) + location = LocationSerializer(nested=True, required=False, allow_null=True, default=None) + rack = RackSerializer(nested=True, required=False, allow_null=True, default=None) + face = ChoiceField(choices=DeviceFaceChoices, allow_blank=True, default=lambda: '') + position = serializers.DecimalField( + max_digits=4, + decimal_places=1, + allow_null=True, + label=_('Position (U)'), + min_value=decimal.Decimal(0.5), + default=None + ) + status = ChoiceField(choices=DeviceStatusChoices, required=False) + airflow = ChoiceField(choices=DeviceAirflowChoices, allow_blank=True, required=False) + primary_ip = NestedIPAddressSerializer(read_only=True) + primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True) + primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True) + oob_ip = NestedIPAddressSerializer(required=False, allow_null=True) + parent_device = serializers.SerializerMethodField() + cluster = NestedClusterSerializer(required=False, allow_null=True) + virtual_chassis = VirtualChassisSerializer(nested=True, required=False, allow_null=True, default=None) + vc_position = serializers.IntegerField(allow_null=True, max_value=255, min_value=0, default=None) + config_template = ConfigTemplateSerializer(nested=True, required=False, allow_null=True, default=None) + + # Counter fields + console_port_count = serializers.IntegerField(read_only=True) + console_server_port_count = serializers.IntegerField(read_only=True) + power_port_count = serializers.IntegerField(read_only=True) + power_outlet_count = serializers.IntegerField(read_only=True) + interface_count = serializers.IntegerField(read_only=True) + front_port_count = serializers.IntegerField(read_only=True) + rear_port_count = serializers.IntegerField(read_only=True) + device_bay_count = serializers.IntegerField(read_only=True) + module_bay_count = serializers.IntegerField(read_only=True) + inventory_item_count = serializers.IntegerField(read_only=True) + + class Meta: + model = Device + fields = [ + 'id', 'url', 'display', 'name', 'device_type', 'role', 'device_role', 'tenant', 'platform', 'serial', + 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device', + 'status', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', + 'vc_position', 'vc_priority', 'description', 'comments', 'config_template', 'local_context_data', 'tags', + 'custom_fields', 'created', 'last_updated', 'console_port_count', 'console_server_port_count', + 'power_port_count', 'power_outlet_count', 'interface_count', 'front_port_count', 'rear_port_count', + 'device_bay_count', 'module_bay_count', 'inventory_item_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') + + @extend_schema_field(NestedDeviceSerializer) + def get_parent_device(self, obj): + try: + device_bay = obj.parent_bay + except DeviceBay.DoesNotExist: + return None + context = {'request': self.context['request']} + data = NestedDeviceSerializer(instance=device_bay.device, context=context).data + data['device_bay'] = NestedDeviceBaySerializer(instance=device_bay, context=context).data + return data + + def get_device_role(self, obj): + return obj.role + + +class DeviceWithConfigContextSerializer(DeviceSerializer): + config_context = serializers.SerializerMethodField(read_only=True) + + class Meta(DeviceSerializer.Meta): + fields = [ + 'id', 'url', 'display', 'name', 'device_type', 'role', 'device_role', 'tenant', 'platform', 'serial', + 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device', + 'status', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', + 'vc_position', 'vc_priority', 'description', 'comments', 'config_template', 'config_context', + 'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated', 'console_port_count', + 'console_server_port_count', 'power_port_count', 'power_outlet_count', 'interface_count', + 'front_port_count', 'rear_port_count', 'device_bay_count', 'module_bay_count', 'inventory_item_count', + ] + + @extend_schema_field(serializers.JSONField(allow_null=True)) + def get_config_context(self, obj): + return obj.get_config_context() + + +class VirtualDeviceContextSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualdevicecontext-detail') + device = DeviceSerializer(nested=True) + tenant = TenantSerializer(nested=True, required=False, allow_null=True, default=None) + primary_ip = NestedIPAddressSerializer(read_only=True, allow_null=True) + primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True) + primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True) + status = ChoiceField(choices=VirtualDeviceContextStatusChoices) + + # Related object counts + interface_count = RelatedObjectCountField('interfaces') + + class Meta: + model = VirtualDeviceContext + fields = [ + 'id', 'url', 'display', 'name', 'device', 'identifier', 'tenant', 'primary_ip', 'primary_ip4', + 'primary_ip6', 'status', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', + 'interface_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'identifier', 'device', 'description') + + +class ModuleSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail') + device = DeviceSerializer(nested=True) + module_bay = NestedModuleBaySerializer() + module_type = ModuleTypeSerializer(nested=True) + status = ChoiceField(choices=ModuleStatusChoices, required=False) + + class Meta: + model = Module + fields = [ + 'id', 'url', 'display', 'device', 'module_bay', 'module_type', 'status', 'serial', 'asset_tag', + 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'device', 'module_bay', 'module_type', 'description') + + +# +# Device components +# + +class ConnectedEndpointsSerializer(serializers.ModelSerializer): + """ + Legacy serializer for pre-v3.3 connections + """ + connected_endpoints_type = serializers.SerializerMethodField(read_only=True) + connected_endpoints = serializers.SerializerMethodField(read_only=True) + connected_endpoints_reachable = serializers.SerializerMethodField(read_only=True) + + @extend_schema_field(OpenApiTypes.STR) + def get_connected_endpoints_type(self, obj): + if endpoints := obj.connected_endpoints: + return f'{endpoints[0]._meta.app_label}.{endpoints[0]._meta.model_name}' + + @extend_schema_field(serializers.ListField) + def get_connected_endpoints(self, obj): + """ + Return the appropriate serializer for the type of connected object. + """ + if endpoints := obj.connected_endpoints: + serializer = get_serializer_for_model(endpoints[0], prefix=NESTED_SERIALIZER_PREFIX) + context = {'request': self.context['request']} + return serializer(endpoints, many=True, context=context).data + + @extend_schema_field(serializers.BooleanField) + def get_connected_endpoints_reachable(self, obj): + return obj._path and obj._path.is_complete and obj._path.is_active + + +class ConsoleServerPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail') + device = DeviceSerializer(nested=True) + module = ModuleSerializer( + nested=True, + requested_fields=('id', 'url', 'display', 'device', 'module_bay'), + required=False, + allow_null=True + ) + type = ChoiceField( + choices=ConsolePortTypeChoices, + allow_blank=True, + required=False + ) + speed = ChoiceField( + choices=ConsolePortSpeedChoices, + allow_null=True, + required=False + ) + + class Meta: + model = ConsoleServerPort + fields = [ + 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'speed', 'description', + 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'connected_endpoints', + 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created', + 'last_updated', '_occupied', + ] + brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') + + +class ConsolePortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail') + device = DeviceSerializer(nested=True) + module = ModuleSerializer( + nested=True, + requested_fields=('id', 'url', 'display', 'device', 'module_bay'), + required=False, + allow_null=True + ) + type = ChoiceField( + choices=ConsolePortTypeChoices, + allow_blank=True, + required=False + ) + speed = ChoiceField( + choices=ConsolePortSpeedChoices, + allow_null=True, + required=False + ) + + class Meta: + model = ConsolePort + fields = [ + 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'speed', 'description', + 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'connected_endpoints', + 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created', + 'last_updated', '_occupied', + ] + brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') + + +class PowerPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail') + device = DeviceSerializer(nested=True) + module = ModuleSerializer( + nested=True, + requested_fields=('id', 'url', 'display', 'device', 'module_bay'), + required=False, + allow_null=True + ) + type = ChoiceField( + choices=PowerPortTypeChoices, + allow_blank=True, + required=False, + allow_null=True + ) + + class Meta: + model = PowerPort + fields = [ + 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', + 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', + 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', + 'created', 'last_updated', '_occupied', + ] + brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') + + +class PowerOutletSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail') + device = DeviceSerializer(nested=True) + module = ModuleSerializer( + nested=True, + requested_fields=('id', 'url', 'display', 'device', 'module_bay'), + required=False, + allow_null=True + ) + type = ChoiceField( + choices=PowerOutletTypeChoices, + allow_blank=True, + required=False, + allow_null=True + ) + power_port = PowerPortSerializer( + nested=True, + required=False, + allow_null=True + ) + feed_leg = ChoiceField( + choices=PowerOutletFeedLegChoices, + allow_blank=True, + required=False, + allow_null=True + ) + + class Meta: + model = PowerOutlet + fields = [ + 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'power_port', 'feed_leg', + 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', + 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', + 'created', 'last_updated', '_occupied', + ] + brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') + + +class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail') + device = DeviceSerializer(nested=True) + vdcs = SerializedPKRelatedField( + queryset=VirtualDeviceContext.objects.all(), + serializer=NestedVirtualDeviceContextSerializer, + required=False, + many=True + ) + module = ModuleSerializer( + nested=True, + requested_fields=('id', 'url', 'display', 'device', 'module_bay'), + required=False, + allow_null=True + ) + type = ChoiceField(choices=InterfaceTypeChoices) + parent = NestedInterfaceSerializer(required=False, allow_null=True) + bridge = NestedInterfaceSerializer(required=False, allow_null=True) + lag = NestedInterfaceSerializer(required=False, allow_null=True) + mode = ChoiceField(choices=InterfaceModeChoices, required=False, allow_blank=True) + duplex = ChoiceField(choices=InterfaceDuplexChoices, required=False, allow_blank=True, allow_null=True) + rf_role = ChoiceField(choices=WirelessRoleChoices, required=False, allow_blank=True) + rf_channel = ChoiceField(choices=WirelessChannelChoices, required=False, allow_blank=True) + poe_mode = ChoiceField(choices=InterfacePoEModeChoices, required=False, allow_blank=True) + poe_type = ChoiceField(choices=InterfacePoETypeChoices, required=False, allow_blank=True) + untagged_vlan = NestedVLANSerializer(required=False, allow_null=True) + tagged_vlans = SerializedPKRelatedField( + queryset=VLAN.objects.all(), + serializer=NestedVLANSerializer, + required=False, + many=True + ) + vrf = NestedVRFSerializer(required=False, allow_null=True) + l2vpn_termination = NestedL2VPNTerminationSerializer(read_only=True, allow_null=True) + wireless_link = NestedWirelessLinkSerializer(read_only=True, allow_null=True) + wireless_lans = SerializedPKRelatedField( + queryset=WirelessLAN.objects.all(), + serializer=NestedWirelessLANSerializer, + required=False, + many=True + ) + count_ipaddresses = serializers.IntegerField(read_only=True) + count_fhrp_groups = serializers.IntegerField(read_only=True) + mac_address = serializers.CharField( + required=False, + default=None, + allow_blank=True, + allow_null=True + ) + wwn = serializers.CharField(required=False, default=None, allow_blank=True, allow_null=True) + + class Meta: + model = Interface + fields = [ + 'id', 'url', 'display', 'device', 'vdcs', 'module', 'name', 'label', 'type', 'enabled', 'parent', 'bridge', + 'lag', 'mtu', 'mac_address', 'speed', 'duplex', 'wwn', 'mgmt_only', 'description', 'mode', 'rf_role', + 'rf_channel', 'poe_mode', 'poe_type', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', + 'untagged_vlan', 'tagged_vlans', 'mark_connected', 'cable', 'cable_end', 'wireless_link', 'link_peers', + 'link_peers_type', 'wireless_lans', 'vrf', 'l2vpn_termination', 'connected_endpoints', + 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created', + 'last_updated', 'count_ipaddresses', 'count_fhrp_groups', '_occupied', + ] + brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') + + def validate(self, data): + + # Validate many-to-many VLAN assignments + if not self.nested: + device = self.instance.device if self.instance else data.get('device') + for vlan in data.get('tagged_vlans', []): + if vlan.site not in [device.site, None]: + raise serializers.ValidationError({ + 'tagged_vlans': f"VLAN {vlan} must belong to the same site as the interface's parent device, " + f"or it must be global." + }) + + return super().validate(data) + + +class RearPortSerializer(NetBoxModelSerializer, CabledObjectSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail') + device = DeviceSerializer(nested=True) + module = ModuleSerializer( + nested=True, + requested_fields=('id', 'url', 'display', 'device', 'module_bay'), + required=False, + allow_null=True + ) + type = ChoiceField(choices=PortTypeChoices) + + class Meta: + model = RearPort + fields = [ + 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'description', + 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'tags', 'custom_fields', 'created', + 'last_updated', '_occupied', + ] + brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') + + +class FrontPortRearPortSerializer(WritableNestedSerializer): + """ + NestedRearPortSerializer but with parent device omitted (since front and rear ports must belong to same device) + """ + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail') + + class Meta: + model = RearPort + fields = ['id', 'url', 'display', 'name', 'label', 'description'] + + +class FrontPortSerializer(NetBoxModelSerializer, CabledObjectSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail') + device = DeviceSerializer(nested=True) + module = ModuleSerializer( + nested=True, + requested_fields=('id', 'url', 'display', 'device', 'module_bay'), + required=False, + allow_null=True + ) + type = ChoiceField(choices=PortTypeChoices) + rear_port = FrontPortRearPortSerializer() + + class Meta: + model = FrontPort + fields = [ + 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', + 'rear_port_position', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', + 'link_peers_type', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied', + ] + brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') + + +class ModuleBaySerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail') + device = DeviceSerializer(nested=True) + installed_module = ModuleSerializer( + nested=True, + requested_fields=('id', 'url', 'display', 'serial', 'description'), + required=False, + allow_null=True + ) + + class Meta: + model = ModuleBay + fields = [ + 'id', 'url', 'display', 'device', 'name', 'installed_module', 'label', 'position', 'description', 'tags', + 'custom_fields', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'installed_module', 'name', 'description') + + +class DeviceBaySerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail') + device = DeviceSerializer(nested=True) + installed_device = DeviceSerializer(nested=True, required=False, allow_null=True) + + class Meta: + model = DeviceBay + fields = [ + 'id', 'url', 'display', 'device', 'name', 'label', 'description', 'installed_device', 'tags', + 'custom_fields', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'device', 'name', 'description') + + +class InventoryItemSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitem-detail') + device = DeviceSerializer(nested=True) + parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None) + role = InventoryItemRoleSerializer(nested=True, required=False, allow_null=True) + manufacturer = ManufacturerSerializer(nested=True, required=False, allow_null=True, default=None) + component_type = ContentTypeField( + queryset=ContentType.objects.filter(MODULAR_COMPONENT_MODELS), + required=False, + allow_null=True + ) + component = serializers.SerializerMethodField(read_only=True) + _depth = serializers.IntegerField(source='level', read_only=True) + + class Meta: + model = InventoryItem + fields = [ + 'id', 'url', 'display', 'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', + 'asset_tag', 'discovered', 'description', 'component_type', 'component_id', 'component', 'tags', + 'custom_fields', 'created', 'last_updated', '_depth', + ] + brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', '_depth') + + @extend_schema_field(serializers.JSONField(allow_null=True)) + def get_component(self, obj): + if obj.component is None: + return None + serializer = get_serializer_for_model(obj.component, prefix=NESTED_SERIALIZER_PREFIX) + context = {'request': self.context['request']} + return serializer(obj.component, context=context).data + + # # Power panels # class PowerPanelSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail') - site = NestedSiteSerializer() - location = NestedLocationSerializer( + site = SiteSerializer(nested=True) + location = LocationSerializer( + nested=True, required=False, allow_null=True, default=None @@ -1272,8 +1305,9 @@ class PowerPanelSerializer(NetBoxModelSerializer): class PowerFeedSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerfeed-detail') - power_panel = NestedPowerPanelSerializer() - rack = NestedRackSerializer( + power_panel = PowerPanelSerializer(nested=True) + rack = RackSerializer( + nested=True, required=False, allow_null=True, default=None @@ -1294,7 +1328,8 @@ class PowerFeedSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect choices=PowerFeedPhaseChoices, default=lambda: PowerFeedPhaseChoices.PHASE_SINGLE, ) - tenant = NestedTenantSerializer( + tenant = TenantSerializer( + nested=True, required=False, allow_null=True ) @@ -1308,3 +1343,23 @@ class PowerFeedSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect 'tenant', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied', ] brief_fields = ('id', 'url', 'display', 'name', 'description', 'cable', '_occupied') + + +class RackUnitSerializer(serializers.Serializer): + """ + A rack unit is an abstraction formed by the set (rack, position, face); it does not exist as a row in the database. + """ + id = serializers.DecimalField( + max_digits=4, + decimal_places=1, + read_only=True + ) + name = serializers.CharField(read_only=True) + face = ChoiceField(choices=DeviceFaceChoices, read_only=True) + device = DeviceSerializer(nested=True, read_only=True) + occupied = serializers.BooleanField(read_only=True) + display = serializers.SerializerMethodField(read_only=True) + + @extend_schema_field(OpenApiTypes.STR) + def get_display(self, obj): + return obj['name'] diff --git a/netbox/extras/api/mixins.py b/netbox/extras/api/mixins.py index 1737ff9f8..aafdf32d4 100644 --- a/netbox/extras/api/mixins.py +++ b/netbox/extras/api/mixins.py @@ -5,7 +5,7 @@ from rest_framework.response import Response from rest_framework.status import HTTP_400_BAD_REQUEST from netbox.api.renderers import TextRenderer -from .nested_serializers import NestedConfigTemplateSerializer +from .serializers import ConfigTemplateSerializer __all__ = ( 'ConfigContextQuerySetMixin', @@ -52,7 +52,7 @@ class ConfigTemplateRenderMixin: if request.accepted_renderer.format == 'txt': return Response(output) - template_serializer = NestedConfigTemplateSerializer(configtemplate, context={'request': request}) + template_serializer = ConfigTemplateSerializer(configtemplate, nested=True, context={'request': request}) return Response({ 'configtemplate': template_serializer.data, diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index 7dad95263..4c8c15159 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -5,8 +5,7 @@ from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import extend_schema_field from rest_framework import serializers -from core.api.nested_serializers import NestedDataSourceSerializer, NestedDataFileSerializer, NestedJobSerializer -from core.api.serializers import JobSerializer +from core.api.serializers import DataFileSerializer, DataSourceSerializer, JobSerializer from core.models import ContentType from dcim.api.nested_serializers import ( NestedDeviceRoleSerializer, NestedDeviceTypeSerializer, NestedLocationSerializer, NestedPlatformSerializer, @@ -22,7 +21,7 @@ from netbox.api.serializers.features import TaggableModelSerializer from netbox.constants import NESTED_SERIALIZER_PREFIX from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer from tenancy.models import Tenant, TenantGroup -from users.api.nested_serializers import NestedUserSerializer +from users.api.serializers import UserSerializer from utilities.api import get_serializer_for_model from virtualization.api.nested_serializers import ( NestedClusterGroupSerializer, NestedClusterSerializer, NestedClusterTypeSerializer, @@ -115,6 +114,28 @@ class WebhookSerializer(NetBoxModelSerializer): # Custom fields # +class CustomFieldChoiceSetSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfieldchoiceset-detail') + base_choices = ChoiceField( + choices=CustomFieldChoiceSetBaseChoices, + required=False + ) + extra_choices = serializers.ListField( + child=serializers.ListField( + min_length=2, + max_length=2 + ) + ) + + class Meta: + model = CustomFieldChoiceSet + fields = [ + 'id', 'url', 'display', 'name', 'description', 'base_choices', 'extra_choices', 'order_alphabetically', + 'choices_count', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description', 'choices_count') + + class CustomFieldSerializer(ValidatedModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfield-detail') content_types = ContentTypeField( @@ -129,7 +150,8 @@ class CustomFieldSerializer(ValidatedModelSerializer): ) filter_logic = ChoiceField(choices=CustomFieldFilterLogicChoices, required=False) data_type = serializers.SerializerMethodField() - choice_set = NestedCustomFieldChoiceSetSerializer( + choice_set = CustomFieldChoiceSetSerializer( + nested=True, required=False, allow_null=True ) @@ -168,28 +190,6 @@ class CustomFieldSerializer(ValidatedModelSerializer): return 'string' -class CustomFieldChoiceSetSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfieldchoiceset-detail') - base_choices = ChoiceField( - choices=CustomFieldChoiceSetBaseChoices, - required=False - ) - extra_choices = serializers.ListField( - child=serializers.ListField( - min_length=2, - max_length=2 - ) - ) - - class Meta: - model = CustomFieldChoiceSet - fields = [ - 'id', 'url', 'display', 'name', 'description', 'base_choices', 'extra_choices', 'order_alphabetically', - 'choices_count', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description', 'choices_count') - - # # Custom links # @@ -220,10 +220,12 @@ class ExportTemplateSerializer(ValidatedModelSerializer): queryset=ContentType.objects.with_feature('export_templates'), many=True ) - data_source = NestedDataSourceSerializer( + data_source = DataSourceSerializer( + nested=True, required=False ) - data_file = NestedDataFileSerializer( + data_file = DataFileSerializer( + nested=True, read_only=True ) @@ -267,7 +269,7 @@ class BookmarkSerializer(ValidatedModelSerializer): queryset=ContentType.objects.with_feature('bookmarks'), ) object = serializers.SerializerMethodField(read_only=True) - user = NestedUserSerializer() + user = UserSerializer(nested=True) class Meta: model = Bookmark @@ -482,10 +484,12 @@ class ConfigContextSerializer(ValidatedModelSerializer): required=False, many=True ) - data_source = NestedDataSourceSerializer( + data_source = DataSourceSerializer( + nested=True, required=False ) - data_file = NestedDataFileSerializer( + data_file = DataFileSerializer( + nested=True, read_only=True ) @@ -506,10 +510,12 @@ class ConfigContextSerializer(ValidatedModelSerializer): class ConfigTemplateSerializer(TaggableModelSerializer, ValidatedModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='extras-api:configtemplate-detail') - data_source = NestedDataSourceSerializer( + data_source = DataSourceSerializer( + nested=True, required=False ) - data_file = NestedDataFileSerializer( + data_file = DataFileSerializer( + nested=True, required=False ) @@ -530,7 +536,7 @@ class ScriptSerializer(ValidatedModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='extras-api:script-detail') description = serializers.SerializerMethodField(read_only=True) vars = serializers.SerializerMethodField(read_only=True) - result = NestedJobSerializer(read_only=True) + result = JobSerializer(nested=True, read_only=True) class Meta: model = Script @@ -596,7 +602,8 @@ class ScriptInputSerializer(serializers.Serializer): class ObjectChangeSerializer(BaseModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='extras-api:objectchange-detail') - user = NestedUserSerializer( + user = UserSerializer( + nested=True, read_only=True ) action = ChoiceField( diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index a9c0ce7cb..c4b1b1799 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -2,29 +2,48 @@ from django.contrib.contenttypes.models import ContentType from drf_spectacular.utils import extend_schema_field from rest_framework import serializers -from dcim.api.nested_serializers import NestedDeviceSerializer, NestedSiteSerializer +from dcim.api.serializers import DeviceSerializer, SiteSerializer 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, RelatedObjectCountField, SerializedPKRelatedField from netbox.api.serializers import NetBoxModelSerializer from netbox.constants import NESTED_SERIALIZER_PREFIX -from tenancy.api.nested_serializers import NestedTenantSerializer +from tenancy.api.serializers import TenantSerializer from utilities.api import get_serializer_for_model -from virtualization.api.nested_serializers import NestedVirtualMachineSerializer +from virtualization.api.serializers import VirtualMachineSerializer from vpn.api.nested_serializers import NestedL2VPNTerminationSerializer from .field_serializers import IPAddressField, IPNetworkField from .nested_serializers import * +# +# RIRs +# + +class RIRSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail') + + # Related object counts + aggregate_count = RelatedObjectCountField('aggregates') + + class Meta: + model = RIR + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'is_private', 'description', 'tags', 'custom_fields', 'created', + 'last_updated', 'aggregate_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'aggregate_count') + + # # ASN ranges # class ASNRangeSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asnrange-detail') - rir = NestedRIRSerializer() - tenant = NestedTenantSerializer(required=False, allow_null=True) + rir = RIRSerializer(nested=True) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) asn_count = serializers.IntegerField(read_only=True) class Meta: @@ -42,8 +61,8 @@ class ASNRangeSerializer(NetBoxModelSerializer): 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) + rir = RIRSerializer(nested=True, required=False, allow_null=True) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) # Related object counts site_count = RelatedObjectCountField('sites') @@ -66,7 +85,7 @@ class AvailableASNSerializer(serializers.Serializer): description = serializers.CharField(required=False) def to_representation(self, asn): - rir = NestedRIRSerializer(self.context['range'].rir, context={ + rir = RIRSerializer(self.context['range'].rir, nested=True, context={ 'request': self.context['request'] }).data return { @@ -81,7 +100,7 @@ class AvailableASNSerializer(serializers.Serializer): class VRFSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vrf-detail') - tenant = NestedTenantSerializer(required=False, allow_null=True) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) import_targets = SerializedPKRelatedField( queryset=RouteTarget.objects.all(), serializer=NestedRouteTargetSerializer, @@ -109,13 +128,9 @@ class VRFSerializer(NetBoxModelSerializer): brief_fields = ('id', 'url', 'display', 'name', 'rd', 'description', 'prefix_count') -# -# Route targets -# - class RouteTargetSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:routetarget-detail') - tenant = NestedTenantSerializer(required=False, allow_null=True) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) class Meta: model = RouteTarget @@ -127,29 +142,14 @@ class RouteTargetSerializer(NetBoxModelSerializer): # -# RIRs/aggregates +# Aggregates # -class RIRSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail') - - # Related object counts - aggregate_count = RelatedObjectCountField('aggregates') - - class Meta: - model = RIR - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'is_private', 'description', 'tags', 'custom_fields', 'created', - 'last_updated', 'aggregate_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'aggregate_count') - - class AggregateSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:aggregate-detail') family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True) - rir = NestedRIRSerializer() - tenant = NestedTenantSerializer(required=False, allow_null=True) + rir = RIRSerializer(nested=True) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) prefix = IPNetworkField() class Meta: @@ -180,7 +180,7 @@ class FHRPGroupSerializer(NetBoxModelSerializer): class FHRPGroupAssignmentSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:fhrpgroupassignment-detail') - group = NestedFHRPGroupSerializer() + group = FHRPGroupSerializer(nested=True) interface_type = ContentTypeField( queryset=ContentType.objects.all() ) @@ -261,11 +261,11 @@ class VLANGroupSerializer(NetBoxModelSerializer): class VLANSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail') - site = NestedSiteSerializer(required=False, allow_null=True) - group = NestedVLANGroupSerializer(required=False, allow_null=True, default=None) - tenant = NestedTenantSerializer(required=False, allow_null=True) + site = SiteSerializer(nested=True, required=False, allow_null=True) + group = VLANGroupSerializer(nested=True, required=False, allow_null=True, default=None) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) status = ChoiceField(choices=VLANStatusChoices, required=False) - role = NestedRoleSerializer(required=False, allow_null=True) + role = RoleSerializer(nested=True, required=False, allow_null=True) l2vpn_termination = NestedL2VPNTerminationSerializer(read_only=True, allow_null=True) # Related object counts @@ -285,23 +285,24 @@ class AvailableVLANSerializer(serializers.Serializer): Representation of a VLAN which does not exist in the database. """ vid = serializers.IntegerField(read_only=True) - group = NestedVLANGroupSerializer(read_only=True) + group = VLANGroupSerializer(nested=True, read_only=True) def to_representation(self, instance): return { 'vid': instance, - 'group': NestedVLANGroupSerializer( + 'group': VLANGroupSerializer( self.context['group'], + nested=True, context={'request': self.context['request']} ).data, } class CreateAvailableVLANSerializer(NetBoxModelSerializer): - site = NestedSiteSerializer(required=False, allow_null=True) - tenant = NestedTenantSerializer(required=False, allow_null=True) + site = SiteSerializer(nested=True, required=False, allow_null=True) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) status = ChoiceField(choices=VLANStatusChoices, required=False) - role = NestedRoleSerializer(required=False, allow_null=True) + role = RoleSerializer(nested=True, required=False, allow_null=True) class Meta: model = VLAN @@ -321,12 +322,12 @@ class CreateAvailableVLANSerializer(NetBoxModelSerializer): class PrefixSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:prefix-detail') family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True) - site = NestedSiteSerializer(required=False, allow_null=True) - vrf = NestedVRFSerializer(required=False, allow_null=True) - tenant = NestedTenantSerializer(required=False, allow_null=True) - vlan = NestedVLANSerializer(required=False, allow_null=True) + site = SiteSerializer(nested=True, required=False, allow_null=True) + vrf = VRFSerializer(nested=True, required=False, allow_null=True) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) + vlan = VLANSerializer(nested=True, required=False, allow_null=True) status = ChoiceField(choices=PrefixStatusChoices, required=False) - role = NestedRoleSerializer(required=False, allow_null=True) + role = RoleSerializer(nested=True, required=False, allow_null=True) children = serializers.IntegerField(read_only=True) _depth = serializers.IntegerField(read_only=True) prefix = IPNetworkField() @@ -374,11 +375,11 @@ class AvailablePrefixSerializer(serializers.Serializer): """ family = serializers.IntegerField(read_only=True) prefix = serializers.CharField(read_only=True) - vrf = NestedVRFSerializer(read_only=True) + vrf = VRFSerializer(nested=True, read_only=True) def to_representation(self, instance): if self.context.get('vrf'): - vrf = NestedVRFSerializer(self.context['vrf'], context={'request': self.context['request']}).data + vrf = VRFSerializer(self.context['vrf'], nested=True, context={'request': self.context['request']}).data else: vrf = None return { @@ -397,10 +398,10 @@ class IPRangeSerializer(NetBoxModelSerializer): family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True) start_address = IPAddressField() end_address = IPAddressField() - vrf = NestedVRFSerializer(required=False, allow_null=True) - tenant = NestedTenantSerializer(required=False, allow_null=True) + vrf = VRFSerializer(nested=True, required=False, allow_null=True) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) status = ChoiceField(choices=IPRangeStatusChoices, required=False) - role = NestedRoleSerializer(required=False, allow_null=True) + role = RoleSerializer(nested=True, required=False, allow_null=True) class Meta: model = IPRange @@ -420,8 +421,8 @@ class IPAddressSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail') family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True) address = IPAddressField() - vrf = NestedVRFSerializer(required=False, allow_null=True) - tenant = NestedTenantSerializer(required=False, allow_null=True) + vrf = VRFSerializer(nested=True, required=False, allow_null=True) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) status = ChoiceField(choices=IPAddressStatusChoices, required=False) role = ChoiceField(choices=IPAddressRoleChoices, allow_blank=True, required=False) assigned_object_type = ContentTypeField( @@ -457,12 +458,12 @@ class AvailableIPSerializer(serializers.Serializer): """ family = serializers.IntegerField(read_only=True) address = serializers.CharField(read_only=True) - vrf = NestedVRFSerializer(read_only=True) + vrf = VRFSerializer(nested=True, read_only=True) description = serializers.CharField(required=False) def to_representation(self, instance): if self.context.get('vrf'): - vrf = NestedVRFSerializer(self.context['vrf'], context={'request': self.context['request']}).data + vrf = VRFSerializer(self.context['vrf'], nested=True, context={'request': self.context['request']}).data else: vrf = None return { @@ -491,8 +492,8 @@ class ServiceTemplateSerializer(NetBoxModelSerializer): class ServiceSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:service-detail') - device = NestedDeviceSerializer(required=False, allow_null=True) - virtual_machine = NestedVirtualMachineSerializer(required=False, allow_null=True) + device = DeviceSerializer(nested=True, required=False, allow_null=True) + virtual_machine = VirtualMachineSerializer(nested=True, required=False, allow_null=True) protocol = ChoiceField(choices=ServiceProtocolChoices, required=False) ipaddresses = SerializedPKRelatedField( queryset=IPAddress.objects.all(), diff --git a/netbox/netbox/api/serializers/base.py b/netbox/netbox/api/serializers/base.py index c715b2d26..7fe68d4ef 100644 --- a/netbox/netbox/api/serializers/base.py +++ b/netbox/netbox/api/serializers/base.py @@ -1,8 +1,9 @@ -from django.db.models import ManyToManyField from rest_framework import serializers from drf_spectacular.utils import extend_schema_field from drf_spectacular.types import OpenApiTypes +from utilities.api import get_related_object_by_attrs + __all__ = ( 'BaseModelSerializer', 'ValidatedModelSerializer', @@ -12,15 +13,30 @@ __all__ = ( class BaseModelSerializer(serializers.ModelSerializer): display = serializers.SerializerMethodField(read_only=True) - def __init__(self, *args, requested_fields=None, **kwargs): + def __init__(self, *args, nested=False, requested_fields=None, **kwargs): super().__init__(*args, **kwargs) + self.nested = nested + + if nested and not requested_fields: + requested_fields = getattr(self.Meta, 'brief_fields', None) + # 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) + def to_internal_value(self, data): + + # If initialized as a nested serializer, we should expect to receive the attrs or PK + # identifying a related object. + if self.nested: + queryset = self.Meta.model.objects.all() + return get_related_object_by_attrs(queryset, data) + + return super().to_internal_value(data) + @extend_schema_field(OpenApiTypes.STR) def get_display(self, obj): return str(obj) @@ -32,6 +48,11 @@ class ValidatedModelSerializer(BaseModelSerializer): validation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144) """ def validate(self, data): + + # Skip validation if we're being used to represent a nested object + if self.nested: + return data + attrs = data.copy() # Remove custom field data (if any) prior to model validation diff --git a/netbox/netbox/api/serializers/nested.py b/netbox/netbox/api/serializers/nested.py index 027f3d11e..e43fd7428 100644 --- a/netbox/netbox/api/serializers/nested.py +++ b/netbox/netbox/api/serializers/nested.py @@ -1,10 +1,7 @@ -from django.core.exceptions import FieldError, MultipleObjectsReturned, ObjectDoesNotExist -from django.utils.translation import gettext_lazy as _ from rest_framework import serializers -from rest_framework.exceptions import ValidationError from extras.models import Tag -from utilities.utils import dict_to_filter_params +from utilities.api import get_related_object_by_attrs from .base import BaseModelSerializer __all__ = ( @@ -20,43 +17,8 @@ class WritableNestedSerializer(BaseModelSerializer): subclassed to return a full representation of the related object on read. """ def to_internal_value(self, data): - - if data is None: - return None - - # Dictionary of related object attributes - if isinstance(data, dict): - params = dict_to_filter_params(data) - queryset = self.Meta.model.objects - try: - return queryset.get(**params) - except ObjectDoesNotExist: - raise ValidationError( - _("Related object not found using the provided attributes: {params}").format(params=params)) - except MultipleObjectsReturned: - raise ValidationError( - _("Multiple objects match the provided attributes: {params}").format(params=params) - ) - except FieldError as e: - raise ValidationError(e) - - # Integer PK of related object - try: - # Cast as integer in case a PK was mistakenly sent as a string - pk = int(data) - except (TypeError, ValueError): - raise ValidationError( - _( - "Related objects must be referenced by numeric ID or by dictionary of attributes. Received an " - "unrecognized value: {value}" - ).format(value=data) - ) - - # Look up object by PK - try: - return self.Meta.model.objects.get(pk=pk) - except ObjectDoesNotExist: - raise ValidationError(_("Related object not found using the provided numeric ID: {id}").format(id=pk)) + queryset = self.Meta.model.objects.all() + return get_related_object_by_attrs(queryset, data) # Declared here for use by PrimaryModelSerializer, but should be imported from extras.api.nested_serializers diff --git a/netbox/tenancy/api/serializers.py b/netbox/tenancy/api/serializers.py index 325d3b439..103eb492b 100644 --- a/netbox/tenancy/api/serializers.py +++ b/netbox/tenancy/api/serializers.py @@ -32,7 +32,7 @@ class TenantGroupSerializer(NestedGroupModelSerializer): class TenantSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenant-detail') - group = NestedTenantGroupSerializer(required=False, allow_null=True) + group = TenantGroupSerializer(nested=True, required=False, allow_null=True) # Related object counts circuit_count = RelatedObjectCountField('circuits') @@ -87,7 +87,7 @@ class ContactRoleSerializer(NetBoxModelSerializer): class ContactSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contact-detail') - group = NestedContactGroupSerializer(required=False, allow_null=True, default=None) + group = ContactGroupSerializer(nested=True, required=False, allow_null=True, default=None) class Meta: model = Contact @@ -104,8 +104,8 @@ class ContactAssignmentSerializer(NetBoxModelSerializer): queryset=ContentType.objects.all() ) object = serializers.SerializerMethodField(read_only=True) - contact = NestedContactSerializer() - role = NestedContactRoleSerializer(required=False, allow_null=True) + contact = ContactSerializer(nested=True) + role = ContactRoleSerializer(nested=True, required=False, allow_null=True) priority = ChoiceField(choices=ContactPriorityChoices, allow_blank=True, required=False, default=lambda: '') class Meta: diff --git a/netbox/users/api/serializers.py b/netbox/users/api/serializers.py index 0eef61dc8..49150f9c9 100644 --- a/netbox/users/api/serializers.py +++ b/netbox/users/api/serializers.py @@ -89,7 +89,7 @@ class TokenSerializer(ValidatedModelSerializer): required=False, write_only=not settings.ALLOW_TOKEN_RETRIEVAL ) - user = NestedUserSerializer() + user = UserSerializer(nested=True) allowed_ips = serializers.ListField( child=IPNetworkSerializer(), required=False, @@ -122,7 +122,8 @@ class TokenSerializer(ValidatedModelSerializer): class TokenProvisionSerializer(TokenSerializer): - user = NestedUserSerializer( + user = UserSerializer( + nested=True, read_only=True ) username = serializers.CharField( diff --git a/netbox/utilities/api.py b/netbox/utilities/api.py index a13e62bfd..1248ed296 100644 --- a/netbox/utilities/api.py +++ b/netbox/utilities/api.py @@ -3,23 +3,26 @@ import sys from django.conf import settings from django.contrib.contenttypes.fields import GenericForeignKey -from django.core.exceptions import FieldDoesNotExist +from django.core.exceptions import ( + FieldDoesNotExist, FieldError, MultipleObjectsReturned, ObjectDoesNotExist, ValidationError, +) from django.db.models.fields.related import ManyToOneRel, RelatedField from django.http import JsonResponse from django.urls import reverse +from django.utils.translation import gettext_lazy as _ 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 +from .utils import count_related, dict_to_filter_params, dynamic_import __all__ = ( 'get_annotations_for_serializer', 'get_graphql_type_for_model', 'get_prefetches_for_serializer', + 'get_related_object_by_attrs', 'get_serializer_for_model', 'get_view_name', 'is_api_request', @@ -103,7 +106,7 @@ def get_prefetches_for_serializer(serializer_class, fields_to_include=None): """ model = serializer_class.Meta.model - # If specific fields are not specified, default to all + # If fields are not specified, default to all if not fields_to_include: fields_to_include = serializer_class.Meta.fields @@ -128,7 +131,9 @@ def get_prefetches_for_serializer(serializer_class, fields_to_include=None): # for the related object. if serializer_field: if issubclass(type(serializer_field), Serializer): - for subfield in get_prefetches_for_serializer(type(serializer_field)): + # Determine which fields to prefetch for the nested object + subfields = serializer_field.Meta.brief_fields if serializer_field.nested else None + for subfield in get_prefetches_for_serializer(type(serializer_field), subfields): prefetch_fields.append(f'{field_name}__{subfield}') return prefetch_fields @@ -154,6 +159,48 @@ def get_annotations_for_serializer(serializer_class, fields_to_include=None): return annotations +def get_related_object_by_attrs(queryset, attrs): + """ + Return an object identified by either a dictionary of attributes or its numeric primary key (ID). This is used + for referencing related objects when creating/updating objects via the REST API. + """ + if attrs is None: + return None + + # Dictionary of related object attributes + if isinstance(attrs, dict): + params = dict_to_filter_params(attrs) + try: + return queryset.get(**params) + except ObjectDoesNotExist: + raise ValidationError( + _("Related object not found using the provided attributes: {params}").format(params=params)) + except MultipleObjectsReturned: + raise ValidationError( + _("Multiple objects match the provided attributes: {params}").format(params=params) + ) + except FieldError as e: + raise ValidationError(e) + + # Integer PK of related object + try: + # Cast as integer in case a PK was mistakenly sent as a string + pk = int(attrs) + except (TypeError, ValueError): + raise ValidationError( + _( + "Related objects must be referenced by numeric ID or by dictionary of attributes. Received an " + "unrecognized value: {value}" + ).format(value=attrs) + ) + + # Look up object by PK + try: + return queryset.get(pk=pk) + except ObjectDoesNotExist: + raise ValidationError(_("Related object not found using the provided numeric ID: {id}").format(id=pk)) + + def rest_api_server_error(request, *args, **kwargs): """ Handle exceptions and return a useful error message for REST API requests. diff --git a/netbox/virtualization/api/serializers.py b/netbox/virtualization/api/serializers.py index 4126c3f36..9f07f6f90 100644 --- a/netbox/virtualization/api/serializers.py +++ b/netbox/virtualization/api/serializers.py @@ -1,16 +1,14 @@ from drf_spectacular.utils import extend_schema_field from rest_framework import serializers -from dcim.api.nested_serializers import ( - NestedDeviceSerializer, NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedSiteSerializer, -) +from dcim.api.serializers import DeviceSerializer, DeviceRoleSerializer, PlatformSerializer, SiteSerializer from dcim.choices import InterfaceModeChoices -from extras.api.nested_serializers import NestedConfigTemplateSerializer +from extras.api.serializers import ConfigTemplateSerializer from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer, NestedVRFSerializer from ipam.models import VLAN from netbox.api.fields import ChoiceField, RelatedObjectCountField, SerializedPKRelatedField from netbox.api.serializers import NetBoxModelSerializer -from tenancy.api.nested_serializers import NestedTenantSerializer +from tenancy.api.serializers import TenantSerializer from virtualization.choices import * from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualDisk, VirtualMachine, VMInterface from vpn.api.nested_serializers import NestedL2VPNTerminationSerializer @@ -53,11 +51,11 @@ class ClusterGroupSerializer(NetBoxModelSerializer): class ClusterSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail') - type = NestedClusterTypeSerializer() - group = NestedClusterGroupSerializer(required=False, allow_null=True, default=None) + type = ClusterTypeSerializer(nested=True) + group = ClusterGroupSerializer(nested=True, required=False, allow_null=True, default=None) status = ChoiceField(choices=ClusterStatusChoices, required=False) - tenant = NestedTenantSerializer(required=False, allow_null=True) - site = NestedSiteSerializer(required=False, allow_null=True, default=None) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) + site = SiteSerializer(nested=True, required=False, allow_null=True, default=None) # Related object counts device_count = RelatedObjectCountField('devices') @@ -79,16 +77,16 @@ class ClusterSerializer(NetBoxModelSerializer): class VirtualMachineSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:virtualmachine-detail') status = ChoiceField(choices=VirtualMachineStatusChoices, required=False) - site = NestedSiteSerializer(required=False, allow_null=True) - cluster = NestedClusterSerializer(required=False, allow_null=True) - device = NestedDeviceSerializer(required=False, allow_null=True) - role = NestedDeviceRoleSerializer(required=False, allow_null=True) - tenant = NestedTenantSerializer(required=False, allow_null=True) - platform = NestedPlatformSerializer(required=False, allow_null=True) + site = SiteSerializer(nested=True, required=False, allow_null=True) + cluster = ClusterSerializer(nested=True, required=False, allow_null=True) + device = DeviceSerializer(nested=True, required=False, allow_null=True) + role = DeviceRoleSerializer(nested=True, required=False, allow_null=True) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) + platform = PlatformSerializer(nested=True, required=False, allow_null=True) primary_ip = NestedIPAddressSerializer(read_only=True) primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True) primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True) - config_template = NestedConfigTemplateSerializer(required=False, allow_null=True, default=None) + config_template = ConfigTemplateSerializer(nested=True, required=False, allow_null=True, default=None) # Counter fields interface_count = serializers.IntegerField(read_only=True) @@ -128,7 +126,7 @@ class VirtualMachineWithConfigContextSerializer(VirtualMachineSerializer): class VMInterfaceSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:vminterface-detail') - virtual_machine = NestedVirtualMachineSerializer() + virtual_machine = VirtualMachineSerializer(nested=True) parent = NestedVMInterfaceSerializer(required=False, allow_null=True) bridge = NestedVMInterfaceSerializer(required=False, allow_null=True) mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False) @@ -178,7 +176,7 @@ class VMInterfaceSerializer(NetBoxModelSerializer): class VirtualDiskSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:virtualdisk-detail') - virtual_machine = NestedVirtualMachineSerializer() + virtual_machine = VirtualMachineSerializer(nested=True) class Meta: model = VirtualDisk diff --git a/netbox/vpn/api/serializers.py b/netbox/vpn/api/serializers.py index 53c1f898c..7ba374fc3 100644 --- a/netbox/vpn/api/serializers.py +++ b/netbox/vpn/api/serializers.py @@ -2,12 +2,13 @@ from django.contrib.contenttypes.models import ContentType from drf_spectacular.utils import extend_schema_field from rest_framework import serializers -from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedRouteTargetSerializer +from ipam.api.serializers import IPAddressSerializer +from ipam.api.nested_serializers import NestedRouteTargetSerializer from ipam.models import RouteTarget 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 +from tenancy.api.serializers import TenantSerializer from utilities.api import get_serializer_for_model from vpn.choices import * from vpn.models import * @@ -27,90 +28,6 @@ __all__ = ( ) -class TunnelGroupSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='vpn-api:tunnelgroup-detail') - - # Related object counts - tunnel_count = RelatedObjectCountField('tunnels') - - class Meta: - model = TunnelGroup - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated', - 'tunnel_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'tunnel_count') - - -class TunnelSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField( - view_name='vpn-api:tunnel-detail' - ) - status = ChoiceField( - choices=TunnelStatusChoices - ) - group = NestedTunnelGroupSerializer( - required=False, - allow_null=True - ) - encapsulation = ChoiceField( - choices=TunnelEncapsulationChoices - ) - ipsec_profile = NestedIPSecProfileSerializer( - required=False, - allow_null=True - ) - tenant = NestedTenantSerializer( - required=False, - 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', 'terminations_count', - ) - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -class TunnelTerminationSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField( - view_name='vpn-api:tunneltermination-detail' - ) - tunnel = NestedTunnelSerializer() - role = ChoiceField( - choices=TunnelTerminationRoleChoices - ) - termination_type = ContentTypeField( - queryset=ContentType.objects.all() - ) - termination = serializers.SerializerMethodField( - read_only=True - ) - outside_ip = NestedIPAddressSerializer( - required=False, - allow_null=True - ) - - class Meta: - model = TunnelTermination - fields = ( - 'id', 'url', 'display', 'tunnel', 'role', 'termination_type', 'termination_id', 'termination', 'outside_ip', - 'tags', 'custom_fields', 'created', 'last_updated', - ) - brief_fields = ('id', 'url', 'display') - - @extend_schema_field(serializers.JSONField(allow_null=True)) - def get_termination(self, obj): - serializer = get_serializer_for_model(obj.termination, prefix=NESTED_SERIALIZER_PREFIX) - context = {'request': self.context['request']} - return serializer(obj.termination, context=context).data - - class IKEProposalSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField( view_name='vpn-api:ikeproposal-detail' @@ -215,8 +132,12 @@ class IPSecProfileSerializer(NetBoxModelSerializer): mode = ChoiceField( choices=IPSecModeChoices ) - ike_policy = NestedIKEPolicySerializer() - ipsec_policy = NestedIPSecPolicySerializer() + ike_policy = IKEPolicySerializer( + nested=True + ) + ipsec_policy = IPSecPolicySerializer( + nested=True + ) class Meta: model = IPSecProfile @@ -227,6 +148,100 @@ class IPSecProfileSerializer(NetBoxModelSerializer): brief_fields = ('id', 'url', 'display', 'name', 'description') +# +# Tunnels +# + +class TunnelGroupSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='vpn-api:tunnelgroup-detail') + + # Related object counts + tunnel_count = RelatedObjectCountField('tunnels') + + class Meta: + model = TunnelGroup + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated', + 'tunnel_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'tunnel_count') + + +class TunnelSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField( + view_name='vpn-api:tunnel-detail' + ) + status = ChoiceField( + choices=TunnelStatusChoices + ) + group = TunnelGroupSerializer( + nested=True, + required=False, + allow_null=True + ) + encapsulation = ChoiceField( + choices=TunnelEncapsulationChoices + ) + ipsec_profile = IPSecProfileSerializer( + nested=True, + required=False, + allow_null=True + ) + tenant = TenantSerializer( + nested=True, + required=False, + 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', 'terminations_count', + ) + brief_fields = ('id', 'url', 'display', 'name', 'description') + + +class TunnelTerminationSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField( + view_name='vpn-api:tunneltermination-detail' + ) + tunnel = TunnelSerializer( + nested=True + ) + role = ChoiceField( + choices=TunnelTerminationRoleChoices + ) + termination_type = ContentTypeField( + queryset=ContentType.objects.all() + ) + termination = serializers.SerializerMethodField( + read_only=True + ) + outside_ip = IPAddressSerializer( + nested=True, + required=False, + allow_null=True + ) + + class Meta: + model = TunnelTermination + fields = ( + 'id', 'url', 'display', 'tunnel', 'role', 'termination_type', 'termination_id', 'termination', 'outside_ip', + 'tags', 'custom_fields', 'created', 'last_updated', + ) + brief_fields = ('id', 'url', 'display') + + @extend_schema_field(serializers.JSONField(allow_null=True)) + def get_termination(self, obj): + serializer = get_serializer_for_model(obj.termination, prefix=NESTED_SERIALIZER_PREFIX) + context = {'request': self.context['request']} + return serializer(obj.termination, context=context).data + + # # L2VPN # @@ -246,7 +261,7 @@ class L2VPNSerializer(NetBoxModelSerializer): required=False, many=True ) - tenant = NestedTenantSerializer(required=False, allow_null=True) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) class Meta: model = L2VPN @@ -259,7 +274,9 @@ class L2VPNSerializer(NetBoxModelSerializer): class L2VPNTerminationSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='vpn-api:l2vpntermination-detail') - l2vpn = NestedL2VPNSerializer() + l2vpn = L2VPNSerializer( + nested=True + ) assigned_object_type = ContentTypeField( queryset=ContentType.objects.all() ) diff --git a/netbox/wireless/api/serializers.py b/netbox/wireless/api/serializers.py index 0aec97fd2..94fd0a94b 100644 --- a/netbox/wireless/api/serializers.py +++ b/netbox/wireless/api/serializers.py @@ -1,11 +1,11 @@ from rest_framework import serializers from dcim.choices import LinkStatusChoices -from dcim.api.serializers import NestedInterfaceSerializer -from ipam.api.serializers import NestedVLANSerializer +from dcim.api.serializers import InterfaceSerializer +from ipam.api.serializers import VLANSerializer from netbox.api.fields import ChoiceField from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer -from tenancy.api.nested_serializers import NestedTenantSerializer +from tenancy.api.serializers import TenantSerializer from wireless.choices import * from wireless.models import * from .nested_serializers import * @@ -33,10 +33,10 @@ class WirelessLANGroupSerializer(NestedGroupModelSerializer): class WirelessLANSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslan-detail') - group = NestedWirelessLANGroupSerializer(required=False, allow_null=True) + group = WirelessLANGroupSerializer(nested=True, required=False, allow_null=True) status = ChoiceField(choices=WirelessLANStatusChoices, required=False, allow_blank=True) - vlan = NestedVLANSerializer(required=False, allow_null=True) - tenant = NestedTenantSerializer(required=False, allow_null=True) + vlan = VLANSerializer(nested=True, required=False, allow_null=True) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) auth_type = ChoiceField(choices=WirelessAuthTypeChoices, required=False, allow_blank=True) auth_cipher = ChoiceField(choices=WirelessAuthCipherChoices, required=False, allow_blank=True) @@ -52,9 +52,9 @@ class WirelessLANSerializer(NetBoxModelSerializer): class WirelessLinkSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslink-detail') status = ChoiceField(choices=LinkStatusChoices, required=False) - interface_a = NestedInterfaceSerializer() - interface_b = NestedInterfaceSerializer() - tenant = NestedTenantSerializer(required=False, allow_null=True) + interface_a = InterfaceSerializer(nested=True) + interface_b = InterfaceSerializer(nested=True) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) auth_type = ChoiceField(choices=WirelessAuthTypeChoices, required=False, allow_blank=True) auth_cipher = ChoiceField(choices=WirelessAuthCipherChoices, required=False, allow_blank=True) From c382ba0ae0d630a198fe350753481cb3e71d6ec8 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 27 Feb 2024 13:26:43 -0500 Subject: [PATCH 02/47] Refactor REST API serializers to avoid circular imports --- netbox/circuits/api/serializers.py | 145 +- netbox/circuits/api/serializers_/__init__.py | 0 netbox/circuits/api/serializers_/circuits.py | 81 + netbox/circuits/api/serializers_/providers.py | 67 + netbox/core/api/serializers.py | 78 +- netbox/core/api/serializers_/__init__.py | 0 netbox/core/api/serializers_/data.py | 53 + netbox/core/api/serializers_/jobs.py | 31 + netbox/dcim/api/serializers.py | 1377 +---------------- netbox/dcim/api/serializers_/__init__.py | 0 netbox/dcim/api/serializers_/base.py | 38 + netbox/dcim/api/serializers_/cables.py | 127 ++ .../api/serializers_/device_components.py | 366 +++++ netbox/dcim/api/serializers_/devices.py | 165 ++ .../api/serializers_/devicetype_components.py | 328 ++++ netbox/dcim/api/serializers_/devicetypes.py | 74 + netbox/dcim/api/serializers_/manufacturers.py | 26 + netbox/dcim/api/serializers_/platforms.py | 29 + netbox/dcim/api/serializers_/power.py | 80 + netbox/dcim/api/serializers_/racks.py | 117 ++ netbox/dcim/api/serializers_/rackunits.py | 31 + netbox/dcim/api/serializers_/roles.py | 43 + netbox/dcim/api/serializers_/sites.py | 97 ++ .../dcim/api/serializers_/virtualchassis.py | 25 + netbox/extras/api/serializers.py | 679 +------- netbox/extras/api/serializers_/__init__.py | 0 netbox/extras/api/serializers_/attachments.py | 50 + netbox/extras/api/serializers_/bookmarks.py | 35 + .../extras/api/serializers_/change_logging.py | 59 + .../extras/api/serializers_/contenttypes.py | 16 + .../extras/api/serializers_/customfields.py | 91 ++ netbox/extras/api/serializers_/customlinks.py | 26 + netbox/extras/api/serializers_/dashboard.py | 13 + netbox/extras/api/serializers_/events.py | 75 + .../api/serializers_/exporttemplates.py | 36 + netbox/extras/api/serializers_/journaling.py | 67 + .../extras/api/serializers_/provisioning.py | 147 ++ .../extras/api/serializers_/savedfilters.py | 26 + netbox/extras/api/serializers_/scripts.py | 77 + netbox/extras/api/serializers_/tags.py | 30 + netbox/ipam/api/serializers.py | 517 +------ netbox/ipam/api/serializers_/__init__.py | 0 netbox/ipam/api/serializers_/asns.py | 78 + netbox/ipam/api/serializers_/fhrpgroups.py | 53 + netbox/ipam/api/serializers_/ip.py | 200 +++ netbox/ipam/api/serializers_/roles.py | 25 + netbox/ipam/api/serializers_/services.py | 48 + netbox/ipam/api/serializers_/vlans.py | 115 ++ netbox/ipam/api/serializers_/vrfs.py | 55 + netbox/tenancy/api/serializers.py | 124 +- netbox/tenancy/api/serializers_/__init__.py | 0 netbox/tenancy/api/serializers_/contacts.py | 82 + netbox/tenancy/api/serializers_/tenants.py | 51 + netbox/users/api/serializers.py | 192 +-- netbox/users/api/serializers_/__init__.py | 0 netbox/users/api/serializers_/permissions.py | 43 + netbox/users/api/serializers_/tokens.py | 94 ++ netbox/users/api/serializers_/users.py | 72 + netbox/virtualization/api/serializers.py | 188 +-- .../api/serializers_/__init__.py | 0 .../api/serializers_/clusters.py | 65 + .../api/serializers_/virtualmachines.py | 143 ++ netbox/vpn/api/serializers.py | 299 +--- netbox/vpn/api/serializers_/__init__.py | 0 netbox/vpn/api/serializers_/crypto.py | 135 ++ netbox/vpn/api/serializers_/l2vpn.py | 69 + netbox/vpn/api/serializers_/tunnels.py | 114 ++ netbox/wireless/api/serializers.py | 68 +- netbox/wireless/api/serializers_/__init__.py | 0 .../wireless/api/serializers_/wirelesslans.py | 46 + .../api/serializers_/wirelesslinks.py | 31 + 71 files changed, 4096 insertions(+), 3616 deletions(-) create mode 100644 netbox/circuits/api/serializers_/__init__.py create mode 100644 netbox/circuits/api/serializers_/circuits.py create mode 100644 netbox/circuits/api/serializers_/providers.py create mode 100644 netbox/core/api/serializers_/__init__.py create mode 100644 netbox/core/api/serializers_/data.py create mode 100644 netbox/core/api/serializers_/jobs.py create mode 100644 netbox/dcim/api/serializers_/__init__.py create mode 100644 netbox/dcim/api/serializers_/base.py create mode 100644 netbox/dcim/api/serializers_/cables.py create mode 100644 netbox/dcim/api/serializers_/device_components.py create mode 100644 netbox/dcim/api/serializers_/devices.py create mode 100644 netbox/dcim/api/serializers_/devicetype_components.py create mode 100644 netbox/dcim/api/serializers_/devicetypes.py create mode 100644 netbox/dcim/api/serializers_/manufacturers.py create mode 100644 netbox/dcim/api/serializers_/platforms.py create mode 100644 netbox/dcim/api/serializers_/power.py create mode 100644 netbox/dcim/api/serializers_/racks.py create mode 100644 netbox/dcim/api/serializers_/rackunits.py create mode 100644 netbox/dcim/api/serializers_/roles.py create mode 100644 netbox/dcim/api/serializers_/sites.py create mode 100644 netbox/dcim/api/serializers_/virtualchassis.py create mode 100644 netbox/extras/api/serializers_/__init__.py create mode 100644 netbox/extras/api/serializers_/attachments.py create mode 100644 netbox/extras/api/serializers_/bookmarks.py create mode 100644 netbox/extras/api/serializers_/change_logging.py create mode 100644 netbox/extras/api/serializers_/contenttypes.py create mode 100644 netbox/extras/api/serializers_/customfields.py create mode 100644 netbox/extras/api/serializers_/customlinks.py create mode 100644 netbox/extras/api/serializers_/dashboard.py create mode 100644 netbox/extras/api/serializers_/events.py create mode 100644 netbox/extras/api/serializers_/exporttemplates.py create mode 100644 netbox/extras/api/serializers_/journaling.py create mode 100644 netbox/extras/api/serializers_/provisioning.py create mode 100644 netbox/extras/api/serializers_/savedfilters.py create mode 100644 netbox/extras/api/serializers_/scripts.py create mode 100644 netbox/extras/api/serializers_/tags.py create mode 100644 netbox/ipam/api/serializers_/__init__.py create mode 100644 netbox/ipam/api/serializers_/asns.py create mode 100644 netbox/ipam/api/serializers_/fhrpgroups.py create mode 100644 netbox/ipam/api/serializers_/ip.py create mode 100644 netbox/ipam/api/serializers_/roles.py create mode 100644 netbox/ipam/api/serializers_/services.py create mode 100644 netbox/ipam/api/serializers_/vlans.py create mode 100644 netbox/ipam/api/serializers_/vrfs.py create mode 100644 netbox/tenancy/api/serializers_/__init__.py create mode 100644 netbox/tenancy/api/serializers_/contacts.py create mode 100644 netbox/tenancy/api/serializers_/tenants.py create mode 100644 netbox/users/api/serializers_/__init__.py create mode 100644 netbox/users/api/serializers_/permissions.py create mode 100644 netbox/users/api/serializers_/tokens.py create mode 100644 netbox/users/api/serializers_/users.py create mode 100644 netbox/virtualization/api/serializers_/__init__.py create mode 100644 netbox/virtualization/api/serializers_/clusters.py create mode 100644 netbox/virtualization/api/serializers_/virtualmachines.py create mode 100644 netbox/vpn/api/serializers_/__init__.py create mode 100644 netbox/vpn/api/serializers_/crypto.py create mode 100644 netbox/vpn/api/serializers_/l2vpn.py create mode 100644 netbox/vpn/api/serializers_/tunnels.py create mode 100644 netbox/wireless/api/serializers_/__init__.py create mode 100644 netbox/wireless/api/serializers_/wirelesslans.py create mode 100644 netbox/wireless/api/serializers_/wirelesslinks.py diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py index c9341853b..5e048218c 100644 --- a/netbox/circuits/api/serializers.py +++ b/netbox/circuits/api/serializers.py @@ -1,144 +1,3 @@ -from rest_framework import serializers - -from circuits.choices import CircuitStatusChoices -from circuits.models import * -from dcim.api.serializers import CabledObjectSerializer, SiteSerializer -from ipam.api.nested_serializers import NestedASNSerializer -from ipam.models import ASN -from netbox.api.fields import ChoiceField, RelatedObjectCountField, SerializedPKRelatedField -from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer -from tenancy.api.serializers import TenantSerializer +from .serializers_.providers import * +from .serializers_.circuits import * from .nested_serializers import * - - -# -# Providers -# - -class ProviderSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail') - accounts = SerializedPKRelatedField( - queryset=ProviderAccount.objects.all(), - serializer=NestedProviderAccountSerializer, - required=False, - many=True - ) - asns = SerializedPKRelatedField( - queryset=ASN.objects.all(), - serializer=NestedASNSerializer, - required=False, - many=True - ) - - # Related object counts - circuit_count = RelatedObjectCountField('circuits') - - class Meta: - model = Provider - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'accounts', 'description', 'comments', 'asns', 'tags', - 'custom_fields', 'created', 'last_updated', 'circuit_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'circuit_count') - - -# -# Provider Accounts -# - -class ProviderAccountSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provideraccount-detail') - provider = ProviderSerializer(nested=True) - - class Meta: - model = ProviderAccount - fields = [ - 'id', 'url', 'display', 'provider', 'name', 'account', 'description', 'comments', 'tags', 'custom_fields', - 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'account', 'description') - - -# -# Provider networks -# - -class ProviderNetworkSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='circuits-api:providernetwork-detail') - provider = ProviderSerializer(nested=True) - - class Meta: - model = ProviderNetwork - fields = [ - 'id', 'url', 'display', 'provider', 'name', 'service_id', 'description', 'comments', 'tags', - 'custom_fields', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -# -# Circuits -# - -class CircuitTypeSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail') - - # 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', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'circuit_count') - - -class CircuitCircuitTerminationSerializer(WritableNestedSerializer): - url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail') - site = SiteSerializer(nested=True, allow_null=True) - provider_network = ProviderNetworkSerializer(nested=True, allow_null=True) - - class Meta: - model = CircuitTermination - fields = [ - 'id', 'url', 'display', 'site', 'provider_network', 'port_speed', 'upstream_speed', 'xconnect_id', - 'description', - ] - - -class CircuitSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail') - provider = ProviderSerializer(nested=True) - provider_account = ProviderAccountSerializer(nested=True, required=False, allow_null=True) - status = ChoiceField(choices=CircuitStatusChoices, required=False) - type = CircuitTypeSerializer(nested=True) - tenant = TenantSerializer(nested=True, required=False, allow_null=True) - termination_a = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True) - termination_z = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True) - - class Meta: - model = Circuit - fields = [ - 'id', 'url', 'display', 'cid', 'provider', 'provider_account', 'type', 'status', 'tenant', 'install_date', - 'termination_date', 'commit_rate', 'description', 'termination_a', 'termination_z', 'comments', 'tags', - 'custom_fields', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'cid', 'description') - - -class CircuitTerminationSerializer(NetBoxModelSerializer, CabledObjectSerializer): - url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail') - circuit = CircuitSerializer(nested=True) - site = SiteSerializer(nested=True, required=False, allow_null=True) - provider_network = ProviderNetworkSerializer(nested=True, required=False, allow_null=True) - - class Meta: - model = CircuitTermination - fields = [ - 'id', 'url', 'display', 'circuit', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed', - 'xconnect_id', 'pp_info', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', - 'link_peers_type', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied', - ] - brief_fields = ('id', 'url', 'display', 'circuit', 'term_side', 'description', 'cable', '_occupied') diff --git a/netbox/circuits/api/serializers_/__init__.py b/netbox/circuits/api/serializers_/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/circuits/api/serializers_/circuits.py b/netbox/circuits/api/serializers_/circuits.py new file mode 100644 index 000000000..b59c73f09 --- /dev/null +++ b/netbox/circuits/api/serializers_/circuits.py @@ -0,0 +1,81 @@ +from rest_framework import serializers + +from circuits.choices import CircuitStatusChoices +from circuits.models import Circuit, CircuitTermination, CircuitType +from dcim.api.serializers_.cables import CabledObjectSerializer +from dcim.api.serializers_.sites import SiteSerializer +from netbox.api.fields import ChoiceField, RelatedObjectCountField +from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer +from tenancy.api.serializers_.tenants import TenantSerializer + +from .providers import ProviderAccountSerializer, ProviderNetworkSerializer, ProviderSerializer + +__all__ = ( + 'CircuitSerializer', + 'CircuitTerminationSerializer', + 'CircuitTypeSerializer', +) + + +class CircuitTypeSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail') + + # 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', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'circuit_count') + + +class CircuitCircuitTerminationSerializer(WritableNestedSerializer): + url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail') + site = SiteSerializer(nested=True, allow_null=True) + provider_network = ProviderNetworkSerializer(nested=True, allow_null=True) + + class Meta: + model = CircuitTermination + fields = [ + 'id', 'url', 'display', 'site', 'provider_network', 'port_speed', 'upstream_speed', 'xconnect_id', + 'description', + ] + + +class CircuitSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail') + provider = ProviderSerializer(nested=True) + provider_account = ProviderAccountSerializer(nested=True, required=False, allow_null=True) + status = ChoiceField(choices=CircuitStatusChoices, required=False) + type = CircuitTypeSerializer(nested=True) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) + termination_a = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True) + termination_z = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True) + + class Meta: + model = Circuit + fields = [ + 'id', 'url', 'display', 'cid', 'provider', 'provider_account', 'type', 'status', 'tenant', 'install_date', + 'termination_date', 'commit_rate', 'description', 'termination_a', 'termination_z', 'comments', 'tags', + 'custom_fields', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'cid', 'description') + + +class CircuitTerminationSerializer(NetBoxModelSerializer, CabledObjectSerializer): + url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail') + circuit = CircuitSerializer(nested=True) + site = SiteSerializer(nested=True, required=False, allow_null=True) + provider_network = ProviderNetworkSerializer(nested=True, required=False, allow_null=True) + + class Meta: + model = CircuitTermination + fields = [ + 'id', 'url', 'display', 'circuit', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed', + 'xconnect_id', 'pp_info', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', + 'link_peers_type', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied', + ] + brief_fields = ('id', 'url', 'display', 'circuit', 'term_side', 'description', 'cable', '_occupied') diff --git a/netbox/circuits/api/serializers_/providers.py b/netbox/circuits/api/serializers_/providers.py new file mode 100644 index 000000000..76ea37360 --- /dev/null +++ b/netbox/circuits/api/serializers_/providers.py @@ -0,0 +1,67 @@ +from rest_framework import serializers + +from circuits.models import Provider, ProviderAccount, ProviderNetwork +from ipam.api.nested_serializers import NestedASNSerializer +from ipam.models import ASN +from netbox.api.fields import RelatedObjectCountField, SerializedPKRelatedField +from netbox.api.serializers import NetBoxModelSerializer +from ..nested_serializers import * + +__all__ = ( + 'ProviderAccountSerializer', + 'ProviderNetworkSerializer', + 'ProviderSerializer', +) + + +class ProviderSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail') + accounts = SerializedPKRelatedField( + queryset=ProviderAccount.objects.all(), + serializer=NestedProviderAccountSerializer, + required=False, + many=True + ) + asns = SerializedPKRelatedField( + queryset=ASN.objects.all(), + serializer=NestedASNSerializer, + required=False, + many=True + ) + + # Related object counts + circuit_count = RelatedObjectCountField('circuits') + + class Meta: + model = Provider + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'accounts', 'description', 'comments', 'asns', 'tags', + 'custom_fields', 'created', 'last_updated', 'circuit_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'circuit_count') + + +class ProviderAccountSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provideraccount-detail') + provider = ProviderSerializer(nested=True) + + class Meta: + model = ProviderAccount + fields = [ + 'id', 'url', 'display', 'provider', 'name', 'account', 'description', 'comments', 'tags', 'custom_fields', + 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'account', 'description') + + +class ProviderNetworkSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='circuits-api:providernetwork-detail') + provider = ProviderSerializer(nested=True) + + class Meta: + model = ProviderNetwork + fields = [ + 'id', 'url', 'display', 'provider', 'name', 'service_id', 'description', 'comments', 'tags', + 'custom_fields', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') diff --git a/netbox/core/api/serializers.py b/netbox/core/api/serializers.py index 3a2794304..8553bb91c 100644 --- a/netbox/core/api/serializers.py +++ b/netbox/core/api/serializers.py @@ -1,75 +1,3 @@ -from rest_framework import serializers - -from core.choices import * -from core.models import * -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.serializers import UserSerializer - -__all__ = ( - 'DataFileSerializer', - 'DataSourceSerializer', - 'JobSerializer', -) - - -class DataSourceSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField( - view_name='core-api:datasource-detail' - ) - type = ChoiceField( - choices=get_data_backend_choices() - ) - status = ChoiceField( - choices=DataSourceStatusChoices, - read_only=True - ) - - # Related object counts - file_count = RelatedObjectCountField('datafiles') - - class Meta: - model = DataSource - fields = [ - 'id', 'url', 'display', 'name', 'type', 'source_url', 'enabled', 'status', 'description', 'comments', - 'parameters', 'ignore_rules', 'custom_fields', 'created', 'last_updated', 'file_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -class DataFileSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField( - view_name='core-api:datafile-detail' - ) - source = DataSourceSerializer( - nested=True, - read_only=True - ) - - class Meta: - model = DataFile - fields = [ - 'id', 'url', 'display', 'source', 'path', 'last_updated', 'size', 'hash', - ] - brief_fields = ('id', 'url', 'display', 'path') - - -class JobSerializer(BaseModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='core-api:job-detail') - user = UserSerializer( - nested=True, - read_only=True - ) - status = ChoiceField(choices=JobStatusChoices, read_only=True) - object_type = ContentTypeField( - read_only=True - ) - - class Meta: - model = Job - fields = [ - 'id', 'url', 'display', 'object_type', 'object_id', 'name', 'status', 'created', 'scheduled', 'interval', - 'started', 'completed', 'user', 'data', 'error', 'job_id', - ] - brief_fields = ('url', 'created', 'completed', 'user', 'status') +from .serializers_.data import * +from .serializers_.jobs import * +from .nested_serializers import * diff --git a/netbox/core/api/serializers_/__init__.py b/netbox/core/api/serializers_/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/core/api/serializers_/data.py b/netbox/core/api/serializers_/data.py new file mode 100644 index 000000000..e60d8d95b --- /dev/null +++ b/netbox/core/api/serializers_/data.py @@ -0,0 +1,53 @@ +from rest_framework import serializers + +from core.choices import * +from core.models import DataFile, DataSource +from netbox.api.fields import ChoiceField, RelatedObjectCountField +from netbox.api.serializers import NetBoxModelSerializer +from netbox.utils import get_data_backend_choices + +__all__ = ( + 'DataFileSerializer', + 'DataSourceSerializer', +) + + +class DataSourceSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField( + view_name='core-api:datasource-detail' + ) + type = ChoiceField( + choices=get_data_backend_choices() + ) + status = ChoiceField( + choices=DataSourceStatusChoices, + read_only=True + ) + + # Related object counts + file_count = RelatedObjectCountField('datafiles') + + class Meta: + model = DataSource + fields = [ + 'id', 'url', 'display', 'name', 'type', 'source_url', 'enabled', 'status', 'description', 'comments', + 'parameters', 'ignore_rules', 'custom_fields', 'created', 'last_updated', 'file_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') + + +class DataFileSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField( + view_name='core-api:datafile-detail' + ) + source = DataSourceSerializer( + nested=True, + read_only=True + ) + + class Meta: + model = DataFile + fields = [ + 'id', 'url', 'display', 'source', 'path', 'last_updated', 'size', 'hash', + ] + brief_fields = ('id', 'url', 'display', 'path') diff --git a/netbox/core/api/serializers_/jobs.py b/netbox/core/api/serializers_/jobs.py new file mode 100644 index 000000000..84afa3627 --- /dev/null +++ b/netbox/core/api/serializers_/jobs.py @@ -0,0 +1,31 @@ +from rest_framework import serializers + +from core.choices import * +from core.models import Job +from netbox.api.fields import ChoiceField, ContentTypeField +from netbox.api.serializers import BaseModelSerializer +from users.api.serializers_.users import UserSerializer + +__all__ = ( + 'JobSerializer', +) + + +class JobSerializer(BaseModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='core-api:job-detail') + user = UserSerializer( + nested=True, + read_only=True + ) + status = ChoiceField(choices=JobStatusChoices, read_only=True) + object_type = ContentTypeField( + read_only=True + ) + + class Meta: + model = Job + fields = [ + 'id', 'url', 'display', 'object_type', 'object_id', 'name', 'status', 'created', 'scheduled', 'interval', + 'started', 'completed', 'user', 'data', 'error', 'job_id', + ] + brief_fields = ('url', 'created', 'completed', 'user', 'status') diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index af0e4b2d6..4f8bbac17 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -1,1365 +1,14 @@ -import decimal - -from django.contrib.contenttypes.models import ContentType -from django.utils.translation import gettext as _ -from drf_spectacular.types import OpenApiTypes -from drf_spectacular.utils import extend_schema_field -from rest_framework import serializers -from timezone_field.rest_framework import TimeZoneSerializerField - -from dcim.choices import * -from dcim.constants import * -from dcim.models import * -from extras.api.serializers import ConfigTemplateSerializer -from ipam.api.nested_serializers import ( - NestedASNSerializer, NestedIPAddressSerializer, NestedVLANSerializer, NestedVRFSerializer, -) -from ipam.models import ASN, VLAN -from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField, SerializedPKRelatedField -from netbox.api.serializers import ( - GenericObjectSerializer, NestedGroupModelSerializer, NetBoxModelSerializer, ValidatedModelSerializer, - WritableNestedSerializer, -) -from netbox.config import ConfigItem -from netbox.constants import NESTED_SERIALIZER_PREFIX -from tenancy.api.serializers import TenantSerializer -from users.api.serializers import UserSerializer -from utilities.api import get_serializer_for_model -from virtualization.api.nested_serializers import NestedClusterSerializer -from vpn.api.nested_serializers import NestedL2VPNTerminationSerializer -from wireless.api.nested_serializers import NestedWirelessLANSerializer, NestedWirelessLinkSerializer -from wireless.choices import * -from wireless.models import WirelessLAN +from .serializers_.cables import * +from .serializers_.sites import * +from .serializers_.racks import * +from .serializers_.manufacturers import * +from .serializers_.platforms import * +from .serializers_.roles import * +from .serializers_.devicetypes import * +from .serializers_.devicetype_components import * +from .serializers_.virtualchassis import * +from .serializers_.devices import * +from .serializers_.device_components import * +from .serializers_.power import * +from .serializers_.rackunits import * from .nested_serializers import * - - -# -# Cables -# - -class CableSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail') - a_terminations = GenericObjectSerializer(many=True, required=False) - b_terminations = GenericObjectSerializer(many=True, required=False) - status = ChoiceField(choices=LinkStatusChoices, required=False) - tenant = TenantSerializer(nested=True, required=False, allow_null=True) - length_unit = ChoiceField(choices=CableLengthUnitChoices, allow_blank=True, required=False, allow_null=True) - - class Meta: - model = Cable - fields = [ - 'id', 'url', 'display', 'type', 'a_terminations', 'b_terminations', 'status', 'tenant', 'label', 'color', - 'length', 'length_unit', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'label', 'description') - - -class TracedCableSerializer(serializers.ModelSerializer): - """ - Used only while tracing a cable path. - """ - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail') - - class Meta: - model = Cable - fields = [ - 'id', 'url', 'type', 'status', 'label', 'color', 'length', 'length_unit', 'description', - ] - - -class CableTerminationSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cabletermination-detail') - termination_type = ContentTypeField( - queryset=ContentType.objects.filter(CABLE_TERMINATION_MODELS) - ) - termination = serializers.SerializerMethodField(read_only=True) - - class Meta: - model = CableTermination - fields = [ - 'id', 'url', 'display', 'cable', 'cable_end', 'termination_type', 'termination_id', 'termination', - 'created', 'last_updated', - ] - - @extend_schema_field(serializers.JSONField(allow_null=True)) - def get_termination(self, obj): - serializer = get_serializer_for_model(obj.termination, prefix=NESTED_SERIALIZER_PREFIX) - context = {'request': self.context['request']} - return serializer(obj.termination, context=context).data - - -class CablePathSerializer(serializers.ModelSerializer): - path = serializers.SerializerMethodField(read_only=True) - - class Meta: - model = CablePath - fields = ['id', 'path', 'is_active', 'is_complete', 'is_split'] - - @extend_schema_field(serializers.ListField) - def get_path(self, obj): - ret = [] - for nodes in obj.path_objects: - serializer = get_serializer_for_model(nodes[0], prefix=NESTED_SERIALIZER_PREFIX) - context = {'request': self.context['request']} - ret.append(serializer(nodes, context=context, many=True).data) - return ret - - -class CabledObjectSerializer(serializers.ModelSerializer): - cable = CableSerializer(nested=True, read_only=True, allow_null=True) - cable_end = serializers.CharField(read_only=True) - link_peers_type = serializers.SerializerMethodField(read_only=True) - link_peers = serializers.SerializerMethodField(read_only=True) - _occupied = serializers.SerializerMethodField(read_only=True) - - @extend_schema_field(OpenApiTypes.STR) - def get_link_peers_type(self, obj): - """ - Return the type of the peer link terminations, or None. - """ - if not obj.cable: - return None - - if obj.link_peers: - return f'{obj.link_peers[0]._meta.app_label}.{obj.link_peers[0]._meta.model_name}' - - return None - - @extend_schema_field(serializers.ListField) - def get_link_peers(self, obj): - """ - Return the appropriate serializer for the link termination model. - """ - if not obj.link_peers: - return [] - - # Return serialized peer termination objects - serializer = get_serializer_for_model(obj.link_peers[0], prefix=NESTED_SERIALIZER_PREFIX) - context = {'request': self.context['request']} - return serializer(obj.link_peers, context=context, many=True).data - - @extend_schema_field(serializers.BooleanField) - def get__occupied(self, obj): - return obj._occupied - - -# -# Regions/sites -# - -class RegionSerializer(NestedGroupModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail') - parent = NestedRegionSerializer(required=False, allow_null=True, default=None) - site_count = serializers.IntegerField(read_only=True) - - class Meta: - model = Region - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created', - 'last_updated', 'site_count', '_depth', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'site_count', '_depth') - - -class SiteGroupSerializer(NestedGroupModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:sitegroup-detail') - parent = NestedSiteGroupSerializer(required=False, allow_null=True, default=None) - site_count = serializers.IntegerField(read_only=True) - - class Meta: - model = SiteGroup - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created', - 'last_updated', 'site_count', '_depth', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'site_count', '_depth') - - -class SiteSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:site-detail') - status = ChoiceField(choices=SiteStatusChoices, required=False) - region = RegionSerializer(nested=True, required=False, allow_null=True) - group = SiteGroupSerializer(nested=True, required=False, allow_null=True) - tenant = TenantSerializer(required=False, allow_null=True) - time_zone = TimeZoneSerializerField(required=False, allow_null=True) - asns = SerializedPKRelatedField( - queryset=ASN.objects.all(), - serializer=NestedASNSerializer, - required=False, - many=True - ) - - # Related object counts - 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 - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'time_zone', - 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments', 'asns', 'tags', - 'custom_fields', 'created', 'last_updated', 'circuit_count', 'device_count', 'prefix_count', 'rack_count', - 'virtualmachine_count', 'vlan_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description', 'slug') - - -# -# Racks -# - -class LocationSerializer(NestedGroupModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:location-detail') - site = SiteSerializer(nested=True) - parent = NestedLocationSerializer(required=False, allow_null=True) - status = ChoiceField(choices=LocationStatusChoices, required=False) - tenant = TenantSerializer(nested=True, required=False, allow_null=True) - rack_count = serializers.IntegerField(read_only=True) - device_count = serializers.IntegerField(read_only=True) - - class Meta: - model = Location - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'site', 'parent', 'status', 'tenant', 'description', 'tags', - 'custom_fields', 'created', 'last_updated', 'rack_count', 'device_count', '_depth', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'rack_count', '_depth') - - -class RackRoleSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail') - - # Related object counts - rack_count = RelatedObjectCountField('racks') - - class Meta: - model = RackRole - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created', - 'last_updated', 'rack_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'rack_count') - - -class RackSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail') - site = SiteSerializer(nested=True) - location = LocationSerializer(nested=True, required=False, allow_null=True, default=None) - tenant = TenantSerializer(nested=True, required=False, allow_null=True) - status = ChoiceField(choices=RackStatusChoices, required=False) - role = RackRoleSerializer(nested=True, required=False, allow_null=True) - type = ChoiceField(choices=RackTypeChoices, allow_blank=True, required=False, allow_null=True) - facility_id = serializers.CharField(max_length=50, allow_blank=True, allow_null=True, label=_('Facility ID'), - default=None) - 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) - - # Related object counts - device_count = RelatedObjectCountField('devices') - powerfeed_count = RelatedObjectCountField('powerfeeds') - - class Meta: - model = Rack - fields = [ - 'id', 'url', 'display', 'name', 'facility_id', 'site', 'location', 'tenant', 'status', 'role', 'serial', - 'asset_tag', 'type', 'width', 'u_height', 'starting_unit', 'weight', 'max_weight', 'weight_unit', - 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'description', 'comments', - 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'powerfeed_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description', 'device_count') - - -class RackReservationSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackreservation-detail') - rack = RackSerializer(nested=True) - user = UserSerializer(nested=True) - tenant = TenantSerializer(nested=True, required=False, allow_null=True) - - class Meta: - model = RackReservation - fields = [ - 'id', 'url', 'display', 'rack', 'units', 'created', 'last_updated', 'user', 'tenant', 'description', - 'comments', 'tags', 'custom_fields', - ] - brief_fields = ('id', 'url', 'display', 'user', 'description', 'units') - - -class RackElevationDetailFilterSerializer(serializers.Serializer): - q = serializers.CharField( - required=False, - default=None - ) - face = serializers.ChoiceField( - choices=DeviceFaceChoices, - default=DeviceFaceChoices.FACE_FRONT - ) - render = serializers.ChoiceField( - choices=RackElevationDetailRenderChoices, - default=RackElevationDetailRenderChoices.RENDER_JSON - ) - unit_width = serializers.IntegerField( - default=ConfigItem('RACK_ELEVATION_DEFAULT_UNIT_WIDTH') - ) - unit_height = serializers.IntegerField( - default=ConfigItem('RACK_ELEVATION_DEFAULT_UNIT_HEIGHT') - ) - legend_width = serializers.IntegerField( - default=RACK_ELEVATION_DEFAULT_LEGEND_WIDTH - ) - margin_width = serializers.IntegerField( - default=RACK_ELEVATION_DEFAULT_MARGIN_WIDTH - ) - exclude = serializers.IntegerField( - required=False, - default=None - ) - expand_devices = serializers.BooleanField( - required=False, - default=True - ) - include_images = serializers.BooleanField( - required=False, - default=True - ) - - -class ManufacturerSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail') - - # Related object counts - devicetype_count = RelatedObjectCountField('device_types') - inventoryitem_count = RelatedObjectCountField('inventory_items') - platform_count = RelatedObjectCountField('platforms') - - class Meta: - model = Manufacturer - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated', - 'devicetype_count', 'inventoryitem_count', 'platform_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'devicetype_count') - - -class DeviceRoleSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail') - config_template = ConfigTemplateSerializer(nested=True, required=False, allow_null=True, default=None) - - # Related object counts - device_count = RelatedObjectCountField('devices') - virtualmachine_count = RelatedObjectCountField('virtual_machines') - - class Meta: - model = DeviceRole - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'color', 'vm_role', 'config_template', 'description', 'tags', - 'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'device_count', 'virtualmachine_count') - - -class InventoryItemRoleSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemrole-detail') - - # Related object counts - inventoryitem_count = RelatedObjectCountField('inventory_items') - - class Meta: - model = InventoryItemRole - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created', - 'last_updated', 'inventoryitem_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'inventoryitem_count') - - -class PlatformSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail') - manufacturer = ManufacturerSerializer(nested=True, required=False, allow_null=True) - config_template = ConfigTemplateSerializer(nested=True, required=False, allow_null=True, default=None) - - # Related object counts - device_count = RelatedObjectCountField('devices') - virtualmachine_count = RelatedObjectCountField('virtual_machines') - - class Meta: - model = Platform - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'manufacturer', 'config_template', 'description', 'tags', - 'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'device_count', 'virtualmachine_count') - - -# -# Device/module types -# - -class DeviceTypeSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail') - manufacturer = ManufacturerSerializer(nested=True) - default_platform = PlatformSerializer(nested=True, required=False, allow_null=True) - u_height = serializers.DecimalField( - max_digits=4, - decimal_places=1, - label=_('Position (U)'), - min_value=0, - default=1.0 - ) - 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) - front_image = serializers.URLField(allow_null=True, required=False) - rear_image = serializers.URLField(allow_null=True, required=False) - - # Counter fields - console_port_template_count = serializers.IntegerField(read_only=True) - console_server_port_template_count = serializers.IntegerField(read_only=True) - power_port_template_count = serializers.IntegerField(read_only=True) - power_outlet_template_count = serializers.IntegerField(read_only=True) - interface_template_count = serializers.IntegerField(read_only=True) - front_port_template_count = serializers.IntegerField(read_only=True) - rear_port_template_count = serializers.IntegerField(read_only=True) - device_bay_template_count = serializers.IntegerField(read_only=True) - module_bay_template_count = serializers.IntegerField(read_only=True) - inventory_item_template_count = serializers.IntegerField(read_only=True) - - # Related object counts - device_count = RelatedObjectCountField('instances') - - class Meta: - model = DeviceType - fields = [ - 'id', 'url', 'display', 'manufacturer', 'default_platform', 'model', 'slug', 'part_number', 'u_height', - 'exclude_from_utilization', 'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit', - 'front_image', 'rear_image', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', - 'device_count', 'console_port_template_count', 'console_server_port_template_count', 'power_port_template_count', - 'power_outlet_template_count', 'interface_template_count', 'front_port_template_count', - 'rear_port_template_count', 'device_bay_template_count', 'module_bay_template_count', - 'inventory_item_template_count', - ] - brief_fields = ('id', 'url', 'display', 'manufacturer', 'model', 'slug', 'description', 'device_count') - - -class ModuleTypeSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:moduletype-detail') - manufacturer = ManufacturerSerializer(nested=True) - weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True) - - class Meta: - model = ModuleType - fields = [ - 'id', 'url', 'display', 'manufacturer', 'model', 'part_number', 'weight', 'weight_unit', 'description', - 'comments', 'tags', 'custom_fields', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'manufacturer', 'model', 'description') - - -# -# Component templates -# - -class ConsolePortTemplateSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleporttemplate-detail') - device_type = DeviceTypeSerializer( - nested=True, - required=False, - allow_null=True, - default=None - ) - module_type = ModuleTypeSerializer( - nested=True, - required=False, - allow_null=True, - default=None - ) - type = ChoiceField( - choices=ConsolePortTypeChoices, - allow_blank=True, - required=False - ) - - class Meta: - model = ConsolePortTemplate - fields = [ - 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'description', 'created', - 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverporttemplate-detail') - device_type = DeviceTypeSerializer( - nested=True, - required=False, - allow_null=True, - default=None - ) - module_type = ModuleTypeSerializer( - nested=True, - required=False, - allow_null=True, - default=None - ) - type = ChoiceField( - choices=ConsolePortTypeChoices, - allow_blank=True, - required=False - ) - - class Meta: - model = ConsoleServerPortTemplate - fields = [ - 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'description', 'created', - 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -class PowerPortTemplateSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerporttemplate-detail') - device_type = DeviceTypeSerializer( - nested=True, - required=False, - allow_null=True, - default=None - ) - module_type = ModuleTypeSerializer( - nested=True, - required=False, - allow_null=True, - default=None - ) - type = ChoiceField( - choices=PowerPortTypeChoices, - allow_blank=True, - required=False, - allow_null=True - ) - - class Meta: - model = PowerPortTemplate - fields = [ - 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw', - 'allocated_draw', 'description', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -class PowerOutletTemplateSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlettemplate-detail') - device_type = DeviceTypeSerializer( - nested=True, - required=False, - allow_null=True, - default=None - ) - module_type = ModuleTypeSerializer( - nested=True, - required=False, - allow_null=True, - default=None - ) - type = ChoiceField( - choices=PowerOutletTypeChoices, - allow_blank=True, - required=False, - allow_null=True - ) - power_port = PowerPortTemplateSerializer( - nested=True, - required=False, - allow_null=True - ) - feed_leg = ChoiceField( - choices=PowerOutletFeedLegChoices, - allow_blank=True, - required=False, - allow_null=True - ) - - class Meta: - model = PowerOutletTemplate - fields = [ - 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'power_port', 'feed_leg', - 'description', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -class InterfaceTemplateSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interfacetemplate-detail') - device_type = DeviceTypeSerializer( - nested=True, - required=False, - allow_null=True, - default=None - ) - module_type = ModuleTypeSerializer( - nested=True, - required=False, - allow_null=True, - default=None - ) - type = ChoiceField(choices=InterfaceTypeChoices) - bridge = NestedInterfaceTemplateSerializer( - required=False, - allow_null=True - ) - poe_mode = ChoiceField( - choices=InterfacePoEModeChoices, - required=False, - allow_blank=True, - allow_null=True - ) - poe_type = ChoiceField( - choices=InterfacePoETypeChoices, - required=False, - allow_blank=True, - allow_null=True - ) - rf_role = ChoiceField( - choices=WirelessRoleChoices, - required=False, - allow_blank=True, - allow_null=True - ) - - class Meta: - model = InterfaceTemplate - fields = [ - 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'enabled', 'mgmt_only', - 'description', 'bridge', 'poe_mode', 'poe_type', 'rf_role', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -class RearPortTemplateSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearporttemplate-detail') - device_type = DeviceTypeSerializer( - required=False, - nested=True, - allow_null=True, - default=None - ) - module_type = ModuleTypeSerializer( - nested=True, - required=False, - allow_null=True, - default=None - ) - type = ChoiceField(choices=PortTypeChoices) - - class Meta: - model = RearPortTemplate - fields = [ - 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', - 'description', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -class FrontPortTemplateSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontporttemplate-detail') - device_type = DeviceTypeSerializer( - nested=True, - required=False, - allow_null=True, - default=None - ) - module_type = ModuleTypeSerializer( - nested=True, - required=False, - allow_null=True, - default=None - ) - type = ChoiceField(choices=PortTypeChoices) - rear_port = RearPortTemplateSerializer(nested=True) - - class Meta: - model = FrontPortTemplate - fields = [ - 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port', - 'rear_port_position', 'description', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -class ModuleBayTemplateSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebaytemplate-detail') - device_type = DeviceTypeSerializer( - nested=True - ) - - class Meta: - model = ModuleBayTemplate - fields = [ - 'id', 'url', 'display', 'device_type', 'name', 'label', 'position', 'description', 'created', - 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -class DeviceBayTemplateSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebaytemplate-detail') - device_type = DeviceTypeSerializer( - nested=True - ) - - class Meta: - model = DeviceBayTemplate - fields = ['id', 'url', 'display', 'device_type', 'name', 'label', 'description', 'created', 'last_updated'] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -class InventoryItemTemplateSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemtemplate-detail') - device_type = DeviceTypeSerializer( - nested=True - ) - parent = serializers.PrimaryKeyRelatedField( - queryset=InventoryItemTemplate.objects.all(), - allow_null=True, - default=None - ) - role = InventoryItemRoleSerializer(nested=True, required=False, allow_null=True) - manufacturer = ManufacturerSerializer( - nested=True, - required=False, - allow_null=True, - default=None - ) - component_type = ContentTypeField( - queryset=ContentType.objects.filter(MODULAR_COMPONENT_TEMPLATE_MODELS), - required=False, - allow_null=True - ) - component = serializers.SerializerMethodField(read_only=True) - _depth = serializers.IntegerField(source='level', read_only=True) - - class Meta: - model = InventoryItemTemplate - fields = [ - 'id', 'url', 'display', 'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', - 'description', 'component_type', 'component_id', 'component', 'created', 'last_updated', '_depth', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description', '_depth') - - @extend_schema_field(serializers.JSONField(allow_null=True)) - def get_component(self, obj): - if obj.component is None: - return None - serializer = get_serializer_for_model(obj.component, prefix=NESTED_SERIALIZER_PREFIX) - context = {'request': self.context['request']} - return serializer(obj.component, context=context).data - - -# -# Virtual chassis -# - -class VirtualChassisSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail') - master = NestedDeviceSerializer(required=False, allow_null=True, default=None) - - # Counter fields - member_count = serializers.IntegerField(read_only=True) - - class Meta: - model = VirtualChassis - fields = [ - 'id', 'url', 'display', 'name', 'domain', 'master', 'description', 'comments', 'tags', 'custom_fields', - 'created', 'last_updated', 'member_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'master', 'description', 'member_count') - - -# -# Devices -# - -class DeviceSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail') - device_type = DeviceTypeSerializer(nested=True) - role = DeviceRoleSerializer(nested=True) - device_role = DeviceRoleSerializer( - nested=True, - read_only=True, - help_text='Deprecated in v3.6 in favor of `role`.' - ) - tenant = TenantSerializer( - nested=True, - required=False, - allow_null=True, - default=None - ) - platform = PlatformSerializer(nested=True, required=False, allow_null=True) - site = SiteSerializer(nested=True) - location = LocationSerializer(nested=True, required=False, allow_null=True, default=None) - rack = RackSerializer(nested=True, required=False, allow_null=True, default=None) - face = ChoiceField(choices=DeviceFaceChoices, allow_blank=True, default=lambda: '') - position = serializers.DecimalField( - max_digits=4, - decimal_places=1, - allow_null=True, - label=_('Position (U)'), - min_value=decimal.Decimal(0.5), - default=None - ) - status = ChoiceField(choices=DeviceStatusChoices, required=False) - airflow = ChoiceField(choices=DeviceAirflowChoices, allow_blank=True, required=False) - primary_ip = NestedIPAddressSerializer(read_only=True) - primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True) - primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True) - oob_ip = NestedIPAddressSerializer(required=False, allow_null=True) - parent_device = serializers.SerializerMethodField() - cluster = NestedClusterSerializer(required=False, allow_null=True) - virtual_chassis = VirtualChassisSerializer(nested=True, required=False, allow_null=True, default=None) - vc_position = serializers.IntegerField(allow_null=True, max_value=255, min_value=0, default=None) - config_template = ConfigTemplateSerializer(nested=True, required=False, allow_null=True, default=None) - - # Counter fields - console_port_count = serializers.IntegerField(read_only=True) - console_server_port_count = serializers.IntegerField(read_only=True) - power_port_count = serializers.IntegerField(read_only=True) - power_outlet_count = serializers.IntegerField(read_only=True) - interface_count = serializers.IntegerField(read_only=True) - front_port_count = serializers.IntegerField(read_only=True) - rear_port_count = serializers.IntegerField(read_only=True) - device_bay_count = serializers.IntegerField(read_only=True) - module_bay_count = serializers.IntegerField(read_only=True) - inventory_item_count = serializers.IntegerField(read_only=True) - - class Meta: - model = Device - fields = [ - 'id', 'url', 'display', 'name', 'device_type', 'role', 'device_role', 'tenant', 'platform', 'serial', - 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device', - 'status', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', - 'vc_position', 'vc_priority', 'description', 'comments', 'config_template', 'local_context_data', 'tags', - 'custom_fields', 'created', 'last_updated', 'console_port_count', 'console_server_port_count', - 'power_port_count', 'power_outlet_count', 'interface_count', 'front_port_count', 'rear_port_count', - 'device_bay_count', 'module_bay_count', 'inventory_item_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - @extend_schema_field(NestedDeviceSerializer) - def get_parent_device(self, obj): - try: - device_bay = obj.parent_bay - except DeviceBay.DoesNotExist: - return None - context = {'request': self.context['request']} - data = NestedDeviceSerializer(instance=device_bay.device, context=context).data - data['device_bay'] = NestedDeviceBaySerializer(instance=device_bay, context=context).data - return data - - def get_device_role(self, obj): - return obj.role - - -class DeviceWithConfigContextSerializer(DeviceSerializer): - config_context = serializers.SerializerMethodField(read_only=True) - - class Meta(DeviceSerializer.Meta): - fields = [ - 'id', 'url', 'display', 'name', 'device_type', 'role', 'device_role', 'tenant', 'platform', 'serial', - 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device', - 'status', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', - 'vc_position', 'vc_priority', 'description', 'comments', 'config_template', 'config_context', - 'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated', 'console_port_count', - 'console_server_port_count', 'power_port_count', 'power_outlet_count', 'interface_count', - 'front_port_count', 'rear_port_count', 'device_bay_count', 'module_bay_count', 'inventory_item_count', - ] - - @extend_schema_field(serializers.JSONField(allow_null=True)) - def get_config_context(self, obj): - return obj.get_config_context() - - -class VirtualDeviceContextSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualdevicecontext-detail') - device = DeviceSerializer(nested=True) - tenant = TenantSerializer(nested=True, required=False, allow_null=True, default=None) - primary_ip = NestedIPAddressSerializer(read_only=True, allow_null=True) - primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True) - primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True) - status = ChoiceField(choices=VirtualDeviceContextStatusChoices) - - # Related object counts - interface_count = RelatedObjectCountField('interfaces') - - class Meta: - model = VirtualDeviceContext - fields = [ - 'id', 'url', 'display', 'name', 'device', 'identifier', 'tenant', 'primary_ip', 'primary_ip4', - 'primary_ip6', 'status', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', - 'interface_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'identifier', 'device', 'description') - - -class ModuleSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail') - device = DeviceSerializer(nested=True) - module_bay = NestedModuleBaySerializer() - module_type = ModuleTypeSerializer(nested=True) - status = ChoiceField(choices=ModuleStatusChoices, required=False) - - class Meta: - model = Module - fields = [ - 'id', 'url', 'display', 'device', 'module_bay', 'module_type', 'status', 'serial', 'asset_tag', - 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'device', 'module_bay', 'module_type', 'description') - - -# -# Device components -# - -class ConnectedEndpointsSerializer(serializers.ModelSerializer): - """ - Legacy serializer for pre-v3.3 connections - """ - connected_endpoints_type = serializers.SerializerMethodField(read_only=True) - connected_endpoints = serializers.SerializerMethodField(read_only=True) - connected_endpoints_reachable = serializers.SerializerMethodField(read_only=True) - - @extend_schema_field(OpenApiTypes.STR) - def get_connected_endpoints_type(self, obj): - if endpoints := obj.connected_endpoints: - return f'{endpoints[0]._meta.app_label}.{endpoints[0]._meta.model_name}' - - @extend_schema_field(serializers.ListField) - def get_connected_endpoints(self, obj): - """ - Return the appropriate serializer for the type of connected object. - """ - if endpoints := obj.connected_endpoints: - serializer = get_serializer_for_model(endpoints[0], prefix=NESTED_SERIALIZER_PREFIX) - context = {'request': self.context['request']} - return serializer(endpoints, many=True, context=context).data - - @extend_schema_field(serializers.BooleanField) - def get_connected_endpoints_reachable(self, obj): - return obj._path and obj._path.is_complete and obj._path.is_active - - -class ConsoleServerPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail') - device = DeviceSerializer(nested=True) - module = ModuleSerializer( - nested=True, - requested_fields=('id', 'url', 'display', 'device', 'module_bay'), - required=False, - allow_null=True - ) - type = ChoiceField( - choices=ConsolePortTypeChoices, - allow_blank=True, - required=False - ) - speed = ChoiceField( - choices=ConsolePortSpeedChoices, - allow_null=True, - required=False - ) - - class Meta: - model = ConsoleServerPort - fields = [ - 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'speed', 'description', - 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'connected_endpoints', - 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created', - 'last_updated', '_occupied', - ] - brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') - - -class ConsolePortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail') - device = DeviceSerializer(nested=True) - module = ModuleSerializer( - nested=True, - requested_fields=('id', 'url', 'display', 'device', 'module_bay'), - required=False, - allow_null=True - ) - type = ChoiceField( - choices=ConsolePortTypeChoices, - allow_blank=True, - required=False - ) - speed = ChoiceField( - choices=ConsolePortSpeedChoices, - allow_null=True, - required=False - ) - - class Meta: - model = ConsolePort - fields = [ - 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'speed', 'description', - 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'connected_endpoints', - 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created', - 'last_updated', '_occupied', - ] - brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') - - -class PowerPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail') - device = DeviceSerializer(nested=True) - module = ModuleSerializer( - nested=True, - requested_fields=('id', 'url', 'display', 'device', 'module_bay'), - required=False, - allow_null=True - ) - type = ChoiceField( - choices=PowerPortTypeChoices, - allow_blank=True, - required=False, - allow_null=True - ) - - class Meta: - model = PowerPort - fields = [ - 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', - 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', - 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', - 'created', 'last_updated', '_occupied', - ] - brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') - - -class PowerOutletSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail') - device = DeviceSerializer(nested=True) - module = ModuleSerializer( - nested=True, - requested_fields=('id', 'url', 'display', 'device', 'module_bay'), - required=False, - allow_null=True - ) - type = ChoiceField( - choices=PowerOutletTypeChoices, - allow_blank=True, - required=False, - allow_null=True - ) - power_port = PowerPortSerializer( - nested=True, - required=False, - allow_null=True - ) - feed_leg = ChoiceField( - choices=PowerOutletFeedLegChoices, - allow_blank=True, - required=False, - allow_null=True - ) - - class Meta: - model = PowerOutlet - fields = [ - 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'power_port', 'feed_leg', - 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', - 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', - 'created', 'last_updated', '_occupied', - ] - brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') - - -class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail') - device = DeviceSerializer(nested=True) - vdcs = SerializedPKRelatedField( - queryset=VirtualDeviceContext.objects.all(), - serializer=NestedVirtualDeviceContextSerializer, - required=False, - many=True - ) - module = ModuleSerializer( - nested=True, - requested_fields=('id', 'url', 'display', 'device', 'module_bay'), - required=False, - allow_null=True - ) - type = ChoiceField(choices=InterfaceTypeChoices) - parent = NestedInterfaceSerializer(required=False, allow_null=True) - bridge = NestedInterfaceSerializer(required=False, allow_null=True) - lag = NestedInterfaceSerializer(required=False, allow_null=True) - mode = ChoiceField(choices=InterfaceModeChoices, required=False, allow_blank=True) - duplex = ChoiceField(choices=InterfaceDuplexChoices, required=False, allow_blank=True, allow_null=True) - rf_role = ChoiceField(choices=WirelessRoleChoices, required=False, allow_blank=True) - rf_channel = ChoiceField(choices=WirelessChannelChoices, required=False, allow_blank=True) - poe_mode = ChoiceField(choices=InterfacePoEModeChoices, required=False, allow_blank=True) - poe_type = ChoiceField(choices=InterfacePoETypeChoices, required=False, allow_blank=True) - untagged_vlan = NestedVLANSerializer(required=False, allow_null=True) - tagged_vlans = SerializedPKRelatedField( - queryset=VLAN.objects.all(), - serializer=NestedVLANSerializer, - required=False, - many=True - ) - vrf = NestedVRFSerializer(required=False, allow_null=True) - l2vpn_termination = NestedL2VPNTerminationSerializer(read_only=True, allow_null=True) - wireless_link = NestedWirelessLinkSerializer(read_only=True, allow_null=True) - wireless_lans = SerializedPKRelatedField( - queryset=WirelessLAN.objects.all(), - serializer=NestedWirelessLANSerializer, - required=False, - many=True - ) - count_ipaddresses = serializers.IntegerField(read_only=True) - count_fhrp_groups = serializers.IntegerField(read_only=True) - mac_address = serializers.CharField( - required=False, - default=None, - allow_blank=True, - allow_null=True - ) - wwn = serializers.CharField(required=False, default=None, allow_blank=True, allow_null=True) - - class Meta: - model = Interface - fields = [ - 'id', 'url', 'display', 'device', 'vdcs', 'module', 'name', 'label', 'type', 'enabled', 'parent', 'bridge', - 'lag', 'mtu', 'mac_address', 'speed', 'duplex', 'wwn', 'mgmt_only', 'description', 'mode', 'rf_role', - 'rf_channel', 'poe_mode', 'poe_type', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', - 'untagged_vlan', 'tagged_vlans', 'mark_connected', 'cable', 'cable_end', 'wireless_link', 'link_peers', - 'link_peers_type', 'wireless_lans', 'vrf', 'l2vpn_termination', 'connected_endpoints', - 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created', - 'last_updated', 'count_ipaddresses', 'count_fhrp_groups', '_occupied', - ] - brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') - - def validate(self, data): - - # Validate many-to-many VLAN assignments - if not self.nested: - device = self.instance.device if self.instance else data.get('device') - for vlan in data.get('tagged_vlans', []): - if vlan.site not in [device.site, None]: - raise serializers.ValidationError({ - 'tagged_vlans': f"VLAN {vlan} must belong to the same site as the interface's parent device, " - f"or it must be global." - }) - - return super().validate(data) - - -class RearPortSerializer(NetBoxModelSerializer, CabledObjectSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail') - device = DeviceSerializer(nested=True) - module = ModuleSerializer( - nested=True, - requested_fields=('id', 'url', 'display', 'device', 'module_bay'), - required=False, - allow_null=True - ) - type = ChoiceField(choices=PortTypeChoices) - - class Meta: - model = RearPort - fields = [ - 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'description', - 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'tags', 'custom_fields', 'created', - 'last_updated', '_occupied', - ] - brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') - - -class FrontPortRearPortSerializer(WritableNestedSerializer): - """ - NestedRearPortSerializer but with parent device omitted (since front and rear ports must belong to same device) - """ - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail') - - class Meta: - model = RearPort - fields = ['id', 'url', 'display', 'name', 'label', 'description'] - - -class FrontPortSerializer(NetBoxModelSerializer, CabledObjectSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail') - device = DeviceSerializer(nested=True) - module = ModuleSerializer( - nested=True, - requested_fields=('id', 'url', 'display', 'device', 'module_bay'), - required=False, - allow_null=True - ) - type = ChoiceField(choices=PortTypeChoices) - rear_port = FrontPortRearPortSerializer() - - class Meta: - model = FrontPort - fields = [ - 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', - 'rear_port_position', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', - 'link_peers_type', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied', - ] - brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') - - -class ModuleBaySerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail') - device = DeviceSerializer(nested=True) - installed_module = ModuleSerializer( - nested=True, - requested_fields=('id', 'url', 'display', 'serial', 'description'), - required=False, - allow_null=True - ) - - class Meta: - model = ModuleBay - fields = [ - 'id', 'url', 'display', 'device', 'name', 'installed_module', 'label', 'position', 'description', 'tags', - 'custom_fields', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'installed_module', 'name', 'description') - - -class DeviceBaySerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail') - device = DeviceSerializer(nested=True) - installed_device = DeviceSerializer(nested=True, required=False, allow_null=True) - - class Meta: - model = DeviceBay - fields = [ - 'id', 'url', 'display', 'device', 'name', 'label', 'description', 'installed_device', 'tags', - 'custom_fields', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'device', 'name', 'description') - - -class InventoryItemSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitem-detail') - device = DeviceSerializer(nested=True) - parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None) - role = InventoryItemRoleSerializer(nested=True, required=False, allow_null=True) - manufacturer = ManufacturerSerializer(nested=True, required=False, allow_null=True, default=None) - component_type = ContentTypeField( - queryset=ContentType.objects.filter(MODULAR_COMPONENT_MODELS), - required=False, - allow_null=True - ) - component = serializers.SerializerMethodField(read_only=True) - _depth = serializers.IntegerField(source='level', read_only=True) - - class Meta: - model = InventoryItem - fields = [ - 'id', 'url', 'display', 'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', - 'asset_tag', 'discovered', 'description', 'component_type', 'component_id', 'component', 'tags', - 'custom_fields', 'created', 'last_updated', '_depth', - ] - brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', '_depth') - - @extend_schema_field(serializers.JSONField(allow_null=True)) - def get_component(self, obj): - if obj.component is None: - return None - serializer = get_serializer_for_model(obj.component, prefix=NESTED_SERIALIZER_PREFIX) - context = {'request': self.context['request']} - return serializer(obj.component, context=context).data - - -# -# Power panels -# - -class PowerPanelSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail') - site = SiteSerializer(nested=True) - location = LocationSerializer( - nested=True, - required=False, - allow_null=True, - default=None - ) - - # Related object counts - powerfeed_count = RelatedObjectCountField('powerfeeds') - - class Meta: - model = PowerPanel - fields = [ - 'id', 'url', 'display', 'site', 'location', 'name', 'description', 'comments', 'tags', 'custom_fields', - 'powerfeed_count', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description', 'powerfeed_count') - - -class PowerFeedSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerfeed-detail') - power_panel = PowerPanelSerializer(nested=True) - rack = RackSerializer( - nested=True, - required=False, - allow_null=True, - default=None - ) - type = ChoiceField( - choices=PowerFeedTypeChoices, - default=lambda: PowerFeedTypeChoices.TYPE_PRIMARY, - ) - status = ChoiceField( - choices=PowerFeedStatusChoices, - default=lambda: PowerFeedStatusChoices.STATUS_ACTIVE, - ) - supply = ChoiceField( - choices=PowerFeedSupplyChoices, - default=lambda: PowerFeedSupplyChoices.SUPPLY_AC, - ) - phase = ChoiceField( - choices=PowerFeedPhaseChoices, - default=lambda: PowerFeedPhaseChoices.PHASE_SINGLE, - ) - tenant = TenantSerializer( - nested=True, - required=False, - allow_null=True - ) - - class Meta: - model = PowerFeed - fields = [ - 'id', 'url', 'display', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage', - 'amperage', 'max_utilization', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', - 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'description', - 'tenant', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description', 'cable', '_occupied') - - -class RackUnitSerializer(serializers.Serializer): - """ - A rack unit is an abstraction formed by the set (rack, position, face); it does not exist as a row in the database. - """ - id = serializers.DecimalField( - max_digits=4, - decimal_places=1, - read_only=True - ) - name = serializers.CharField(read_only=True) - face = ChoiceField(choices=DeviceFaceChoices, read_only=True) - device = DeviceSerializer(nested=True, read_only=True) - occupied = serializers.BooleanField(read_only=True) - display = serializers.SerializerMethodField(read_only=True) - - @extend_schema_field(OpenApiTypes.STR) - def get_display(self, obj): - return obj['name'] diff --git a/netbox/dcim/api/serializers_/__init__.py b/netbox/dcim/api/serializers_/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/dcim/api/serializers_/base.py b/netbox/dcim/api/serializers_/base.py new file mode 100644 index 000000000..3ce44ebcf --- /dev/null +++ b/netbox/dcim/api/serializers_/base.py @@ -0,0 +1,38 @@ +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import extend_schema_field +from rest_framework import serializers + +from netbox.constants import NESTED_SERIALIZER_PREFIX +from utilities.api import get_serializer_for_model + +__all__ = ( + 'ConnectedEndpointsSerializer', +) + + +class ConnectedEndpointsSerializer(serializers.ModelSerializer): + """ + Legacy serializer for pre-v3.3 connections + """ + connected_endpoints_type = serializers.SerializerMethodField(read_only=True) + connected_endpoints = serializers.SerializerMethodField(read_only=True) + connected_endpoints_reachable = serializers.SerializerMethodField(read_only=True) + + @extend_schema_field(OpenApiTypes.STR) + def get_connected_endpoints_type(self, obj): + if endpoints := obj.connected_endpoints: + return f'{endpoints[0]._meta.app_label}.{endpoints[0]._meta.model_name}' + + @extend_schema_field(serializers.ListField) + def get_connected_endpoints(self, obj): + """ + Return the appropriate serializer for the type of connected object. + """ + if endpoints := obj.connected_endpoints: + serializer = get_serializer_for_model(endpoints[0], prefix=NESTED_SERIALIZER_PREFIX) + context = {'request': self.context['request']} + return serializer(endpoints, many=True, context=context).data + + @extend_schema_field(serializers.BooleanField) + def get_connected_endpoints_reachable(self, obj): + return obj._path and obj._path.is_complete and obj._path.is_active diff --git a/netbox/dcim/api/serializers_/cables.py b/netbox/dcim/api/serializers_/cables.py new file mode 100644 index 000000000..099cd8945 --- /dev/null +++ b/netbox/dcim/api/serializers_/cables.py @@ -0,0 +1,127 @@ +from django.contrib.contenttypes.models import ContentType +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import extend_schema_field +from rest_framework import serializers + +from dcim.choices import * +from dcim.constants import * +from dcim.models import Cable, CablePath, CableTermination +from netbox.api.fields import ChoiceField, ContentTypeField +from netbox.api.serializers import GenericObjectSerializer, NetBoxModelSerializer +from netbox.constants import NESTED_SERIALIZER_PREFIX +from tenancy.api.serializers_.tenants import TenantSerializer +from utilities.api import get_serializer_for_model + +__all__ = ( + 'CablePathSerializer', + 'CableSerializer', + 'CableTerminationSerializer', + 'CabledObjectSerializer', + 'TracedCableSerializer', +) + + +class CableSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail') + a_terminations = GenericObjectSerializer(many=True, required=False) + b_terminations = GenericObjectSerializer(many=True, required=False) + status = ChoiceField(choices=LinkStatusChoices, required=False) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) + length_unit = ChoiceField(choices=CableLengthUnitChoices, allow_blank=True, required=False, allow_null=True) + + class Meta: + model = Cable + fields = [ + 'id', 'url', 'display', 'type', 'a_terminations', 'b_terminations', 'status', 'tenant', 'label', 'color', + 'length', 'length_unit', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'label', 'description') + + +class TracedCableSerializer(serializers.ModelSerializer): + """ + Used only while tracing a cable path. + """ + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail') + + class Meta: + model = Cable + fields = [ + 'id', 'url', 'type', 'status', 'label', 'color', 'length', 'length_unit', 'description', + ] + + +class CableTerminationSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cabletermination-detail') + termination_type = ContentTypeField( + queryset=ContentType.objects.filter(CABLE_TERMINATION_MODELS) + ) + termination = serializers.SerializerMethodField(read_only=True) + + class Meta: + model = CableTermination + fields = [ + 'id', 'url', 'display', 'cable', 'cable_end', 'termination_type', 'termination_id', 'termination', + 'created', 'last_updated', + ] + + @extend_schema_field(serializers.JSONField(allow_null=True)) + def get_termination(self, obj): + serializer = get_serializer_for_model(obj.termination, prefix=NESTED_SERIALIZER_PREFIX) + context = {'request': self.context['request']} + return serializer(obj.termination, context=context).data + + +class CablePathSerializer(serializers.ModelSerializer): + path = serializers.SerializerMethodField(read_only=True) + + class Meta: + model = CablePath + fields = ['id', 'path', 'is_active', 'is_complete', 'is_split'] + + @extend_schema_field(serializers.ListField) + def get_path(self, obj): + ret = [] + for nodes in obj.path_objects: + serializer = get_serializer_for_model(nodes[0], prefix=NESTED_SERIALIZER_PREFIX) + context = {'request': self.context['request']} + ret.append(serializer(nodes, context=context, many=True).data) + return ret + + +class CabledObjectSerializer(serializers.ModelSerializer): + cable = CableSerializer(nested=True, read_only=True, allow_null=True) + cable_end = serializers.CharField(read_only=True) + link_peers_type = serializers.SerializerMethodField(read_only=True) + link_peers = serializers.SerializerMethodField(read_only=True) + _occupied = serializers.SerializerMethodField(read_only=True) + + @extend_schema_field(OpenApiTypes.STR) + def get_link_peers_type(self, obj): + """ + Return the type of the peer link terminations, or None. + """ + if not obj.cable: + return None + + if obj.link_peers: + return f'{obj.link_peers[0]._meta.app_label}.{obj.link_peers[0]._meta.model_name}' + + return None + + @extend_schema_field(serializers.ListField) + def get_link_peers(self, obj): + """ + Return the appropriate serializer for the link termination model. + """ + if not obj.link_peers: + return [] + + # Return serialized peer termination objects + serializer = get_serializer_for_model(obj.link_peers[0], prefix=NESTED_SERIALIZER_PREFIX) + context = {'request': self.context['request']} + return serializer(obj.link_peers, context=context, many=True).data + + @extend_schema_field(serializers.BooleanField) + def get__occupied(self, obj): + return obj._occupied diff --git a/netbox/dcim/api/serializers_/device_components.py b/netbox/dcim/api/serializers_/device_components.py new file mode 100644 index 000000000..2891a1154 --- /dev/null +++ b/netbox/dcim/api/serializers_/device_components.py @@ -0,0 +1,366 @@ +from django.contrib.contenttypes.models import ContentType +from drf_spectacular.utils import extend_schema_field +from rest_framework import serializers + +from dcim.choices import * +from dcim.constants import * +from dcim.models import ( + ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, InventoryItem, ModuleBay, PowerOutlet, PowerPort, + RearPort, VirtualDeviceContext, +) +from ipam.api.serializers_.vlans import VLANSerializer +from ipam.api.serializers_.vrfs import VRFSerializer +from ipam.api.nested_serializers import NestedVLANSerializer +from ipam.models import VLAN +from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField +from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer +from netbox.constants import NESTED_SERIALIZER_PREFIX +from utilities.api import get_serializer_for_model +from vpn.api.serializers_.l2vpn import L2VPNTerminationSerializer +from wireless.api.nested_serializers import NestedWirelessLANSerializer, NestedWirelessLinkSerializer +from wireless.choices import * +from wireless.models import WirelessLAN +from .base import ConnectedEndpointsSerializer +from .cables import CabledObjectSerializer +from .devices import DeviceSerializer, ModuleSerializer +from .manufacturers import ManufacturerSerializer +from .roles import InventoryItemRoleSerializer +from ..nested_serializers import * + +__all__ = ( + 'ConsolePortSerializer', + 'ConsoleServerPortSerializer', + 'DeviceBaySerializer', + 'FrontPortSerializer', + 'InterfaceSerializer', + 'InventoryItemSerializer', + 'ModuleBaySerializer', + 'PowerOutletSerializer', + 'PowerPortSerializer', + 'RearPortSerializer', +) + + +class ConsoleServerPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail') + device = DeviceSerializer(nested=True) + module = ModuleSerializer( + nested=True, + requested_fields=('id', 'url', 'display', 'device', 'module_bay'), + required=False, + allow_null=True + ) + type = ChoiceField( + choices=ConsolePortTypeChoices, + allow_blank=True, + required=False + ) + speed = ChoiceField( + choices=ConsolePortSpeedChoices, + allow_null=True, + required=False + ) + + class Meta: + model = ConsoleServerPort + fields = [ + 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'speed', 'description', + 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'connected_endpoints', + 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created', + 'last_updated', '_occupied', + ] + brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') + + +class ConsolePortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail') + device = DeviceSerializer(nested=True) + module = ModuleSerializer( + nested=True, + requested_fields=('id', 'url', 'display', 'device', 'module_bay'), + required=False, + allow_null=True + ) + type = ChoiceField( + choices=ConsolePortTypeChoices, + allow_blank=True, + required=False + ) + speed = ChoiceField( + choices=ConsolePortSpeedChoices, + allow_null=True, + required=False + ) + + class Meta: + model = ConsolePort + fields = [ + 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'speed', 'description', + 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'connected_endpoints', + 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created', + 'last_updated', '_occupied', + ] + brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') + + +class PowerPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail') + device = DeviceSerializer(nested=True) + module = ModuleSerializer( + nested=True, + requested_fields=('id', 'url', 'display', 'device', 'module_bay'), + required=False, + allow_null=True + ) + type = ChoiceField( + choices=PowerPortTypeChoices, + allow_blank=True, + required=False, + allow_null=True + ) + + class Meta: + model = PowerPort + fields = [ + 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', + 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', + 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', + 'created', 'last_updated', '_occupied', + ] + brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') + + +class PowerOutletSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail') + device = DeviceSerializer(nested=True) + module = ModuleSerializer( + nested=True, + requested_fields=('id', 'url', 'display', 'device', 'module_bay'), + required=False, + allow_null=True + ) + type = ChoiceField( + choices=PowerOutletTypeChoices, + allow_blank=True, + required=False, + allow_null=True + ) + power_port = PowerPortSerializer( + nested=True, + required=False, + allow_null=True + ) + feed_leg = ChoiceField( + choices=PowerOutletFeedLegChoices, + allow_blank=True, + required=False, + allow_null=True + ) + + class Meta: + model = PowerOutlet + fields = [ + 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'power_port', 'feed_leg', + 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', + 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', + 'created', 'last_updated', '_occupied', + ] + brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') + + +class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail') + device = DeviceSerializer(nested=True) + vdcs = SerializedPKRelatedField( + queryset=VirtualDeviceContext.objects.all(), + serializer=NestedVirtualDeviceContextSerializer, + required=False, + many=True + ) + module = ModuleSerializer( + nested=True, + requested_fields=('id', 'url', 'display', 'device', 'module_bay'), + required=False, + allow_null=True + ) + type = ChoiceField(choices=InterfaceTypeChoices) + parent = NestedInterfaceSerializer(required=False, allow_null=True) + bridge = NestedInterfaceSerializer(required=False, allow_null=True) + lag = NestedInterfaceSerializer(required=False, allow_null=True) + mode = ChoiceField(choices=InterfaceModeChoices, required=False, allow_blank=True) + duplex = ChoiceField(choices=InterfaceDuplexChoices, required=False, allow_blank=True, allow_null=True) + rf_role = ChoiceField(choices=WirelessRoleChoices, required=False, allow_blank=True) + rf_channel = ChoiceField(choices=WirelessChannelChoices, required=False, allow_blank=True) + poe_mode = ChoiceField(choices=InterfacePoEModeChoices, required=False, allow_blank=True) + poe_type = ChoiceField(choices=InterfacePoETypeChoices, required=False, allow_blank=True) + untagged_vlan = VLANSerializer(nested=True, required=False, allow_null=True) + tagged_vlans = SerializedPKRelatedField( + queryset=VLAN.objects.all(), + serializer=NestedVLANSerializer, + required=False, + many=True + ) + vrf = VRFSerializer(nested=True, required=False, allow_null=True) + l2vpn_termination = L2VPNTerminationSerializer(nested=True, read_only=True, allow_null=True) + wireless_link = NestedWirelessLinkSerializer(read_only=True, allow_null=True) + wireless_lans = SerializedPKRelatedField( + queryset=WirelessLAN.objects.all(), + serializer=NestedWirelessLANSerializer, + required=False, + many=True + ) + count_ipaddresses = serializers.IntegerField(read_only=True) + count_fhrp_groups = serializers.IntegerField(read_only=True) + mac_address = serializers.CharField( + required=False, + default=None, + allow_blank=True, + allow_null=True + ) + wwn = serializers.CharField(required=False, default=None, allow_blank=True, allow_null=True) + + class Meta: + model = Interface + fields = [ + 'id', 'url', 'display', 'device', 'vdcs', 'module', 'name', 'label', 'type', 'enabled', 'parent', 'bridge', + 'lag', 'mtu', 'mac_address', 'speed', 'duplex', 'wwn', 'mgmt_only', 'description', 'mode', 'rf_role', + 'rf_channel', 'poe_mode', 'poe_type', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', + 'untagged_vlan', 'tagged_vlans', 'mark_connected', 'cable', 'cable_end', 'wireless_link', 'link_peers', + 'link_peers_type', 'wireless_lans', 'vrf', 'l2vpn_termination', 'connected_endpoints', + 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created', + 'last_updated', 'count_ipaddresses', 'count_fhrp_groups', '_occupied', + ] + brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') + + def validate(self, data): + + # Validate many-to-many VLAN assignments + if not self.nested: + device = self.instance.device if self.instance else data.get('device') + for vlan in data.get('tagged_vlans', []): + if vlan.site not in [device.site, None]: + raise serializers.ValidationError({ + 'tagged_vlans': f"VLAN {vlan} must belong to the same site as the interface's parent device, " + f"or it must be global." + }) + + return super().validate(data) + + +class RearPortSerializer(NetBoxModelSerializer, CabledObjectSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail') + device = DeviceSerializer(nested=True) + module = ModuleSerializer( + nested=True, + requested_fields=('id', 'url', 'display', 'device', 'module_bay'), + required=False, + allow_null=True + ) + type = ChoiceField(choices=PortTypeChoices) + + class Meta: + model = RearPort + fields = [ + 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'description', + 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'tags', 'custom_fields', 'created', + 'last_updated', '_occupied', + ] + brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') + + +class FrontPortRearPortSerializer(WritableNestedSerializer): + """ + NestedRearPortSerializer but with parent device omitted (since front and rear ports must belong to same device) + """ + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail') + + class Meta: + model = RearPort + fields = ['id', 'url', 'display', 'name', 'label', 'description'] + + +class FrontPortSerializer(NetBoxModelSerializer, CabledObjectSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail') + device = DeviceSerializer(nested=True) + module = ModuleSerializer( + nested=True, + requested_fields=('id', 'url', 'display', 'device', 'module_bay'), + required=False, + allow_null=True + ) + type = ChoiceField(choices=PortTypeChoices) + rear_port = FrontPortRearPortSerializer() + + class Meta: + model = FrontPort + fields = [ + 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', + 'rear_port_position', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', + 'link_peers_type', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied', + ] + brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') + + +class ModuleBaySerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail') + device = DeviceSerializer(nested=True) + installed_module = ModuleSerializer( + nested=True, + requested_fields=('id', 'url', 'display', 'serial', 'description'), + required=False, + allow_null=True + ) + + class Meta: + model = ModuleBay + fields = [ + 'id', 'url', 'display', 'device', 'name', 'installed_module', 'label', 'position', 'description', 'tags', + 'custom_fields', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'installed_module', 'name', 'description') + + +class DeviceBaySerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail') + device = DeviceSerializer(nested=True) + installed_device = DeviceSerializer(nested=True, required=False, allow_null=True) + + class Meta: + model = DeviceBay + fields = [ + 'id', 'url', 'display', 'device', 'name', 'label', 'description', 'installed_device', 'tags', + 'custom_fields', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'device', 'name', 'description') + + +class InventoryItemSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitem-detail') + device = DeviceSerializer(nested=True) + parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None) + role = InventoryItemRoleSerializer(nested=True, required=False, allow_null=True) + manufacturer = ManufacturerSerializer(nested=True, required=False, allow_null=True, default=None) + component_type = ContentTypeField( + queryset=ContentType.objects.filter(MODULAR_COMPONENT_MODELS), + required=False, + allow_null=True + ) + component = serializers.SerializerMethodField(read_only=True) + _depth = serializers.IntegerField(source='level', read_only=True) + + class Meta: + model = InventoryItem + fields = [ + 'id', 'url', 'display', 'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', + 'asset_tag', 'discovered', 'description', 'component_type', 'component_id', 'component', 'tags', + 'custom_fields', 'created', 'last_updated', '_depth', + ] + brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', '_depth') + + @extend_schema_field(serializers.JSONField(allow_null=True)) + def get_component(self, obj): + if obj.component is None: + return None + serializer = get_serializer_for_model(obj.component, prefix=NESTED_SERIALIZER_PREFIX) + context = {'request': self.context['request']} + return serializer(obj.component, context=context).data diff --git a/netbox/dcim/api/serializers_/devices.py b/netbox/dcim/api/serializers_/devices.py new file mode 100644 index 000000000..4950ab908 --- /dev/null +++ b/netbox/dcim/api/serializers_/devices.py @@ -0,0 +1,165 @@ +import decimal + +from django.utils.translation import gettext as _ +from drf_spectacular.utils import extend_schema_field +from rest_framework import serializers + +from dcim.choices import * +from dcim.models import Device, DeviceBay, Module, VirtualDeviceContext +from extras.api.serializers_.provisioning import ConfigTemplateSerializer +from ipam.api.serializers_.ip import IPAddressSerializer +from netbox.api.fields import ChoiceField, RelatedObjectCountField +from netbox.api.serializers import NetBoxModelSerializer +from tenancy.api.serializers_.tenants import TenantSerializer +from virtualization.api.serializers_.clusters import ClusterSerializer +from .devicetypes import * +from .platforms import PlatformSerializer +from .racks import RackSerializer +from .roles import DeviceRoleSerializer +from .sites import LocationSerializer, SiteSerializer +from .virtualchassis import VirtualChassisSerializer +from ..nested_serializers import * + +__all__ = ( + 'DeviceSerializer', + 'DeviceWithConfigContextSerializer', + 'ModuleSerializer', + 'VirtualDeviceContextSerializer', +) + + +class DeviceSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail') + device_type = DeviceTypeSerializer(nested=True) + role = DeviceRoleSerializer(nested=True) + device_role = DeviceRoleSerializer( + nested=True, + read_only=True, + help_text='Deprecated in v3.6 in favor of `role`.' + ) + tenant = TenantSerializer( + nested=True, + required=False, + allow_null=True, + default=None + ) + platform = PlatformSerializer(nested=True, required=False, allow_null=True) + site = SiteSerializer(nested=True) + location = LocationSerializer(nested=True, required=False, allow_null=True, default=None) + rack = RackSerializer(nested=True, required=False, allow_null=True, default=None) + face = ChoiceField(choices=DeviceFaceChoices, allow_blank=True, default=lambda: '') + position = serializers.DecimalField( + max_digits=4, + decimal_places=1, + allow_null=True, + label=_('Position (U)'), + min_value=decimal.Decimal(0.5), + default=None + ) + status = ChoiceField(choices=DeviceStatusChoices, required=False) + airflow = ChoiceField(choices=DeviceAirflowChoices, allow_blank=True, required=False) + primary_ip = IPAddressSerializer(nested=True, read_only=True) + primary_ip4 = IPAddressSerializer(nested=True, required=False, allow_null=True) + primary_ip6 = IPAddressSerializer(nested=True, required=False, allow_null=True) + oob_ip = IPAddressSerializer(nested=True, required=False, allow_null=True) + parent_device = serializers.SerializerMethodField() + cluster = ClusterSerializer(nested=True, required=False, allow_null=True) + virtual_chassis = VirtualChassisSerializer(nested=True, required=False, allow_null=True, default=None) + vc_position = serializers.IntegerField(allow_null=True, max_value=255, min_value=0, default=None) + config_template = ConfigTemplateSerializer(nested=True, required=False, allow_null=True, default=None) + + # Counter fields + console_port_count = serializers.IntegerField(read_only=True) + console_server_port_count = serializers.IntegerField(read_only=True) + power_port_count = serializers.IntegerField(read_only=True) + power_outlet_count = serializers.IntegerField(read_only=True) + interface_count = serializers.IntegerField(read_only=True) + front_port_count = serializers.IntegerField(read_only=True) + rear_port_count = serializers.IntegerField(read_only=True) + device_bay_count = serializers.IntegerField(read_only=True) + module_bay_count = serializers.IntegerField(read_only=True) + inventory_item_count = serializers.IntegerField(read_only=True) + + class Meta: + model = Device + fields = [ + 'id', 'url', 'display', 'name', 'device_type', 'role', 'device_role', 'tenant', 'platform', 'serial', + 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device', + 'status', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', + 'vc_position', 'vc_priority', 'description', 'comments', 'config_template', 'local_context_data', 'tags', + 'custom_fields', 'created', 'last_updated', 'console_port_count', 'console_server_port_count', + 'power_port_count', 'power_outlet_count', 'interface_count', 'front_port_count', 'rear_port_count', + 'device_bay_count', 'module_bay_count', 'inventory_item_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') + + @extend_schema_field(NestedDeviceSerializer) + def get_parent_device(self, obj): + try: + device_bay = obj.parent_bay + except DeviceBay.DoesNotExist: + return None + context = {'request': self.context['request']} + data = NestedDeviceSerializer(instance=device_bay.device, context=context).data + data['device_bay'] = NestedDeviceBaySerializer(instance=device_bay, context=context).data + return data + + def get_device_role(self, obj): + return obj.role + + +class DeviceWithConfigContextSerializer(DeviceSerializer): + config_context = serializers.SerializerMethodField(read_only=True) + + class Meta(DeviceSerializer.Meta): + fields = [ + 'id', 'url', 'display', 'name', 'device_type', 'role', 'device_role', 'tenant', 'platform', 'serial', + 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device', + 'status', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', + 'vc_position', 'vc_priority', 'description', 'comments', 'config_template', 'config_context', + 'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated', 'console_port_count', + 'console_server_port_count', 'power_port_count', 'power_outlet_count', 'interface_count', + 'front_port_count', 'rear_port_count', 'device_bay_count', 'module_bay_count', 'inventory_item_count', + ] + + @extend_schema_field(serializers.JSONField(allow_null=True)) + def get_config_context(self, obj): + return obj.get_config_context() + + +class VirtualDeviceContextSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualdevicecontext-detail') + device = DeviceSerializer(nested=True) + tenant = TenantSerializer(nested=True, required=False, allow_null=True, default=None) + primary_ip = IPAddressSerializer(nested=True, read_only=True, allow_null=True) + primary_ip4 = IPAddressSerializer(nested=True, required=False, allow_null=True) + primary_ip6 = IPAddressSerializer(nested=True, required=False, allow_null=True) + status = ChoiceField(choices=VirtualDeviceContextStatusChoices) + + # Related object counts + interface_count = RelatedObjectCountField('interfaces') + + class Meta: + model = VirtualDeviceContext + fields = [ + 'id', 'url', 'display', 'name', 'device', 'identifier', 'tenant', 'primary_ip', 'primary_ip4', + 'primary_ip6', 'status', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', + 'interface_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'identifier', 'device', 'description') + + +class ModuleSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail') + device = DeviceSerializer(nested=True) + module_bay = NestedModuleBaySerializer() + module_type = ModuleTypeSerializer(nested=True) + status = ChoiceField(choices=ModuleStatusChoices, required=False) + + class Meta: + model = Module + fields = [ + 'id', 'url', 'display', 'device', 'module_bay', 'module_type', 'status', 'serial', 'asset_tag', + 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'device', 'module_bay', 'module_type', 'description') diff --git a/netbox/dcim/api/serializers_/devicetype_components.py b/netbox/dcim/api/serializers_/devicetype_components.py new file mode 100644 index 000000000..958b51ece --- /dev/null +++ b/netbox/dcim/api/serializers_/devicetype_components.py @@ -0,0 +1,328 @@ +from django.contrib.contenttypes.models import ContentType +from drf_spectacular.utils import extend_schema_field +from rest_framework import serializers + +from dcim.choices import * +from dcim.constants import * +from dcim.models import ( + ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, FrontPortTemplate, InterfaceTemplate, + InventoryItemTemplate, ModuleBayTemplate, PowerOutletTemplate, PowerPortTemplate, RearPortTemplate, +) +from netbox.api.fields import ChoiceField, ContentTypeField +from netbox.api.serializers import ValidatedModelSerializer +from netbox.constants import NESTED_SERIALIZER_PREFIX +from utilities.api import get_serializer_for_model +from wireless.choices import * +from .devicetypes import DeviceTypeSerializer, ModuleTypeSerializer +from .manufacturers import ManufacturerSerializer +from .roles import InventoryItemRoleSerializer +from ..nested_serializers import * + +__all__ = ( + 'ConsolePortTemplateSerializer', + 'ConsoleServerPortTemplateSerializer', + 'DeviceBayTemplateSerializer', + 'FrontPortTemplateSerializer', + 'InterfaceTemplateSerializer', + 'InventoryItemTemplateSerializer', + 'ModuleBayTemplateSerializer', + 'PowerOutletTemplateSerializer', + 'PowerPortTemplateSerializer', + 'RearPortTemplateSerializer', +) + + +class ConsolePortTemplateSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleporttemplate-detail') + device_type = DeviceTypeSerializer( + nested=True, + required=False, + allow_null=True, + default=None + ) + module_type = ModuleTypeSerializer( + nested=True, + required=False, + allow_null=True, + default=None + ) + type = ChoiceField( + choices=ConsolePortTypeChoices, + allow_blank=True, + required=False + ) + + class Meta: + model = ConsolePortTemplate + fields = [ + 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'description', 'created', + 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') + + +class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverporttemplate-detail') + device_type = DeviceTypeSerializer( + nested=True, + required=False, + allow_null=True, + default=None + ) + module_type = ModuleTypeSerializer( + nested=True, + required=False, + allow_null=True, + default=None + ) + type = ChoiceField( + choices=ConsolePortTypeChoices, + allow_blank=True, + required=False + ) + + class Meta: + model = ConsoleServerPortTemplate + fields = [ + 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'description', 'created', + 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') + + +class PowerPortTemplateSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerporttemplate-detail') + device_type = DeviceTypeSerializer( + nested=True, + required=False, + allow_null=True, + default=None + ) + module_type = ModuleTypeSerializer( + nested=True, + required=False, + allow_null=True, + default=None + ) + type = ChoiceField( + choices=PowerPortTypeChoices, + allow_blank=True, + required=False, + allow_null=True + ) + + class Meta: + model = PowerPortTemplate + fields = [ + 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw', + 'allocated_draw', 'description', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') + + +class PowerOutletTemplateSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlettemplate-detail') + device_type = DeviceTypeSerializer( + nested=True, + required=False, + allow_null=True, + default=None + ) + module_type = ModuleTypeSerializer( + nested=True, + required=False, + allow_null=True, + default=None + ) + type = ChoiceField( + choices=PowerOutletTypeChoices, + allow_blank=True, + required=False, + allow_null=True + ) + power_port = PowerPortTemplateSerializer( + nested=True, + required=False, + allow_null=True + ) + feed_leg = ChoiceField( + choices=PowerOutletFeedLegChoices, + allow_blank=True, + required=False, + allow_null=True + ) + + class Meta: + model = PowerOutletTemplate + fields = [ + 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'power_port', 'feed_leg', + 'description', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') + + +class InterfaceTemplateSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interfacetemplate-detail') + device_type = DeviceTypeSerializer( + nested=True, + required=False, + allow_null=True, + default=None + ) + module_type = ModuleTypeSerializer( + nested=True, + required=False, + allow_null=True, + default=None + ) + type = ChoiceField(choices=InterfaceTypeChoices) + bridge = NestedInterfaceTemplateSerializer( + required=False, + allow_null=True + ) + poe_mode = ChoiceField( + choices=InterfacePoEModeChoices, + required=False, + allow_blank=True, + allow_null=True + ) + poe_type = ChoiceField( + choices=InterfacePoETypeChoices, + required=False, + allow_blank=True, + allow_null=True + ) + rf_role = ChoiceField( + choices=WirelessRoleChoices, + required=False, + allow_blank=True, + allow_null=True + ) + + class Meta: + model = InterfaceTemplate + fields = [ + 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'enabled', 'mgmt_only', + 'description', 'bridge', 'poe_mode', 'poe_type', 'rf_role', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') + + +class RearPortTemplateSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearporttemplate-detail') + device_type = DeviceTypeSerializer( + required=False, + nested=True, + allow_null=True, + default=None + ) + module_type = ModuleTypeSerializer( + nested=True, + required=False, + allow_null=True, + default=None + ) + type = ChoiceField(choices=PortTypeChoices) + + class Meta: + model = RearPortTemplate + fields = [ + 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', + 'description', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') + + +class FrontPortTemplateSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontporttemplate-detail') + device_type = DeviceTypeSerializer( + nested=True, + required=False, + allow_null=True, + default=None + ) + module_type = ModuleTypeSerializer( + nested=True, + required=False, + allow_null=True, + default=None + ) + type = ChoiceField(choices=PortTypeChoices) + rear_port = RearPortTemplateSerializer(nested=True) + + class Meta: + model = FrontPortTemplate + fields = [ + 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port', + 'rear_port_position', 'description', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') + + +class ModuleBayTemplateSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebaytemplate-detail') + device_type = DeviceTypeSerializer( + nested=True + ) + + class Meta: + model = ModuleBayTemplate + fields = [ + 'id', 'url', 'display', 'device_type', 'name', 'label', 'position', 'description', 'created', + 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') + + +class DeviceBayTemplateSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebaytemplate-detail') + device_type = DeviceTypeSerializer( + nested=True + ) + + class Meta: + model = DeviceBayTemplate + fields = ['id', 'url', 'display', 'device_type', 'name', 'label', 'description', 'created', 'last_updated'] + brief_fields = ('id', 'url', 'display', 'name', 'description') + + +class InventoryItemTemplateSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemtemplate-detail') + device_type = DeviceTypeSerializer( + nested=True + ) + parent = serializers.PrimaryKeyRelatedField( + queryset=InventoryItemTemplate.objects.all(), + allow_null=True, + default=None + ) + role = InventoryItemRoleSerializer(nested=True, required=False, allow_null=True) + manufacturer = ManufacturerSerializer( + nested=True, + required=False, + allow_null=True, + default=None + ) + component_type = ContentTypeField( + queryset=ContentType.objects.filter(MODULAR_COMPONENT_TEMPLATE_MODELS), + required=False, + allow_null=True + ) + component = serializers.SerializerMethodField(read_only=True) + _depth = serializers.IntegerField(source='level', read_only=True) + + class Meta: + model = InventoryItemTemplate + fields = [ + 'id', 'url', 'display', 'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', + 'description', 'component_type', 'component_id', 'component', 'created', 'last_updated', '_depth', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description', '_depth') + + @extend_schema_field(serializers.JSONField(allow_null=True)) + def get_component(self, obj): + if obj.component is None: + return None + serializer = get_serializer_for_model(obj.component, prefix=NESTED_SERIALIZER_PREFIX) + context = {'request': self.context['request']} + return serializer(obj.component, context=context).data diff --git a/netbox/dcim/api/serializers_/devicetypes.py b/netbox/dcim/api/serializers_/devicetypes.py new file mode 100644 index 000000000..2384f7b02 --- /dev/null +++ b/netbox/dcim/api/serializers_/devicetypes.py @@ -0,0 +1,74 @@ +from django.utils.translation import gettext as _ +from rest_framework import serializers + +from dcim.choices import * +from dcim.models import DeviceType, ModuleType +from netbox.api.fields import ChoiceField, RelatedObjectCountField +from netbox.api.serializers import NetBoxModelSerializer +from .manufacturers import ManufacturerSerializer +from .platforms import PlatformSerializer + +__all__ = ( + 'DeviceTypeSerializer', + 'ModuleTypeSerializer', +) + + +class DeviceTypeSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail') + manufacturer = ManufacturerSerializer(nested=True) + default_platform = PlatformSerializer(nested=True, required=False, allow_null=True) + u_height = serializers.DecimalField( + max_digits=4, + decimal_places=1, + label=_('Position (U)'), + min_value=0, + default=1.0 + ) + 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) + front_image = serializers.URLField(allow_null=True, required=False) + rear_image = serializers.URLField(allow_null=True, required=False) + + # Counter fields + console_port_template_count = serializers.IntegerField(read_only=True) + console_server_port_template_count = serializers.IntegerField(read_only=True) + power_port_template_count = serializers.IntegerField(read_only=True) + power_outlet_template_count = serializers.IntegerField(read_only=True) + interface_template_count = serializers.IntegerField(read_only=True) + front_port_template_count = serializers.IntegerField(read_only=True) + rear_port_template_count = serializers.IntegerField(read_only=True) + device_bay_template_count = serializers.IntegerField(read_only=True) + module_bay_template_count = serializers.IntegerField(read_only=True) + inventory_item_template_count = serializers.IntegerField(read_only=True) + + # Related object counts + device_count = RelatedObjectCountField('instances') + + class Meta: + model = DeviceType + fields = [ + 'id', 'url', 'display', 'manufacturer', 'default_platform', 'model', 'slug', 'part_number', 'u_height', + 'exclude_from_utilization', 'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit', + 'front_image', 'rear_image', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', + 'device_count', 'console_port_template_count', 'console_server_port_template_count', + 'power_port_template_count', 'power_outlet_template_count', 'interface_template_count', + 'front_port_template_count', 'rear_port_template_count', 'device_bay_template_count', + 'module_bay_template_count', 'inventory_item_template_count', + ] + brief_fields = ('id', 'url', 'display', 'manufacturer', 'model', 'slug', 'description', 'device_count') + + +class ModuleTypeSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:moduletype-detail') + manufacturer = ManufacturerSerializer(nested=True) + weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True) + + class Meta: + model = ModuleType + fields = [ + 'id', 'url', 'display', 'manufacturer', 'model', 'part_number', 'weight', 'weight_unit', 'description', + 'comments', 'tags', 'custom_fields', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'manufacturer', 'model', 'description') diff --git a/netbox/dcim/api/serializers_/manufacturers.py b/netbox/dcim/api/serializers_/manufacturers.py new file mode 100644 index 000000000..fd50fe97d --- /dev/null +++ b/netbox/dcim/api/serializers_/manufacturers.py @@ -0,0 +1,26 @@ +from rest_framework import serializers + +from dcim.models import Manufacturer +from netbox.api.fields import RelatedObjectCountField +from netbox.api.serializers import NetBoxModelSerializer + +__all__ = ( + 'ManufacturerSerializer', +) + + +class ManufacturerSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail') + + # Related object counts + devicetype_count = RelatedObjectCountField('device_types') + inventoryitem_count = RelatedObjectCountField('inventory_items') + platform_count = RelatedObjectCountField('platforms') + + class Meta: + model = Manufacturer + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated', + 'devicetype_count', 'inventoryitem_count', 'platform_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'devicetype_count') diff --git a/netbox/dcim/api/serializers_/platforms.py b/netbox/dcim/api/serializers_/platforms.py new file mode 100644 index 000000000..03fa9c8f0 --- /dev/null +++ b/netbox/dcim/api/serializers_/platforms.py @@ -0,0 +1,29 @@ +from rest_framework import serializers + +from dcim.models import Platform +from extras.api.serializers_.provisioning import ConfigTemplateSerializer +from netbox.api.fields import RelatedObjectCountField +from netbox.api.serializers import NetBoxModelSerializer +from .manufacturers import ManufacturerSerializer + +__all__ = ( + 'PlatformSerializer', +) + + +class PlatformSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail') + manufacturer = ManufacturerSerializer(nested=True, required=False, allow_null=True) + config_template = ConfigTemplateSerializer(nested=True, required=False, allow_null=True, default=None) + + # Related object counts + device_count = RelatedObjectCountField('devices') + virtualmachine_count = RelatedObjectCountField('virtual_machines') + + class Meta: + model = Platform + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'manufacturer', 'config_template', 'description', 'tags', + 'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'device_count', 'virtualmachine_count') diff --git a/netbox/dcim/api/serializers_/power.py b/netbox/dcim/api/serializers_/power.py new file mode 100644 index 000000000..dddd54906 --- /dev/null +++ b/netbox/dcim/api/serializers_/power.py @@ -0,0 +1,80 @@ +from rest_framework import serializers + +from dcim.choices import * +from dcim.models import PowerFeed, PowerPanel +from netbox.api.fields import ChoiceField, RelatedObjectCountField +from netbox.api.serializers import NetBoxModelSerializer +from tenancy.api.serializers_.tenants import TenantSerializer +from .base import ConnectedEndpointsSerializer +from .cables import CabledObjectSerializer +from .racks import RackSerializer +from .sites import LocationSerializer, SiteSerializer + +__all__ = ( + 'PowerFeedSerializer', + 'PowerPanelSerializer', +) + + +class PowerPanelSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail') + site = SiteSerializer(nested=True) + location = LocationSerializer( + nested=True, + required=False, + allow_null=True, + default=None + ) + + # Related object counts + powerfeed_count = RelatedObjectCountField('powerfeeds') + + class Meta: + model = PowerPanel + fields = [ + 'id', 'url', 'display', 'site', 'location', 'name', 'description', 'comments', 'tags', 'custom_fields', + 'powerfeed_count', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description', 'powerfeed_count') + + +class PowerFeedSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerfeed-detail') + power_panel = PowerPanelSerializer(nested=True) + rack = RackSerializer( + nested=True, + required=False, + allow_null=True, + default=None + ) + type = ChoiceField( + choices=PowerFeedTypeChoices, + default=lambda: PowerFeedTypeChoices.TYPE_PRIMARY, + ) + status = ChoiceField( + choices=PowerFeedStatusChoices, + default=lambda: PowerFeedStatusChoices.STATUS_ACTIVE, + ) + supply = ChoiceField( + choices=PowerFeedSupplyChoices, + default=lambda: PowerFeedSupplyChoices.SUPPLY_AC, + ) + phase = ChoiceField( + choices=PowerFeedPhaseChoices, + default=lambda: PowerFeedPhaseChoices.PHASE_SINGLE, + ) + tenant = TenantSerializer( + nested=True, + required=False, + allow_null=True + ) + + class Meta: + model = PowerFeed + fields = [ + 'id', 'url', 'display', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage', + 'amperage', 'max_utilization', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', + 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'description', + 'tenant', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description', 'cable', '_occupied') diff --git a/netbox/dcim/api/serializers_/racks.py b/netbox/dcim/api/serializers_/racks.py new file mode 100644 index 000000000..a6754cba0 --- /dev/null +++ b/netbox/dcim/api/serializers_/racks.py @@ -0,0 +1,117 @@ +from django.utils.translation import gettext as _ +from rest_framework import serializers + +from dcim.choices import * +from dcim.constants import * +from dcim.models import Rack, RackReservation, RackRole +from netbox.api.fields import ChoiceField, RelatedObjectCountField +from netbox.api.serializers import NetBoxModelSerializer +from netbox.config import ConfigItem +from tenancy.api.serializers_.tenants import TenantSerializer +from users.api.serializers_.users import UserSerializer +from .sites import LocationSerializer, SiteSerializer + +__all__ = ( + 'RackElevationDetailFilterSerializer', + 'RackReservationSerializer', + 'RackRoleSerializer', + 'RackSerializer', +) + + +class RackRoleSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail') + + # Related object counts + rack_count = RelatedObjectCountField('racks') + + class Meta: + model = RackRole + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created', + 'last_updated', 'rack_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'rack_count') + + +class RackSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail') + site = SiteSerializer(nested=True) + location = LocationSerializer(nested=True, required=False, allow_null=True, default=None) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) + status = ChoiceField(choices=RackStatusChoices, required=False) + role = RackRoleSerializer(nested=True, required=False, allow_null=True) + type = ChoiceField(choices=RackTypeChoices, allow_blank=True, required=False, allow_null=True) + facility_id = serializers.CharField(max_length=50, allow_blank=True, allow_null=True, label=_('Facility ID'), + default=None) + 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) + + # Related object counts + device_count = RelatedObjectCountField('devices') + powerfeed_count = RelatedObjectCountField('powerfeeds') + + class Meta: + model = Rack + fields = [ + 'id', 'url', 'display', 'name', 'facility_id', 'site', 'location', 'tenant', 'status', 'role', 'serial', + 'asset_tag', 'type', 'width', 'u_height', 'starting_unit', 'weight', 'max_weight', 'weight_unit', + 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'description', 'comments', + 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'powerfeed_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description', 'device_count') + + +class RackReservationSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackreservation-detail') + rack = RackSerializer(nested=True) + user = UserSerializer(nested=True) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) + + class Meta: + model = RackReservation + fields = [ + 'id', 'url', 'display', 'rack', 'units', 'created', 'last_updated', 'user', 'tenant', 'description', + 'comments', 'tags', 'custom_fields', + ] + brief_fields = ('id', 'url', 'display', 'user', 'description', 'units') + + +class RackElevationDetailFilterSerializer(serializers.Serializer): + q = serializers.CharField( + required=False, + default=None + ) + face = serializers.ChoiceField( + choices=DeviceFaceChoices, + default=DeviceFaceChoices.FACE_FRONT + ) + render = serializers.ChoiceField( + choices=RackElevationDetailRenderChoices, + default=RackElevationDetailRenderChoices.RENDER_JSON + ) + unit_width = serializers.IntegerField( + default=ConfigItem('RACK_ELEVATION_DEFAULT_UNIT_WIDTH') + ) + unit_height = serializers.IntegerField( + default=ConfigItem('RACK_ELEVATION_DEFAULT_UNIT_HEIGHT') + ) + legend_width = serializers.IntegerField( + default=RACK_ELEVATION_DEFAULT_LEGEND_WIDTH + ) + margin_width = serializers.IntegerField( + default=RACK_ELEVATION_DEFAULT_MARGIN_WIDTH + ) + exclude = serializers.IntegerField( + required=False, + default=None + ) + expand_devices = serializers.BooleanField( + required=False, + default=True + ) + include_images = serializers.BooleanField( + required=False, + default=True + ) diff --git a/netbox/dcim/api/serializers_/rackunits.py b/netbox/dcim/api/serializers_/rackunits.py new file mode 100644 index 000000000..1f5306718 --- /dev/null +++ b/netbox/dcim/api/serializers_/rackunits.py @@ -0,0 +1,31 @@ +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import extend_schema_field +from rest_framework import serializers + +from dcim.choices import * +from netbox.api.fields import ChoiceField +from .devices import DeviceSerializer + +__all__ = ( + 'RackUnitSerializer', +) + + +class RackUnitSerializer(serializers.Serializer): + """ + A rack unit is an abstraction formed by the set (rack, position, face); it does not exist as a row in the database. + """ + id = serializers.DecimalField( + max_digits=4, + decimal_places=1, + read_only=True + ) + name = serializers.CharField(read_only=True) + face = ChoiceField(choices=DeviceFaceChoices, read_only=True) + device = DeviceSerializer(nested=True, read_only=True) + occupied = serializers.BooleanField(read_only=True) + display = serializers.SerializerMethodField(read_only=True) + + @extend_schema_field(OpenApiTypes.STR) + def get_display(self, obj): + return obj['name'] diff --git a/netbox/dcim/api/serializers_/roles.py b/netbox/dcim/api/serializers_/roles.py new file mode 100644 index 000000000..300cc4895 --- /dev/null +++ b/netbox/dcim/api/serializers_/roles.py @@ -0,0 +1,43 @@ +from rest_framework import serializers + +from dcim.models import DeviceRole, InventoryItemRole +from extras.api.serializers_.provisioning import ConfigTemplateSerializer +from netbox.api.fields import RelatedObjectCountField +from netbox.api.serializers import NetBoxModelSerializer + +__all__ = ( + 'DeviceRoleSerializer', + 'InventoryItemRoleSerializer', +) + + +class DeviceRoleSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail') + config_template = ConfigTemplateSerializer(nested=True, required=False, allow_null=True, default=None) + + # Related object counts + device_count = RelatedObjectCountField('devices') + virtualmachine_count = RelatedObjectCountField('virtual_machines') + + class Meta: + model = DeviceRole + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'color', 'vm_role', 'config_template', 'description', 'tags', + 'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'device_count', 'virtualmachine_count') + + +class InventoryItemRoleSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemrole-detail') + + # Related object counts + inventoryitem_count = RelatedObjectCountField('inventory_items') + + class Meta: + model = InventoryItemRole + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created', + 'last_updated', 'inventoryitem_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'inventoryitem_count') diff --git a/netbox/dcim/api/serializers_/sites.py b/netbox/dcim/api/serializers_/sites.py new file mode 100644 index 000000000..f1f83227d --- /dev/null +++ b/netbox/dcim/api/serializers_/sites.py @@ -0,0 +1,97 @@ +from rest_framework import serializers +from timezone_field.rest_framework import TimeZoneSerializerField + +from dcim.choices import * +from dcim.models import Location, Region, Site, SiteGroup +from ipam.api.nested_serializers import NestedASNSerializer +from ipam.models import ASN +from netbox.api.fields import ChoiceField, RelatedObjectCountField, SerializedPKRelatedField +from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer +from tenancy.api.serializers_.tenants import TenantSerializer +from ..nested_serializers import * + +__all__ = ( + 'LocationSerializer', + 'RegionSerializer', + 'SiteGroupSerializer', + 'SiteSerializer', +) + + +class RegionSerializer(NestedGroupModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail') + parent = NestedRegionSerializer(required=False, allow_null=True, default=None) + site_count = serializers.IntegerField(read_only=True) + + class Meta: + model = Region + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created', + 'last_updated', 'site_count', '_depth', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'site_count', '_depth') + + +class SiteGroupSerializer(NestedGroupModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:sitegroup-detail') + parent = NestedSiteGroupSerializer(required=False, allow_null=True, default=None) + site_count = serializers.IntegerField(read_only=True) + + class Meta: + model = SiteGroup + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created', + 'last_updated', 'site_count', '_depth', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'site_count', '_depth') + + +class SiteSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:site-detail') + status = ChoiceField(choices=SiteStatusChoices, required=False) + region = RegionSerializer(nested=True, required=False, allow_null=True) + group = SiteGroupSerializer(nested=True, required=False, allow_null=True) + tenant = TenantSerializer(required=False, allow_null=True) + time_zone = TimeZoneSerializerField(required=False, allow_null=True) + asns = SerializedPKRelatedField( + queryset=ASN.objects.all(), + serializer=NestedASNSerializer, + required=False, + many=True + ) + + # Related object counts + 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 + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'time_zone', + 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments', 'asns', 'tags', + 'custom_fields', 'created', 'last_updated', 'circuit_count', 'device_count', 'prefix_count', 'rack_count', + 'virtualmachine_count', 'vlan_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description', 'slug') + + +class LocationSerializer(NestedGroupModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:location-detail') + site = SiteSerializer(nested=True) + parent = NestedLocationSerializer(required=False, allow_null=True) + status = ChoiceField(choices=LocationStatusChoices, required=False) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) + rack_count = serializers.IntegerField(read_only=True) + device_count = serializers.IntegerField(read_only=True) + + class Meta: + model = Location + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'site', 'parent', 'status', 'tenant', 'description', 'tags', + 'custom_fields', 'created', 'last_updated', 'rack_count', 'device_count', '_depth', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'rack_count', '_depth') diff --git a/netbox/dcim/api/serializers_/virtualchassis.py b/netbox/dcim/api/serializers_/virtualchassis.py new file mode 100644 index 000000000..570abfc7d --- /dev/null +++ b/netbox/dcim/api/serializers_/virtualchassis.py @@ -0,0 +1,25 @@ +from rest_framework import serializers + +from dcim.models import VirtualChassis +from netbox.api.serializers import NetBoxModelSerializer +from ..nested_serializers import * + +__all__ = ( + 'VirtualChassisSerializer', +) + + +class VirtualChassisSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail') + master = NestedDeviceSerializer(required=False, allow_null=True, default=None) + + # Counter fields + member_count = serializers.IntegerField(read_only=True) + + class Meta: + model = VirtualChassis + fields = [ + 'id', 'url', 'display', 'name', 'domain', 'master', 'description', 'comments', 'tags', 'custom_fields', + 'created', 'last_updated', 'member_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'master', 'description', 'member_count') diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index 4c8c15159..83fe945c2 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -1,666 +1,15 @@ -from django.contrib.auth import get_user_model -from django.core.exceptions import ObjectDoesNotExist -from django.utils.translation import gettext as _ -from drf_spectacular.types import OpenApiTypes -from drf_spectacular.utils import extend_schema_field -from rest_framework import serializers - -from core.api.serializers import DataFileSerializer, DataSourceSerializer, JobSerializer -from core.models import ContentType -from dcim.api.nested_serializers import ( - NestedDeviceRoleSerializer, NestedDeviceTypeSerializer, NestedLocationSerializer, NestedPlatformSerializer, - NestedRegionSerializer, NestedSiteSerializer, NestedSiteGroupSerializer, -) -from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup -from extras.choices import * -from extras.models import * -from netbox.api.exceptions import SerializerNotFound -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 -from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer -from tenancy.models import Tenant, TenantGroup -from users.api.serializers import UserSerializer -from utilities.api import get_serializer_for_model -from virtualization.api.nested_serializers import ( - NestedClusterGroupSerializer, NestedClusterSerializer, NestedClusterTypeSerializer, -) -from virtualization.models import Cluster, ClusterGroup, ClusterType +from .serializers_.attachments import * +from .serializers_.bookmarks import * +from .serializers_.change_logging import * +from .serializers_.contenttypes import * +from .serializers_.customfields import * +from .serializers_.customlinks import * +from .serializers_.dashboard import * +from .serializers_.events import * +from .serializers_.exporttemplates import * +from .serializers_.journaling import * +from .serializers_.provisioning import * +from .serializers_.savedfilters import * +from .serializers_.scripts import * +from .serializers_.tags import * from .nested_serializers import * - -__all__ = ( - 'BookmarkSerializer', - 'ConfigContextSerializer', - 'ConfigTemplateSerializer', - 'ContentTypeSerializer', - 'CustomFieldChoiceSetSerializer', - 'CustomFieldSerializer', - 'CustomLinkSerializer', - 'DashboardSerializer', - 'EventRuleSerializer', - 'ExportTemplateSerializer', - 'ImageAttachmentSerializer', - 'JournalEntrySerializer', - 'ObjectChangeSerializer', - 'SavedFilterSerializer', - 'ScriptDetailSerializer', - 'ScriptInputSerializer', - 'ScriptSerializer', - 'TagSerializer', - 'WebhookSerializer', -) - - -# -# Event Rules -# - -class EventRuleSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='extras-api:eventrule-detail') - content_types = ContentTypeField( - queryset=ContentType.objects.with_feature('event_rules'), - many=True - ) - action_type = ChoiceField(choices=EventRuleActionChoices) - action_object_type = ContentTypeField( - queryset=ContentType.objects.with_feature('event_rules'), - ) - action_object = serializers.SerializerMethodField(read_only=True) - - class Meta: - model = EventRule - fields = [ - 'id', 'url', 'display', 'content_types', 'name', 'type_create', 'type_update', 'type_delete', - 'type_job_start', 'type_job_end', 'enabled', 'conditions', 'action_type', 'action_object_type', - 'action_object_id', 'action_object', 'description', 'custom_fields', 'tags', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - @extend_schema_field(OpenApiTypes.OBJECT) - def get_action_object(self, instance): - context = {'request': self.context['request']} - # We need to manually instantiate the serializer for scripts - if instance.action_type == EventRuleActionChoices.SCRIPT: - script = instance.action_object - instance = script.python_class() if script.python_class else None - return NestedScriptSerializer(instance, context=context).data - else: - serializer = get_serializer_for_model( - model=instance.action_object_type.model_class(), - prefix=NESTED_SERIALIZER_PREFIX - ) - return serializer(instance.action_object, context=context).data - - -# -# Webhooks -# - -class WebhookSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='extras-api:webhook-detail') - - class Meta: - model = Webhook - fields = [ - 'id', 'url', 'display', 'name', 'description', 'payload_url', 'http_method', 'http_content_type', - 'additional_headers', 'body_template', 'secret', 'ssl_verification', 'ca_file_path', 'custom_fields', - 'tags', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -# -# Custom fields -# - -class CustomFieldChoiceSetSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfieldchoiceset-detail') - base_choices = ChoiceField( - choices=CustomFieldChoiceSetBaseChoices, - required=False - ) - extra_choices = serializers.ListField( - child=serializers.ListField( - min_length=2, - max_length=2 - ) - ) - - class Meta: - model = CustomFieldChoiceSet - fields = [ - 'id', 'url', 'display', 'name', 'description', 'base_choices', 'extra_choices', 'order_alphabetically', - 'choices_count', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description', 'choices_count') - - -class CustomFieldSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfield-detail') - content_types = ContentTypeField( - queryset=ContentType.objects.with_feature('custom_fields'), - many=True - ) - type = ChoiceField(choices=CustomFieldTypeChoices) - object_type = ContentTypeField( - queryset=ContentType.objects.all(), - required=False, - allow_null=True - ) - filter_logic = ChoiceField(choices=CustomFieldFilterLogicChoices, required=False) - data_type = serializers.SerializerMethodField() - choice_set = CustomFieldChoiceSetSerializer( - nested=True, - required=False, - allow_null=True - ) - ui_visible = ChoiceField(choices=CustomFieldUIVisibleChoices, required=False) - ui_editable = ChoiceField(choices=CustomFieldUIEditableChoices, required=False) - - class Meta: - model = CustomField - fields = [ - 'id', 'url', 'display', 'content_types', 'type', 'object_type', 'data_type', 'name', 'label', 'group_name', - 'description', 'required', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'is_cloneable', - 'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choice_set', - 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - def validate_type(self, value): - if self.instance and self.instance.type != value: - raise serializers.ValidationError(_('Changing the type of custom fields is not supported.')) - - return value - - @extend_schema_field(OpenApiTypes.STR) - def get_data_type(self, obj): - types = CustomFieldTypeChoices - if obj.type == types.TYPE_INTEGER: - return 'integer' - if obj.type == types.TYPE_DECIMAL: - return 'decimal' - if obj.type == types.TYPE_BOOLEAN: - return 'boolean' - if obj.type in (types.TYPE_JSON, types.TYPE_OBJECT): - return 'object' - if obj.type in (types.TYPE_MULTISELECT, types.TYPE_MULTIOBJECT): - return 'array' - return 'string' - - -# -# Custom links -# - -class CustomLinkSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='extras-api:customlink-detail') - content_types = ContentTypeField( - queryset=ContentType.objects.with_feature('custom_links'), - many=True - ) - - class Meta: - model = CustomLink - fields = [ - 'id', 'url', 'display', 'content_types', 'name', 'enabled', 'link_text', 'link_url', 'weight', 'group_name', - 'button_class', 'new_window', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name') - - -# -# Export templates -# - -class ExportTemplateSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='extras-api:exporttemplate-detail') - content_types = ContentTypeField( - queryset=ContentType.objects.with_feature('export_templates'), - many=True - ) - data_source = DataSourceSerializer( - nested=True, - required=False - ) - data_file = DataFileSerializer( - nested=True, - read_only=True - ) - - class Meta: - model = ExportTemplate - fields = [ - 'id', 'url', 'display', 'content_types', 'name', 'description', 'template_code', 'mime_type', - 'file_extension', 'as_attachment', 'data_source', 'data_path', 'data_file', 'data_synced', 'created', - 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -# -# Saved filters -# - -class SavedFilterSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='extras-api:savedfilter-detail') - content_types = ContentTypeField( - queryset=ContentType.objects.all(), - many=True - ) - - class Meta: - model = SavedFilter - fields = [ - 'id', 'url', 'display', 'content_types', 'name', 'slug', 'description', 'user', 'weight', 'enabled', - 'shared', 'parameters', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description') - - -# -# Bookmarks -# - -class BookmarkSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='extras-api:bookmark-detail') - object_type = ContentTypeField( - queryset=ContentType.objects.with_feature('bookmarks'), - ) - object = serializers.SerializerMethodField(read_only=True) - user = UserSerializer(nested=True) - - class Meta: - model = Bookmark - fields = [ - 'id', 'url', 'display', 'object_type', 'object_id', 'object', 'user', 'created', - ] - brief_fields = ('id', 'url', 'display', 'object_id', 'object_type') - - @extend_schema_field(serializers.JSONField(allow_null=True)) - def get_object(self, instance): - serializer = get_serializer_for_model(instance.object, prefix=NESTED_SERIALIZER_PREFIX) - return serializer(instance.object, context={'request': self.context['request']}).data - - -# -# Tags -# - -class TagSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='extras-api:tag-detail') - object_types = ContentTypeField( - queryset=ContentType.objects.with_feature('tags'), - many=True, - required=False - ) - - # Related object counts - tagged_items = RelatedObjectCountField('extras_taggeditem_items') - - class Meta: - model = Tag - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'object_types', 'tagged_items', 'created', - 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'color', 'description') - - -# -# Image attachments -# - -class ImageAttachmentSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='extras-api:imageattachment-detail') - content_type = ContentTypeField( - queryset=ContentType.objects.all() - ) - parent = serializers.SerializerMethodField(read_only=True) - - class Meta: - model = ImageAttachment - fields = [ - 'id', 'url', 'display', 'content_type', 'object_id', 'parent', 'name', 'image', 'image_height', - 'image_width', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'image') - - def validate(self, data): - - # Validate that the parent object exists - try: - data['content_type'].get_object_for_this_type(id=data['object_id']) - except ObjectDoesNotExist: - raise serializers.ValidationError( - "Invalid parent object: {} ID {}".format(data['content_type'], data['object_id']) - ) - - # Enforce model validation - super().validate(data) - - return data - - @extend_schema_field(serializers.JSONField(allow_null=True)) - def get_parent(self, obj): - serializer = get_serializer_for_model(obj.parent, prefix=NESTED_SERIALIZER_PREFIX) - return serializer(obj.parent, context={'request': self.context['request']}).data - - -# -# Journal entries -# - -class JournalEntrySerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='extras-api:journalentry-detail') - assigned_object_type = ContentTypeField( - queryset=ContentType.objects.all() - ) - assigned_object = serializers.SerializerMethodField(read_only=True) - created_by = serializers.PrimaryKeyRelatedField( - allow_null=True, - queryset=get_user_model().objects.all(), - required=False, - default=serializers.CurrentUserDefault() - ) - kind = ChoiceField( - choices=JournalEntryKindChoices, - required=False - ) - - class Meta: - model = JournalEntry - fields = [ - 'id', 'url', 'display', 'assigned_object_type', 'assigned_object_id', 'assigned_object', 'created', - 'created_by', 'kind', 'comments', 'tags', 'custom_fields', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'created') - - def validate(self, data): - - # Validate that the parent object exists - if 'assigned_object_type' in data and 'assigned_object_id' in data: - try: - data['assigned_object_type'].get_object_for_this_type(id=data['assigned_object_id']) - except ObjectDoesNotExist: - raise serializers.ValidationError( - f"Invalid assigned_object: {data['assigned_object_type']} ID {data['assigned_object_id']}" - ) - - # Enforce model validation - super().validate(data) - - return data - - @extend_schema_field(serializers.JSONField(allow_null=True)) - def get_assigned_object(self, instance): - serializer = get_serializer_for_model(instance.assigned_object_type.model_class(), prefix=NESTED_SERIALIZER_PREFIX) - context = {'request': self.context['request']} - return serializer(instance.assigned_object, context=context).data - - -# -# Config contexts -# - -class ConfigContextSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='extras-api:configcontext-detail') - regions = SerializedPKRelatedField( - queryset=Region.objects.all(), - serializer=NestedRegionSerializer, - required=False, - many=True - ) - site_groups = SerializedPKRelatedField( - queryset=SiteGroup.objects.all(), - serializer=NestedSiteGroupSerializer, - required=False, - many=True - ) - sites = SerializedPKRelatedField( - queryset=Site.objects.all(), - serializer=NestedSiteSerializer, - required=False, - many=True - ) - locations = SerializedPKRelatedField( - queryset=Location.objects.all(), - serializer=NestedLocationSerializer, - required=False, - many=True - ) - device_types = SerializedPKRelatedField( - queryset=DeviceType.objects.all(), - serializer=NestedDeviceTypeSerializer, - required=False, - many=True - ) - roles = SerializedPKRelatedField( - queryset=DeviceRole.objects.all(), - serializer=NestedDeviceRoleSerializer, - required=False, - many=True - ) - platforms = SerializedPKRelatedField( - queryset=Platform.objects.all(), - serializer=NestedPlatformSerializer, - required=False, - many=True - ) - cluster_types = SerializedPKRelatedField( - queryset=ClusterType.objects.all(), - serializer=NestedClusterTypeSerializer, - required=False, - many=True - ) - cluster_groups = SerializedPKRelatedField( - queryset=ClusterGroup.objects.all(), - serializer=NestedClusterGroupSerializer, - required=False, - many=True - ) - clusters = SerializedPKRelatedField( - queryset=Cluster.objects.all(), - serializer=NestedClusterSerializer, - required=False, - many=True - ) - tenant_groups = SerializedPKRelatedField( - queryset=TenantGroup.objects.all(), - serializer=NestedTenantGroupSerializer, - required=False, - many=True - ) - tenants = SerializedPKRelatedField( - queryset=Tenant.objects.all(), - serializer=NestedTenantSerializer, - required=False, - many=True - ) - tags = serializers.SlugRelatedField( - queryset=Tag.objects.all(), - slug_field='slug', - required=False, - many=True - ) - data_source = DataSourceSerializer( - nested=True, - required=False - ) - data_file = DataFileSerializer( - nested=True, - read_only=True - ) - - class Meta: - model = ConfigContext - fields = [ - 'id', 'url', 'display', 'name', 'weight', 'description', 'is_active', 'regions', 'site_groups', 'sites', - 'locations', 'device_types', 'roles', 'platforms', 'cluster_types', 'cluster_groups', 'clusters', - 'tenant_groups', 'tenants', 'tags', 'data_source', 'data_path', 'data_file', 'data_synced', 'data', - 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -# -# Config templates -# - -class ConfigTemplateSerializer(TaggableModelSerializer, ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='extras-api:configtemplate-detail') - data_source = DataSourceSerializer( - nested=True, - required=False - ) - data_file = DataFileSerializer( - nested=True, - required=False - ) - - class Meta: - model = ConfigTemplate - fields = [ - 'id', 'url', 'display', 'name', 'description', 'environment_params', 'template_code', 'data_source', - 'data_path', 'data_file', 'data_synced', 'tags', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -# -# Scripts -# - -class ScriptSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='extras-api:script-detail') - description = serializers.SerializerMethodField(read_only=True) - vars = serializers.SerializerMethodField(read_only=True) - result = JobSerializer(nested=True, read_only=True) - - class Meta: - model = Script - fields = [ - 'id', 'url', 'module', 'name', 'description', 'vars', 'result', 'display', 'is_executable', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - @extend_schema_field(serializers.JSONField(allow_null=True)) - def get_vars(self, obj): - if obj.python_class: - return { - k: v.__class__.__name__ for k, v in obj.python_class()._get_vars().items() - } - else: - return {} - - @extend_schema_field(serializers.CharField()) - def get_display(self, obj): - return f'{obj.name} ({obj.module})' - - @extend_schema_field(serializers.CharField()) - def get_description(self, obj): - if obj.python_class: - return obj.python_class().description - else: - return None - - -class ScriptDetailSerializer(ScriptSerializer): - result = serializers.SerializerMethodField(read_only=True) - - @extend_schema_field(JobSerializer()) - def get_result(self, obj): - job = obj.jobs.all().order_by('-created').first() - context = { - 'request': self.context['request'] - } - data = JobSerializer(job, context=context).data - return data - - -class ScriptInputSerializer(serializers.Serializer): - data = serializers.JSONField() - commit = serializers.BooleanField() - schedule_at = serializers.DateTimeField(required=False, allow_null=True) - interval = serializers.IntegerField(required=False, allow_null=True) - - def validate_schedule_at(self, value): - if value and not self.context['script'].scheduling_enabled: - raise serializers.ValidationError(_("Scheduling is not enabled for this script.")) - return value - - def validate_interval(self, value): - if value and not self.context['script'].scheduling_enabled: - raise serializers.ValidationError(_("Scheduling is not enabled for this script.")) - return value - - -# -# Change logging -# - -class ObjectChangeSerializer(BaseModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='extras-api:objectchange-detail') - user = UserSerializer( - nested=True, - read_only=True - ) - action = ChoiceField( - choices=ObjectChangeActionChoices, - read_only=True - ) - changed_object_type = ContentTypeField( - read_only=True - ) - changed_object = serializers.SerializerMethodField( - read_only=True - ) - - class Meta: - model = ObjectChange - fields = [ - 'id', 'url', 'display', 'time', 'user', 'user_name', 'request_id', 'action', 'changed_object_type', - 'changed_object_id', 'changed_object', 'prechange_data', 'postchange_data', - ] - - @extend_schema_field(serializers.JSONField(allow_null=True)) - def get_changed_object(self, obj): - """ - Serialize a nested representation of the changed object. - """ - if obj.changed_object is None: - return None - - try: - serializer = get_serializer_for_model(obj.changed_object, prefix=NESTED_SERIALIZER_PREFIX) - except SerializerNotFound: - return obj.object_repr - context = { - 'request': self.context['request'] - } - data = serializer(obj.changed_object, context=context).data - - return data - - -# -# ContentTypes -# - -class ContentTypeSerializer(BaseModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='extras-api:contenttype-detail') - - class Meta: - model = ContentType - fields = ['id', 'url', 'display', 'app_label', 'model'] - - -# -# User dashboard -# - -class DashboardSerializer(serializers.ModelSerializer): - class Meta: - model = Dashboard - fields = ('layout', 'config') diff --git a/netbox/extras/api/serializers_/__init__.py b/netbox/extras/api/serializers_/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/extras/api/serializers_/attachments.py b/netbox/extras/api/serializers_/attachments.py new file mode 100644 index 000000000..2a6730557 --- /dev/null +++ b/netbox/extras/api/serializers_/attachments.py @@ -0,0 +1,50 @@ +from django.core.exceptions import ObjectDoesNotExist +from drf_spectacular.utils import extend_schema_field +from rest_framework import serializers + +from core.models import ContentType +from extras.models import ImageAttachment +from netbox.api.fields import ContentTypeField +from netbox.api.serializers import ValidatedModelSerializer +from netbox.constants import NESTED_SERIALIZER_PREFIX +from utilities.api import get_serializer_for_model + +__all__ = ( + 'ImageAttachmentSerializer', +) + + +class ImageAttachmentSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='extras-api:imageattachment-detail') + content_type = ContentTypeField( + queryset=ContentType.objects.all() + ) + parent = serializers.SerializerMethodField(read_only=True) + + class Meta: + model = ImageAttachment + fields = [ + 'id', 'url', 'display', 'content_type', 'object_id', 'parent', 'name', 'image', 'image_height', + 'image_width', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'image') + + def validate(self, data): + + # Validate that the parent object exists + try: + data['content_type'].get_object_for_this_type(id=data['object_id']) + except ObjectDoesNotExist: + raise serializers.ValidationError( + "Invalid parent object: {} ID {}".format(data['content_type'], data['object_id']) + ) + + # Enforce model validation + super().validate(data) + + return data + + @extend_schema_field(serializers.JSONField(allow_null=True)) + def get_parent(self, obj): + serializer = get_serializer_for_model(obj.parent, prefix=NESTED_SERIALIZER_PREFIX) + return serializer(obj.parent, context={'request': self.context['request']}).data diff --git a/netbox/extras/api/serializers_/bookmarks.py b/netbox/extras/api/serializers_/bookmarks.py new file mode 100644 index 000000000..e8b34fb0f --- /dev/null +++ b/netbox/extras/api/serializers_/bookmarks.py @@ -0,0 +1,35 @@ +from drf_spectacular.utils import extend_schema_field +from rest_framework import serializers + +from core.models import ContentType +from extras.models import Bookmark +from netbox.api.fields import ContentTypeField +from netbox.api.serializers import ValidatedModelSerializer +from netbox.constants import NESTED_SERIALIZER_PREFIX +from users.api.serializers_.users import UserSerializer +from utilities.api import get_serializer_for_model + +__all__ = ( + 'BookmarkSerializer', +) + + +class BookmarkSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='extras-api:bookmark-detail') + object_type = ContentTypeField( + queryset=ContentType.objects.with_feature('bookmarks'), + ) + object = serializers.SerializerMethodField(read_only=True) + user = UserSerializer(nested=True) + + class Meta: + model = Bookmark + fields = [ + 'id', 'url', 'display', 'object_type', 'object_id', 'object', 'user', 'created', + ] + brief_fields = ('id', 'url', 'display', 'object_id', 'object_type') + + @extend_schema_field(serializers.JSONField(allow_null=True)) + def get_object(self, instance): + serializer = get_serializer_for_model(instance.object, prefix=NESTED_SERIALIZER_PREFIX) + return serializer(instance.object, context={'request': self.context['request']}).data diff --git a/netbox/extras/api/serializers_/change_logging.py b/netbox/extras/api/serializers_/change_logging.py new file mode 100644 index 000000000..bf63a777f --- /dev/null +++ b/netbox/extras/api/serializers_/change_logging.py @@ -0,0 +1,59 @@ +from drf_spectacular.utils import extend_schema_field +from rest_framework import serializers + +from extras.choices import * +from extras.models import ObjectChange +from netbox.api.exceptions import SerializerNotFound +from netbox.api.fields import ChoiceField, ContentTypeField +from netbox.api.serializers import BaseModelSerializer +from netbox.constants import NESTED_SERIALIZER_PREFIX +from users.api.serializers_.users import UserSerializer +from utilities.api import get_serializer_for_model + +__all__ = ( + 'ObjectChangeSerializer', +) + + +class ObjectChangeSerializer(BaseModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='extras-api:objectchange-detail') + user = UserSerializer( + nested=True, + read_only=True + ) + action = ChoiceField( + choices=ObjectChangeActionChoices, + read_only=True + ) + changed_object_type = ContentTypeField( + read_only=True + ) + changed_object = serializers.SerializerMethodField( + read_only=True + ) + + class Meta: + model = ObjectChange + fields = [ + 'id', 'url', 'display', 'time', 'user', 'user_name', 'request_id', 'action', 'changed_object_type', + 'changed_object_id', 'changed_object', 'prechange_data', 'postchange_data', + ] + + @extend_schema_field(serializers.JSONField(allow_null=True)) + def get_changed_object(self, obj): + """ + Serialize a nested representation of the changed object. + """ + if obj.changed_object is None: + return None + + try: + serializer = get_serializer_for_model(obj.changed_object, prefix=NESTED_SERIALIZER_PREFIX) + except SerializerNotFound: + return obj.object_repr + context = { + 'request': self.context['request'] + } + data = serializer(obj.changed_object, context=context).data + + return data diff --git a/netbox/extras/api/serializers_/contenttypes.py b/netbox/extras/api/serializers_/contenttypes.py new file mode 100644 index 000000000..cc11e88b6 --- /dev/null +++ b/netbox/extras/api/serializers_/contenttypes.py @@ -0,0 +1,16 @@ +from rest_framework import serializers + +from core.models import ContentType +from netbox.api.serializers import BaseModelSerializer + +__all__ = ( + 'ContentTypeSerializer', +) + + +class ContentTypeSerializer(BaseModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='extras-api:contenttype-detail') + + class Meta: + model = ContentType + fields = ['id', 'url', 'display', 'app_label', 'model'] diff --git a/netbox/extras/api/serializers_/customfields.py b/netbox/extras/api/serializers_/customfields.py new file mode 100644 index 000000000..668e780d9 --- /dev/null +++ b/netbox/extras/api/serializers_/customfields.py @@ -0,0 +1,91 @@ +from django.utils.translation import gettext as _ +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import extend_schema_field +from rest_framework import serializers + +from core.models import ContentType +from extras.choices import * +from extras.models import CustomField, CustomFieldChoiceSet +from netbox.api.fields import ChoiceField, ContentTypeField +from netbox.api.serializers import ValidatedModelSerializer + +__all__ = ( + 'CustomFieldChoiceSetSerializer', + 'CustomFieldSerializer', +) + + +class CustomFieldChoiceSetSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfieldchoiceset-detail') + base_choices = ChoiceField( + choices=CustomFieldChoiceSetBaseChoices, + required=False + ) + extra_choices = serializers.ListField( + child=serializers.ListField( + min_length=2, + max_length=2 + ) + ) + + class Meta: + model = CustomFieldChoiceSet + fields = [ + 'id', 'url', 'display', 'name', 'description', 'base_choices', 'extra_choices', 'order_alphabetically', + 'choices_count', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description', 'choices_count') + + +class CustomFieldSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfield-detail') + content_types = ContentTypeField( + queryset=ContentType.objects.with_feature('custom_fields'), + many=True + ) + type = ChoiceField(choices=CustomFieldTypeChoices) + object_type = ContentTypeField( + queryset=ContentType.objects.all(), + required=False, + allow_null=True + ) + filter_logic = ChoiceField(choices=CustomFieldFilterLogicChoices, required=False) + data_type = serializers.SerializerMethodField() + choice_set = CustomFieldChoiceSetSerializer( + nested=True, + required=False, + allow_null=True + ) + ui_visible = ChoiceField(choices=CustomFieldUIVisibleChoices, required=False) + ui_editable = ChoiceField(choices=CustomFieldUIEditableChoices, required=False) + + class Meta: + model = CustomField + fields = [ + 'id', 'url', 'display', 'content_types', 'type', 'object_type', 'data_type', 'name', 'label', 'group_name', + 'description', 'required', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'is_cloneable', + 'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choice_set', + 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') + + def validate_type(self, value): + if self.instance and self.instance.type != value: + raise serializers.ValidationError(_('Changing the type of custom fields is not supported.')) + + return value + + @extend_schema_field(OpenApiTypes.STR) + def get_data_type(self, obj): + types = CustomFieldTypeChoices + if obj.type == types.TYPE_INTEGER: + return 'integer' + if obj.type == types.TYPE_DECIMAL: + return 'decimal' + if obj.type == types.TYPE_BOOLEAN: + return 'boolean' + if obj.type in (types.TYPE_JSON, types.TYPE_OBJECT): + return 'object' + if obj.type in (types.TYPE_MULTISELECT, types.TYPE_MULTIOBJECT): + return 'array' + return 'string' diff --git a/netbox/extras/api/serializers_/customlinks.py b/netbox/extras/api/serializers_/customlinks.py new file mode 100644 index 000000000..8f53db2ca --- /dev/null +++ b/netbox/extras/api/serializers_/customlinks.py @@ -0,0 +1,26 @@ +from rest_framework import serializers + +from core.models import ContentType +from extras.models import CustomLink +from netbox.api.fields import ContentTypeField +from netbox.api.serializers import ValidatedModelSerializer + +__all__ = ( + 'CustomLinkSerializer', +) + + +class CustomLinkSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='extras-api:customlink-detail') + content_types = ContentTypeField( + queryset=ContentType.objects.with_feature('custom_links'), + many=True + ) + + class Meta: + model = CustomLink + fields = [ + 'id', 'url', 'display', 'content_types', 'name', 'enabled', 'link_text', 'link_url', 'weight', 'group_name', + 'button_class', 'new_window', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name') diff --git a/netbox/extras/api/serializers_/dashboard.py b/netbox/extras/api/serializers_/dashboard.py new file mode 100644 index 000000000..74d5daecf --- /dev/null +++ b/netbox/extras/api/serializers_/dashboard.py @@ -0,0 +1,13 @@ +from rest_framework import serializers + +from extras.models import Dashboard + +__all__ = ( + 'DashboardSerializer', +) + + +class DashboardSerializer(serializers.ModelSerializer): + class Meta: + model = Dashboard + fields = ('layout', 'config') diff --git a/netbox/extras/api/serializers_/events.py b/netbox/extras/api/serializers_/events.py new file mode 100644 index 000000000..c95f628db --- /dev/null +++ b/netbox/extras/api/serializers_/events.py @@ -0,0 +1,75 @@ +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import extend_schema_field +from rest_framework import serializers + +from core.models import ContentType +from extras.choices import * +from extras.models import EventRule, Webhook +from netbox.api.fields import ChoiceField, ContentTypeField +from netbox.api.serializers import NetBoxModelSerializer +from netbox.constants import NESTED_SERIALIZER_PREFIX +from utilities.api import get_serializer_for_model +from ..nested_serializers import * + +__all__ = ( + 'EventRuleSerializer', + 'WebhookSerializer', +) + + +# +# Event Rules +# + +class EventRuleSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='extras-api:eventrule-detail') + content_types = ContentTypeField( + queryset=ContentType.objects.with_feature('event_rules'), + many=True + ) + action_type = ChoiceField(choices=EventRuleActionChoices) + action_object_type = ContentTypeField( + queryset=ContentType.objects.with_feature('event_rules'), + ) + action_object = serializers.SerializerMethodField(read_only=True) + + class Meta: + model = EventRule + fields = [ + 'id', 'url', 'display', 'content_types', 'name', 'type_create', 'type_update', 'type_delete', + 'type_job_start', 'type_job_end', 'enabled', 'conditions', 'action_type', 'action_object_type', + 'action_object_id', 'action_object', 'description', 'custom_fields', 'tags', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') + + @extend_schema_field(OpenApiTypes.OBJECT) + def get_action_object(self, instance): + context = {'request': self.context['request']} + # We need to manually instantiate the serializer for scripts + if instance.action_type == EventRuleActionChoices.SCRIPT: + script = instance.action_object + instance = script.python_class() if script.python_class else None + return NestedScriptSerializer(instance, context=context).data + else: + serializer = get_serializer_for_model( + model=instance.action_object_type.model_class(), + prefix=NESTED_SERIALIZER_PREFIX + ) + return serializer(instance.action_object, context=context).data + + +# +# Webhooks +# + +class WebhookSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='extras-api:webhook-detail') + + class Meta: + model = Webhook + fields = [ + 'id', 'url', 'display', 'name', 'description', 'payload_url', 'http_method', 'http_content_type', + 'additional_headers', 'body_template', 'secret', 'ssl_verification', 'ca_file_path', 'custom_fields', + 'tags', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') diff --git a/netbox/extras/api/serializers_/exporttemplates.py b/netbox/extras/api/serializers_/exporttemplates.py new file mode 100644 index 000000000..37e36fd55 --- /dev/null +++ b/netbox/extras/api/serializers_/exporttemplates.py @@ -0,0 +1,36 @@ +from rest_framework import serializers + +from core.api.serializers_.data import DataFileSerializer, DataSourceSerializer +from core.models import ContentType +from extras.models import ExportTemplate +from netbox.api.fields import ContentTypeField +from netbox.api.serializers import ValidatedModelSerializer + +__all__ = ( + 'ExportTemplateSerializer', +) + + +class ExportTemplateSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='extras-api:exporttemplate-detail') + content_types = ContentTypeField( + queryset=ContentType.objects.with_feature('export_templates'), + many=True + ) + data_source = DataSourceSerializer( + nested=True, + required=False + ) + data_file = DataFileSerializer( + nested=True, + read_only=True + ) + + class Meta: + model = ExportTemplate + fields = [ + 'id', 'url', 'display', 'content_types', 'name', 'description', 'template_code', 'mime_type', + 'file_extension', 'as_attachment', 'data_source', 'data_path', 'data_file', 'data_synced', 'created', + 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') diff --git a/netbox/extras/api/serializers_/journaling.py b/netbox/extras/api/serializers_/journaling.py new file mode 100644 index 000000000..a7d449152 --- /dev/null +++ b/netbox/extras/api/serializers_/journaling.py @@ -0,0 +1,67 @@ +from django.contrib.auth import get_user_model +from django.core.exceptions import ObjectDoesNotExist +from drf_spectacular.utils import extend_schema_field +from rest_framework import serializers + +from core.models import ContentType +from extras.choices import * +from extras.models import JournalEntry +from netbox.api.fields import ChoiceField, ContentTypeField +from netbox.api.serializers import NetBoxModelSerializer +from netbox.constants import NESTED_SERIALIZER_PREFIX +from utilities.api import get_serializer_for_model + +__all__ = ( + 'JournalEntrySerializer', +) + + +class JournalEntrySerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='extras-api:journalentry-detail') + assigned_object_type = ContentTypeField( + queryset=ContentType.objects.all() + ) + assigned_object = serializers.SerializerMethodField(read_only=True) + created_by = serializers.PrimaryKeyRelatedField( + allow_null=True, + queryset=get_user_model().objects.all(), + required=False, + default=serializers.CurrentUserDefault() + ) + kind = ChoiceField( + choices=JournalEntryKindChoices, + required=False + ) + + class Meta: + model = JournalEntry + fields = [ + 'id', 'url', 'display', 'assigned_object_type', 'assigned_object_id', 'assigned_object', 'created', + 'created_by', 'kind', 'comments', 'tags', 'custom_fields', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'created') + + def validate(self, data): + + # Validate that the parent object exists + if 'assigned_object_type' in data and 'assigned_object_id' in data: + try: + data['assigned_object_type'].get_object_for_this_type(id=data['assigned_object_id']) + except ObjectDoesNotExist: + raise serializers.ValidationError( + f"Invalid assigned_object: {data['assigned_object_type']} ID {data['assigned_object_id']}" + ) + + # Enforce model validation + super().validate(data) + + return data + + @extend_schema_field(serializers.JSONField(allow_null=True)) + def get_assigned_object(self, instance): + serializer = get_serializer_for_model( + instance.assigned_object_type.model_class(), + prefix=NESTED_SERIALIZER_PREFIX + ) + context = {'request': self.context['request']} + return serializer(instance.assigned_object, context=context).data diff --git a/netbox/extras/api/serializers_/provisioning.py b/netbox/extras/api/serializers_/provisioning.py new file mode 100644 index 000000000..a4593a582 --- /dev/null +++ b/netbox/extras/api/serializers_/provisioning.py @@ -0,0 +1,147 @@ +from rest_framework import serializers + +from core.api.serializers_.data import DataFileSerializer, DataSourceSerializer +from dcim.api.nested_serializers import ( + NestedDeviceRoleSerializer, NestedDeviceTypeSerializer, NestedLocationSerializer, NestedPlatformSerializer, + NestedRegionSerializer, NestedSiteSerializer, NestedSiteGroupSerializer, +) +from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup +from extras.models import ConfigContext, ConfigTemplate, Tag +from netbox.api.fields import SerializedPKRelatedField +from netbox.api.serializers import ValidatedModelSerializer +from netbox.api.serializers.features import TaggableModelSerializer +from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer +from tenancy.models import Tenant, TenantGroup +from virtualization.api.nested_serializers import ( + NestedClusterGroupSerializer, NestedClusterSerializer, NestedClusterTypeSerializer, +) +from virtualization.models import Cluster, ClusterGroup, ClusterType + +__all__ = ( + 'ConfigContextSerializer', + 'ConfigTemplateSerializer', +) + + +class ConfigContextSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='extras-api:configcontext-detail') + regions = SerializedPKRelatedField( + queryset=Region.objects.all(), + serializer=NestedRegionSerializer, + required=False, + many=True + ) + site_groups = SerializedPKRelatedField( + queryset=SiteGroup.objects.all(), + serializer=NestedSiteGroupSerializer, + required=False, + many=True + ) + sites = SerializedPKRelatedField( + queryset=Site.objects.all(), + serializer=NestedSiteSerializer, + required=False, + many=True + ) + locations = SerializedPKRelatedField( + queryset=Location.objects.all(), + serializer=NestedLocationSerializer, + required=False, + many=True + ) + device_types = SerializedPKRelatedField( + queryset=DeviceType.objects.all(), + serializer=NestedDeviceTypeSerializer, + required=False, + many=True + ) + roles = SerializedPKRelatedField( + queryset=DeviceRole.objects.all(), + serializer=NestedDeviceRoleSerializer, + required=False, + many=True + ) + platforms = SerializedPKRelatedField( + queryset=Platform.objects.all(), + serializer=NestedPlatformSerializer, + required=False, + many=True + ) + cluster_types = SerializedPKRelatedField( + queryset=ClusterType.objects.all(), + serializer=NestedClusterTypeSerializer, + required=False, + many=True + ) + cluster_groups = SerializedPKRelatedField( + queryset=ClusterGroup.objects.all(), + serializer=NestedClusterGroupSerializer, + required=False, + many=True + ) + clusters = SerializedPKRelatedField( + queryset=Cluster.objects.all(), + serializer=NestedClusterSerializer, + required=False, + many=True + ) + tenant_groups = SerializedPKRelatedField( + queryset=TenantGroup.objects.all(), + serializer=NestedTenantGroupSerializer, + required=False, + many=True + ) + tenants = SerializedPKRelatedField( + queryset=Tenant.objects.all(), + serializer=NestedTenantSerializer, + required=False, + many=True + ) + tags = serializers.SlugRelatedField( + queryset=Tag.objects.all(), + slug_field='slug', + required=False, + many=True + ) + data_source = DataSourceSerializer( + nested=True, + required=False + ) + data_file = DataFileSerializer( + nested=True, + read_only=True + ) + + class Meta: + model = ConfigContext + fields = [ + 'id', 'url', 'display', 'name', 'weight', 'description', 'is_active', 'regions', 'site_groups', 'sites', + 'locations', 'device_types', 'roles', 'platforms', 'cluster_types', 'cluster_groups', 'clusters', + 'tenant_groups', 'tenants', 'tags', 'data_source', 'data_path', 'data_file', 'data_synced', 'data', + 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') + + +# +# Config templates +# + +class ConfigTemplateSerializer(TaggableModelSerializer, ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='extras-api:configtemplate-detail') + data_source = DataSourceSerializer( + nested=True, + required=False + ) + data_file = DataFileSerializer( + nested=True, + required=False + ) + + class Meta: + model = ConfigTemplate + fields = [ + 'id', 'url', 'display', 'name', 'description', 'environment_params', 'template_code', 'data_source', + 'data_path', 'data_file', 'data_synced', 'tags', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') diff --git a/netbox/extras/api/serializers_/savedfilters.py b/netbox/extras/api/serializers_/savedfilters.py new file mode 100644 index 000000000..cb27c0b0d --- /dev/null +++ b/netbox/extras/api/serializers_/savedfilters.py @@ -0,0 +1,26 @@ +from rest_framework import serializers + +from core.models import ContentType +from extras.models import SavedFilter +from netbox.api.fields import ContentTypeField +from netbox.api.serializers import ValidatedModelSerializer + +__all__ = ( + 'SavedFilterSerializer', +) + + +class SavedFilterSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='extras-api:savedfilter-detail') + content_types = ContentTypeField( + queryset=ContentType.objects.all(), + many=True + ) + + class Meta: + model = SavedFilter + fields = [ + 'id', 'url', 'display', 'content_types', 'name', 'slug', 'description', 'user', 'weight', 'enabled', + 'shared', 'parameters', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description') diff --git a/netbox/extras/api/serializers_/scripts.py b/netbox/extras/api/serializers_/scripts.py new file mode 100644 index 000000000..b2a8ef29d --- /dev/null +++ b/netbox/extras/api/serializers_/scripts.py @@ -0,0 +1,77 @@ +from django.utils.translation import gettext as _ +from drf_spectacular.utils import extend_schema_field +from rest_framework import serializers + +from core.api.serializers_.jobs import JobSerializer +from extras.models import Script +from netbox.api.serializers import ValidatedModelSerializer + +__all__ = ( + 'ScriptDetailSerializer', + 'ScriptInputSerializer', + 'ScriptSerializer', +) + + +class ScriptSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='extras-api:script-detail') + description = serializers.SerializerMethodField(read_only=True) + vars = serializers.SerializerMethodField(read_only=True) + result = JobSerializer(nested=True, read_only=True) + + class Meta: + model = Script + fields = [ + 'id', 'url', 'module', 'name', 'description', 'vars', 'result', 'display', 'is_executable', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') + + @extend_schema_field(serializers.JSONField(allow_null=True)) + def get_vars(self, obj): + if obj.python_class: + return { + k: v.__class__.__name__ for k, v in obj.python_class()._get_vars().items() + } + else: + return {} + + @extend_schema_field(serializers.CharField()) + def get_display(self, obj): + return f'{obj.name} ({obj.module})' + + @extend_schema_field(serializers.CharField()) + def get_description(self, obj): + if obj.python_class: + return obj.python_class().description + else: + return None + + +class ScriptDetailSerializer(ScriptSerializer): + result = serializers.SerializerMethodField(read_only=True) + + @extend_schema_field(JobSerializer()) + def get_result(self, obj): + job = obj.jobs.all().order_by('-created').first() + context = { + 'request': self.context['request'] + } + data = JobSerializer(job, context=context).data + return data + + +class ScriptInputSerializer(serializers.Serializer): + data = serializers.JSONField() + commit = serializers.BooleanField() + schedule_at = serializers.DateTimeField(required=False, allow_null=True) + interval = serializers.IntegerField(required=False, allow_null=True) + + def validate_schedule_at(self, value): + if value and not self.context['script'].scheduling_enabled: + raise serializers.ValidationError(_("Scheduling is not enabled for this script.")) + return value + + def validate_interval(self, value): + if value and not self.context['script'].scheduling_enabled: + raise serializers.ValidationError(_("Scheduling is not enabled for this script.")) + return value diff --git a/netbox/extras/api/serializers_/tags.py b/netbox/extras/api/serializers_/tags.py new file mode 100644 index 000000000..28a021f29 --- /dev/null +++ b/netbox/extras/api/serializers_/tags.py @@ -0,0 +1,30 @@ +from rest_framework import serializers + +from core.models import ContentType +from extras.models import Tag +from netbox.api.fields import ContentTypeField, RelatedObjectCountField +from netbox.api.serializers import ValidatedModelSerializer + +__all__ = ( + 'TagSerializer', +) + + +class TagSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='extras-api:tag-detail') + object_types = ContentTypeField( + queryset=ContentType.objects.with_feature('tags'), + many=True, + required=False + ) + + # Related object counts + tagged_items = RelatedObjectCountField('extras_taggeditem_items') + + class Meta: + model = Tag + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'object_types', 'tagged_items', 'created', + 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'color', 'description') diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index c4b1b1799..1f5f21028 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -1,511 +1,8 @@ -from django.contrib.contenttypes.models import ContentType -from drf_spectacular.utils import extend_schema_field -from rest_framework import serializers - -from dcim.api.serializers import DeviceSerializer, SiteSerializer -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, RelatedObjectCountField, SerializedPKRelatedField -from netbox.api.serializers import NetBoxModelSerializer -from netbox.constants import NESTED_SERIALIZER_PREFIX -from tenancy.api.serializers import TenantSerializer -from utilities.api import get_serializer_for_model -from virtualization.api.serializers import VirtualMachineSerializer -from vpn.api.nested_serializers import NestedL2VPNTerminationSerializer -from .field_serializers import IPAddressField, IPNetworkField +from .serializers_.asns import * +from .serializers_.vrfs import * +from .serializers_.roles import * +from .serializers_.vlans import * +from .serializers_.ip import * +from .serializers_.fhrpgroups import * +from .serializers_.services import * from .nested_serializers import * - - -# -# RIRs -# - -class RIRSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail') - - # Related object counts - aggregate_count = RelatedObjectCountField('aggregates') - - class Meta: - model = RIR - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'is_private', 'description', 'tags', 'custom_fields', 'created', - 'last_updated', 'aggregate_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'aggregate_count') - - -# -# ASN ranges -# - -class ASNRangeSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asnrange-detail') - rir = RIRSerializer(nested=True) - tenant = TenantSerializer(nested=True, required=False, allow_null=True) - asn_count = serializers.IntegerField(read_only=True) - - class Meta: - model = ASNRange - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'rir', 'start', 'end', 'tenant', 'description', 'tags', - 'custom_fields', 'created', 'last_updated', 'asn_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -# -# ASNs -# - -class ASNSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asn-detail') - rir = RIRSerializer(nested=True, required=False, allow_null=True) - tenant = TenantSerializer(nested=True, required=False, allow_null=True) - - # Related object counts - site_count = RelatedObjectCountField('sites') - provider_count = RelatedObjectCountField('providers') - - class Meta: - model = ASN - fields = [ - 'id', 'url', 'display', 'asn', 'rir', 'tenant', 'description', 'comments', 'tags', 'custom_fields', - 'created', 'last_updated', 'site_count', 'provider_count', - ] - brief_fields = ('id', 'url', 'display', 'asn', 'description') - - -class AvailableASNSerializer(serializers.Serializer): - """ - Representation of an ASN which does not exist in the database. - """ - asn = serializers.IntegerField(read_only=True) - description = serializers.CharField(required=False) - - def to_representation(self, asn): - rir = RIRSerializer(self.context['range'].rir, nested=True, context={ - 'request': self.context['request'] - }).data - return { - 'rir': rir, - 'asn': asn, - } - - -# -# VRFs -# - -class VRFSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vrf-detail') - tenant = TenantSerializer(nested=True, required=False, allow_null=True) - import_targets = SerializedPKRelatedField( - queryset=RouteTarget.objects.all(), - serializer=NestedRouteTargetSerializer, - required=False, - many=True - ) - export_targets = SerializedPKRelatedField( - queryset=RouteTarget.objects.all(), - serializer=NestedRouteTargetSerializer, - required=False, - many=True - ) - - # Related object counts - ipaddress_count = RelatedObjectCountField('ip_addresses') - prefix_count = RelatedObjectCountField('prefixes') - - class Meta: - model = VRF - fields = [ - 'id', 'url', 'display', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'comments', - 'import_targets', 'export_targets', 'tags', 'custom_fields', 'created', 'last_updated', 'ipaddress_count', - 'prefix_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'rd', 'description', 'prefix_count') - - -class RouteTargetSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='ipam-api:routetarget-detail') - tenant = TenantSerializer(nested=True, required=False, allow_null=True) - - class Meta: - model = RouteTarget - fields = [ - 'id', 'url', 'display', 'name', 'tenant', 'description', 'comments', 'tags', 'custom_fields', 'created', - 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -# -# Aggregates -# - -class AggregateSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='ipam-api:aggregate-detail') - family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True) - rir = RIRSerializer(nested=True) - tenant = TenantSerializer(nested=True, required=False, allow_null=True) - prefix = IPNetworkField() - - class Meta: - model = Aggregate - fields = [ - 'id', 'url', 'display', 'family', 'prefix', 'rir', 'tenant', 'date_added', 'description', 'comments', - 'tags', 'custom_fields', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'family', 'prefix', 'description') - - -# -# FHRP Groups -# - -class FHRPGroupSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='ipam-api:fhrpgroup-detail') - ip_addresses = NestedIPAddressSerializer(many=True, read_only=True) - - class Meta: - model = FHRPGroup - fields = [ - 'id', 'name', 'url', 'display', 'protocol', 'group_id', 'auth_type', 'auth_key', 'description', 'comments', - 'tags', 'custom_fields', 'created', 'last_updated', 'ip_addresses', - ] - brief_fields = ('id', 'url', 'display', 'protocol', 'group_id', 'description') - - -class FHRPGroupAssignmentSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='ipam-api:fhrpgroupassignment-detail') - group = FHRPGroupSerializer(nested=True) - interface_type = ContentTypeField( - queryset=ContentType.objects.all() - ) - interface = serializers.SerializerMethodField(read_only=True) - - class Meta: - model = FHRPGroupAssignment - fields = [ - 'id', 'url', 'display', 'group', 'interface_type', 'interface_id', 'interface', 'priority', 'created', - 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'group', 'interface_type', 'interface_id', 'priority') - - @extend_schema_field(serializers.JSONField(allow_null=True)) - def get_interface(self, obj): - if obj.interface is None: - return None - serializer = get_serializer_for_model(obj.interface, prefix=NESTED_SERIALIZER_PREFIX) - context = {'request': self.context['request']} - return serializer(obj.interface, context=context).data - - -# -# VLANs -# - -class RoleSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='ipam-api:role-detail') - - # Related object counts - prefix_count = RelatedObjectCountField('prefixes') - vlan_count = RelatedObjectCountField('vlans') - - class Meta: - model = Role - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'weight', 'description', 'tags', 'custom_fields', 'created', - 'last_updated', 'prefix_count', 'vlan_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'prefix_count', 'vlan_count') - - -class VLANGroupSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail') - scope_type = ContentTypeField( - queryset=ContentType.objects.filter( - model__in=VLANGROUP_SCOPE_TYPES - ), - allow_null=True, - required=False, - default=None - ) - scope_id = serializers.IntegerField(allow_null=True, required=False, default=None) - scope = serializers.SerializerMethodField(read_only=True) - utilization = serializers.CharField(read_only=True) - - # Related object counts - vlan_count = RelatedObjectCountField('vlans') - - class Meta: - model = VLANGroup - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'scope_type', 'scope_id', 'scope', 'min_vid', 'max_vid', - 'description', 'tags', 'custom_fields', 'created', 'last_updated', 'vlan_count', 'utilization' - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'vlan_count') - validators = [] - - @extend_schema_field(serializers.JSONField(allow_null=True)) - def get_scope(self, obj): - if obj.scope_id is None: - return None - serializer = get_serializer_for_model(obj.scope, prefix=NESTED_SERIALIZER_PREFIX) - context = {'request': self.context['request']} - - return serializer(obj.scope, context=context).data - - -class VLANSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail') - site = SiteSerializer(nested=True, required=False, allow_null=True) - group = VLANGroupSerializer(nested=True, required=False, allow_null=True, default=None) - tenant = TenantSerializer(nested=True, required=False, allow_null=True) - status = ChoiceField(choices=VLANStatusChoices, required=False) - role = RoleSerializer(nested=True, required=False, allow_null=True) - l2vpn_termination = NestedL2VPNTerminationSerializer(read_only=True, allow_null=True) - - # Related object counts - prefix_count = RelatedObjectCountField('prefixes') - - class Meta: - model = VLAN - fields = [ - 'id', 'url', 'display', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', - 'comments', 'l2vpn_termination', 'tags', 'custom_fields', 'created', 'last_updated', 'prefix_count', - ] - brief_fields = ('id', 'url', 'display', 'vid', 'name', 'description') - - -class AvailableVLANSerializer(serializers.Serializer): - """ - Representation of a VLAN which does not exist in the database. - """ - vid = serializers.IntegerField(read_only=True) - group = VLANGroupSerializer(nested=True, read_only=True) - - def to_representation(self, instance): - return { - 'vid': instance, - 'group': VLANGroupSerializer( - self.context['group'], - nested=True, - context={'request': self.context['request']} - ).data, - } - - -class CreateAvailableVLANSerializer(NetBoxModelSerializer): - site = SiteSerializer(nested=True, required=False, allow_null=True) - tenant = TenantSerializer(nested=True, required=False, allow_null=True) - status = ChoiceField(choices=VLANStatusChoices, required=False) - role = RoleSerializer(nested=True, required=False, allow_null=True) - - class Meta: - model = VLAN - fields = [ - 'name', 'site', 'tenant', 'status', 'role', 'description', 'tags', 'custom_fields', - ] - - def validate(self, data): - # Bypass model validation since we don't have a VID yet - return data - - -# -# Prefixes -# - -class PrefixSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='ipam-api:prefix-detail') - family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True) - site = SiteSerializer(nested=True, required=False, allow_null=True) - vrf = VRFSerializer(nested=True, required=False, allow_null=True) - tenant = TenantSerializer(nested=True, required=False, allow_null=True) - vlan = VLANSerializer(nested=True, required=False, allow_null=True) - status = ChoiceField(choices=PrefixStatusChoices, required=False) - role = RoleSerializer(nested=True, required=False, allow_null=True) - children = serializers.IntegerField(read_only=True) - _depth = serializers.IntegerField(read_only=True) - prefix = IPNetworkField() - - class Meta: - model = Prefix - fields = [ - 'id', 'url', 'display', 'family', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', - 'mark_utilized', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'children', - '_depth', - ] - brief_fields = ('id', 'url', 'display', 'family', 'prefix', 'description', '_depth') - - -class PrefixLengthSerializer(serializers.Serializer): - - prefix_length = serializers.IntegerField() - - def to_internal_value(self, data): - requested_prefix = data.get('prefix_length') - if requested_prefix is None: - raise serializers.ValidationError({ - 'prefix_length': 'this field can not be missing' - }) - if not isinstance(requested_prefix, int): - raise serializers.ValidationError({ - 'prefix_length': 'this field must be int type' - }) - - prefix = self.context.get('prefix') - if prefix.family == 4 and requested_prefix > 32: - raise serializers.ValidationError({ - 'prefix_length': 'Invalid prefix length ({}) for IPv4'.format((requested_prefix)) - }) - elif prefix.family == 6 and requested_prefix > 128: - raise serializers.ValidationError({ - 'prefix_length': 'Invalid prefix length ({}) for IPv6'.format((requested_prefix)) - }) - return data - - -class AvailablePrefixSerializer(serializers.Serializer): - """ - Representation of a prefix which does not exist in the database. - """ - family = serializers.IntegerField(read_only=True) - prefix = serializers.CharField(read_only=True) - vrf = VRFSerializer(nested=True, read_only=True) - - def to_representation(self, instance): - if self.context.get('vrf'): - vrf = VRFSerializer(self.context['vrf'], nested=True, context={'request': self.context['request']}).data - else: - vrf = None - return { - 'family': instance.version, - 'prefix': str(instance), - 'vrf': vrf, - } - - -# -# IP ranges -# - -class IPRangeSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='ipam-api:iprange-detail') - family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True) - start_address = IPAddressField() - end_address = IPAddressField() - vrf = VRFSerializer(nested=True, required=False, allow_null=True) - tenant = TenantSerializer(nested=True, required=False, allow_null=True) - status = ChoiceField(choices=IPRangeStatusChoices, required=False) - role = RoleSerializer(nested=True, required=False, allow_null=True) - - class Meta: - model = IPRange - fields = [ - 'id', 'url', 'display', 'family', 'start_address', 'end_address', 'size', 'vrf', 'tenant', 'status', 'role', - 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', - 'mark_utilized', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'family', 'start_address', 'end_address', 'description') - - -# -# IP addresses -# - -class IPAddressSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail') - family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True) - address = IPAddressField() - vrf = VRFSerializer(nested=True, required=False, allow_null=True) - tenant = TenantSerializer(nested=True, required=False, allow_null=True) - status = ChoiceField(choices=IPAddressStatusChoices, required=False) - role = ChoiceField(choices=IPAddressRoleChoices, allow_blank=True, required=False) - assigned_object_type = ContentTypeField( - queryset=ContentType.objects.filter(IPADDRESS_ASSIGNMENT_MODELS), - required=False, - allow_null=True - ) - assigned_object = serializers.SerializerMethodField(read_only=True) - nat_inside = NestedIPAddressSerializer(required=False, allow_null=True) - nat_outside = NestedIPAddressSerializer(many=True, read_only=True) - - class Meta: - model = IPAddress - fields = [ - 'id', 'url', 'display', 'family', 'address', 'vrf', 'tenant', 'status', 'role', 'assigned_object_type', - 'assigned_object_id', 'assigned_object', 'nat_inside', 'nat_outside', 'dns_name', 'description', 'comments', - 'tags', 'custom_fields', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'family', 'address', 'description') - - @extend_schema_field(serializers.JSONField(allow_null=True)) - def get_assigned_object(self, obj): - if obj.assigned_object is None: - return None - serializer = get_serializer_for_model(obj.assigned_object, prefix=NESTED_SERIALIZER_PREFIX) - context = {'request': self.context['request']} - return serializer(obj.assigned_object, context=context).data - - -class AvailableIPSerializer(serializers.Serializer): - """ - Representation of an IP address which does not exist in the database. - """ - family = serializers.IntegerField(read_only=True) - address = serializers.CharField(read_only=True) - vrf = VRFSerializer(nested=True, read_only=True) - description = serializers.CharField(required=False) - - def to_representation(self, instance): - if self.context.get('vrf'): - vrf = VRFSerializer(self.context['vrf'], nested=True, context={'request': self.context['request']}).data - else: - vrf = None - return { - 'family': self.context['parent'].family, - 'address': f"{instance}/{self.context['parent'].mask_length}", - 'vrf': vrf, - } - - -# -# Services -# - -class ServiceTemplateSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='ipam-api:servicetemplate-detail') - protocol = ChoiceField(choices=ServiceProtocolChoices, required=False) - - class Meta: - model = ServiceTemplate - fields = [ - 'id', 'url', 'display', 'name', 'protocol', 'ports', 'description', 'comments', 'tags', 'custom_fields', - 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'protocol', 'ports', 'description') - - -class ServiceSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='ipam-api:service-detail') - device = DeviceSerializer(nested=True, required=False, allow_null=True) - virtual_machine = VirtualMachineSerializer(nested=True, required=False, allow_null=True) - protocol = ChoiceField(choices=ServiceProtocolChoices, required=False) - ipaddresses = SerializedPKRelatedField( - queryset=IPAddress.objects.all(), - serializer=NestedIPAddressSerializer, - required=False, - many=True - ) - - class Meta: - model = Service - fields = [ - 'id', 'url', 'display', 'device', 'virtual_machine', 'name', 'protocol', 'ports', 'ipaddresses', - 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'protocol', 'ports', 'description') diff --git a/netbox/ipam/api/serializers_/__init__.py b/netbox/ipam/api/serializers_/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/ipam/api/serializers_/asns.py b/netbox/ipam/api/serializers_/asns.py new file mode 100644 index 000000000..9a8ab5b00 --- /dev/null +++ b/netbox/ipam/api/serializers_/asns.py @@ -0,0 +1,78 @@ +from rest_framework import serializers + +from ipam.models import ASN, ASNRange, RIR +from netbox.api.fields import RelatedObjectCountField +from netbox.api.serializers import NetBoxModelSerializer +from tenancy.api.serializers_.tenants import TenantSerializer + +__all__ = ( + 'ASNRangeSerializer', + 'ASNSerializer', + 'AvailableASNSerializer', + 'RIRSerializer', +) + + +class RIRSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail') + + # Related object counts + aggregate_count = RelatedObjectCountField('aggregates') + + class Meta: + model = RIR + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'is_private', 'description', 'tags', 'custom_fields', 'created', + 'last_updated', 'aggregate_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'aggregate_count') + + +class ASNRangeSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asnrange-detail') + rir = RIRSerializer(nested=True) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) + asn_count = serializers.IntegerField(read_only=True) + + class Meta: + model = ASNRange + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'rir', 'start', 'end', 'tenant', 'description', 'tags', + 'custom_fields', 'created', 'last_updated', 'asn_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') + + +class ASNSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asn-detail') + rir = RIRSerializer(nested=True, required=False, allow_null=True) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) + + # Related object counts + site_count = RelatedObjectCountField('sites') + provider_count = RelatedObjectCountField('providers') + + class Meta: + model = ASN + fields = [ + 'id', 'url', 'display', 'asn', 'rir', 'tenant', 'description', 'comments', 'tags', 'custom_fields', + 'created', 'last_updated', 'site_count', 'provider_count', + ] + brief_fields = ('id', 'url', 'display', 'asn', 'description') + + +class AvailableASNSerializer(serializers.Serializer): + """ + Representation of an ASN which does not exist in the database. + """ + asn = serializers.IntegerField(read_only=True) + description = serializers.CharField(required=False) + + def to_representation(self, asn): + rir = RIRSerializer(self.context['range'].rir, nested=True, context={ + 'request': self.context['request'] + }).data + return { + 'rir': rir, + 'asn': asn, + } diff --git a/netbox/ipam/api/serializers_/fhrpgroups.py b/netbox/ipam/api/serializers_/fhrpgroups.py new file mode 100644 index 000000000..1fae8f2e9 --- /dev/null +++ b/netbox/ipam/api/serializers_/fhrpgroups.py @@ -0,0 +1,53 @@ +from django.contrib.contenttypes.models import ContentType +from drf_spectacular.utils import extend_schema_field +from rest_framework import serializers + +from ipam.models import FHRPGroup, FHRPGroupAssignment +from netbox.api.fields import ContentTypeField +from netbox.api.serializers import NetBoxModelSerializer +from netbox.constants import NESTED_SERIALIZER_PREFIX +from utilities.api import get_serializer_for_model +from ..nested_serializers import * + +__all__ = ( + 'FHRPGroupAssignmentSerializer', + 'FHRPGroupSerializer', +) + + +class FHRPGroupSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:fhrpgroup-detail') + ip_addresses = NestedIPAddressSerializer(many=True, read_only=True) + + class Meta: + model = FHRPGroup + fields = [ + 'id', 'name', 'url', 'display', 'protocol', 'group_id', 'auth_type', 'auth_key', 'description', 'comments', + 'tags', 'custom_fields', 'created', 'last_updated', 'ip_addresses', + ] + brief_fields = ('id', 'url', 'display', 'protocol', 'group_id', 'description') + + +class FHRPGroupAssignmentSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:fhrpgroupassignment-detail') + group = FHRPGroupSerializer(nested=True) + interface_type = ContentTypeField( + queryset=ContentType.objects.all() + ) + interface = serializers.SerializerMethodField(read_only=True) + + class Meta: + model = FHRPGroupAssignment + fields = [ + 'id', 'url', 'display', 'group', 'interface_type', 'interface_id', 'interface', 'priority', 'created', + 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'group', 'interface_type', 'interface_id', 'priority') + + @extend_schema_field(serializers.JSONField(allow_null=True)) + def get_interface(self, obj): + if obj.interface is None: + return None + serializer = get_serializer_for_model(obj.interface, prefix=NESTED_SERIALIZER_PREFIX) + context = {'request': self.context['request']} + return serializer(obj.interface, context=context).data diff --git a/netbox/ipam/api/serializers_/ip.py b/netbox/ipam/api/serializers_/ip.py new file mode 100644 index 000000000..33dcca326 --- /dev/null +++ b/netbox/ipam/api/serializers_/ip.py @@ -0,0 +1,200 @@ +from django.contrib.contenttypes.models import ContentType +from drf_spectacular.utils import extend_schema_field +from rest_framework import serializers + +from dcim.api.serializers_.sites import SiteSerializer +from ipam.choices import * +from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS +from ipam.models import Aggregate, IPAddress, IPRange, Prefix +from netbox.api.fields import ChoiceField, ContentTypeField +from netbox.api.serializers import NetBoxModelSerializer +from netbox.constants import NESTED_SERIALIZER_PREFIX +from tenancy.api.serializers_.tenants import TenantSerializer +from utilities.api import get_serializer_for_model +from ..field_serializers import IPAddressField, IPNetworkField +from ..nested_serializers import * + +from .asns import RIRSerializer +from .vrfs import VRFSerializer +from .roles import RoleSerializer +from .vlans import VLANSerializer + +__all__ = ( + 'AggregateSerializer', + 'AvailableIPSerializer', + 'AvailablePrefixSerializer', + 'IPAddressSerializer', + 'IPRangeSerializer', + 'PrefixLengthSerializer', + 'PrefixSerializer', +) + + +class AggregateSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:aggregate-detail') + family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True) + rir = RIRSerializer(nested=True) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) + prefix = IPNetworkField() + + class Meta: + model = Aggregate + fields = [ + 'id', 'url', 'display', 'family', 'prefix', 'rir', 'tenant', 'date_added', 'description', 'comments', + 'tags', 'custom_fields', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'family', 'prefix', 'description') + + +class PrefixSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:prefix-detail') + family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True) + site = SiteSerializer(nested=True, required=False, allow_null=True) + vrf = VRFSerializer(nested=True, required=False, allow_null=True) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) + vlan = VLANSerializer(nested=True, required=False, allow_null=True) + status = ChoiceField(choices=PrefixStatusChoices, required=False) + role = RoleSerializer(nested=True, required=False, allow_null=True) + children = serializers.IntegerField(read_only=True) + _depth = serializers.IntegerField(read_only=True) + prefix = IPNetworkField() + + class Meta: + model = Prefix + fields = [ + 'id', 'url', 'display', 'family', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', + 'mark_utilized', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'children', + '_depth', + ] + brief_fields = ('id', 'url', 'display', 'family', 'prefix', 'description', '_depth') + + +class PrefixLengthSerializer(serializers.Serializer): + + prefix_length = serializers.IntegerField() + + def to_internal_value(self, data): + requested_prefix = data.get('prefix_length') + if requested_prefix is None: + raise serializers.ValidationError({ + 'prefix_length': 'this field can not be missing' + }) + if not isinstance(requested_prefix, int): + raise serializers.ValidationError({ + 'prefix_length': 'this field must be int type' + }) + + prefix = self.context.get('prefix') + if prefix.family == 4 and requested_prefix > 32: + raise serializers.ValidationError({ + 'prefix_length': 'Invalid prefix length ({}) for IPv4'.format(requested_prefix) + }) + elif prefix.family == 6 and requested_prefix > 128: + raise serializers.ValidationError({ + 'prefix_length': 'Invalid prefix length ({}) for IPv6'.format(requested_prefix) + }) + return data + + +class AvailablePrefixSerializer(serializers.Serializer): + """ + Representation of a prefix which does not exist in the database. + """ + family = serializers.IntegerField(read_only=True) + prefix = serializers.CharField(read_only=True) + vrf = VRFSerializer(nested=True, read_only=True) + + def to_representation(self, instance): + if self.context.get('vrf'): + vrf = VRFSerializer(self.context['vrf'], nested=True, context={'request': self.context['request']}).data + else: + vrf = None + return { + 'family': instance.version, + 'prefix': str(instance), + 'vrf': vrf, + } + + +# +# IP ranges +# + +class IPRangeSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:iprange-detail') + family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True) + start_address = IPAddressField() + end_address = IPAddressField() + vrf = VRFSerializer(nested=True, required=False, allow_null=True) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) + status = ChoiceField(choices=IPRangeStatusChoices, required=False) + role = RoleSerializer(nested=True, required=False, allow_null=True) + + class Meta: + model = IPRange + fields = [ + 'id', 'url', 'display', 'family', 'start_address', 'end_address', 'size', 'vrf', 'tenant', 'status', 'role', + 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', + 'mark_utilized', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'family', 'start_address', 'end_address', 'description') + + +# +# IP addresses +# + +class IPAddressSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail') + family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True) + address = IPAddressField() + vrf = VRFSerializer(nested=True, required=False, allow_null=True) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) + status = ChoiceField(choices=IPAddressStatusChoices, required=False) + role = ChoiceField(choices=IPAddressRoleChoices, allow_blank=True, required=False) + assigned_object_type = ContentTypeField( + queryset=ContentType.objects.filter(IPADDRESS_ASSIGNMENT_MODELS), + required=False, + allow_null=True + ) + assigned_object = serializers.SerializerMethodField(read_only=True) + nat_inside = NestedIPAddressSerializer(required=False, allow_null=True) + nat_outside = NestedIPAddressSerializer(many=True, read_only=True) + + class Meta: + model = IPAddress + fields = [ + 'id', 'url', 'display', 'family', 'address', 'vrf', 'tenant', 'status', 'role', 'assigned_object_type', + 'assigned_object_id', 'assigned_object', 'nat_inside', 'nat_outside', 'dns_name', 'description', 'comments', + 'tags', 'custom_fields', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'family', 'address', 'description') + + @extend_schema_field(serializers.JSONField(allow_null=True)) + def get_assigned_object(self, obj): + if obj.assigned_object is None: + return None + serializer = get_serializer_for_model(obj.assigned_object, prefix=NESTED_SERIALIZER_PREFIX) + context = {'request': self.context['request']} + return serializer(obj.assigned_object, context=context).data + + +class AvailableIPSerializer(serializers.Serializer): + """ + Representation of an IP address which does not exist in the database. + """ + family = serializers.IntegerField(read_only=True) + address = serializers.CharField(read_only=True) + vrf = VRFSerializer(nested=True, read_only=True) + description = serializers.CharField(required=False) + + def to_representation(self, instance): + if self.context.get('vrf'): + vrf = VRFSerializer(self.context['vrf'], nested=True, context={'request': self.context['request']}).data + else: + vrf = None + return { + 'family': self.context['parent'].family, + 'address': f"{instance}/{self.context['parent'].mask_length}", + 'vrf': vrf, + } diff --git a/netbox/ipam/api/serializers_/roles.py b/netbox/ipam/api/serializers_/roles.py new file mode 100644 index 000000000..8208b8074 --- /dev/null +++ b/netbox/ipam/api/serializers_/roles.py @@ -0,0 +1,25 @@ +from rest_framework import serializers + +from ipam.models import Role +from netbox.api.fields import RelatedObjectCountField +from netbox.api.serializers import NetBoxModelSerializer + +__all__ = ( + 'RoleSerializer', +) + + +class RoleSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:role-detail') + + # Related object counts + prefix_count = RelatedObjectCountField('prefixes') + vlan_count = RelatedObjectCountField('vlans') + + class Meta: + model = Role + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'weight', 'description', 'tags', 'custom_fields', 'created', + 'last_updated', 'prefix_count', 'vlan_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'prefix_count', 'vlan_count') diff --git a/netbox/ipam/api/serializers_/services.py b/netbox/ipam/api/serializers_/services.py new file mode 100644 index 000000000..8904f819c --- /dev/null +++ b/netbox/ipam/api/serializers_/services.py @@ -0,0 +1,48 @@ +from rest_framework import serializers + +from dcim.api.serializers_.devices import DeviceSerializer +from ipam.choices import * +from ipam.models import IPAddress, Service, ServiceTemplate +from netbox.api.fields import ChoiceField, SerializedPKRelatedField +from netbox.api.serializers import NetBoxModelSerializer +from virtualization.api.serializers_.virtualmachines import VirtualMachineSerializer +from ..nested_serializers import * + +__all__ = ( + 'ServiceSerializer', + 'ServiceTemplateSerializer', +) + + +class ServiceTemplateSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:servicetemplate-detail') + protocol = ChoiceField(choices=ServiceProtocolChoices, required=False) + + class Meta: + model = ServiceTemplate + fields = [ + 'id', 'url', 'display', 'name', 'protocol', 'ports', 'description', 'comments', 'tags', 'custom_fields', + 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'protocol', 'ports', 'description') + + +class ServiceSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:service-detail') + device = DeviceSerializer(nested=True, required=False, allow_null=True) + virtual_machine = VirtualMachineSerializer(nested=True, required=False, allow_null=True) + protocol = ChoiceField(choices=ServiceProtocolChoices, required=False) + ipaddresses = SerializedPKRelatedField( + queryset=IPAddress.objects.all(), + serializer=NestedIPAddressSerializer, + required=False, + many=True + ) + + class Meta: + model = Service + fields = [ + 'id', 'url', 'display', 'device', 'virtual_machine', 'name', 'protocol', 'ports', 'ipaddresses', + 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'protocol', 'ports', 'description') diff --git a/netbox/ipam/api/serializers_/vlans.py b/netbox/ipam/api/serializers_/vlans.py new file mode 100644 index 000000000..c92e09a70 --- /dev/null +++ b/netbox/ipam/api/serializers_/vlans.py @@ -0,0 +1,115 @@ +from django.contrib.contenttypes.models import ContentType +from drf_spectacular.utils import extend_schema_field +from rest_framework import serializers + +from dcim.api.serializers_.sites import SiteSerializer +from ipam.choices import * +from ipam.constants import VLANGROUP_SCOPE_TYPES +from ipam.models import VLAN, VLANGroup +from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField +from netbox.api.serializers import NetBoxModelSerializer +from netbox.constants import NESTED_SERIALIZER_PREFIX +from tenancy.api.serializers_.tenants import TenantSerializer +from utilities.api import get_serializer_for_model +from vpn.api.serializers_.l2vpn import L2VPNTerminationSerializer + +from .roles import RoleSerializer + +__all__ = ( + 'AvailableVLANSerializer', + 'CreateAvailableVLANSerializer', + 'VLANGroupSerializer', + 'VLANSerializer', +) + + +class VLANGroupSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail') + scope_type = ContentTypeField( + queryset=ContentType.objects.filter( + model__in=VLANGROUP_SCOPE_TYPES + ), + allow_null=True, + required=False, + default=None + ) + scope_id = serializers.IntegerField(allow_null=True, required=False, default=None) + scope = serializers.SerializerMethodField(read_only=True) + utilization = serializers.CharField(read_only=True) + + # Related object counts + vlan_count = RelatedObjectCountField('vlans') + + class Meta: + model = VLANGroup + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'scope_type', 'scope_id', 'scope', 'min_vid', 'max_vid', + 'description', 'tags', 'custom_fields', 'created', 'last_updated', 'vlan_count', 'utilization' + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'vlan_count') + validators = [] + + @extend_schema_field(serializers.JSONField(allow_null=True)) + def get_scope(self, obj): + if obj.scope_id is None: + return None + serializer = get_serializer_for_model(obj.scope, prefix=NESTED_SERIALIZER_PREFIX) + context = {'request': self.context['request']} + + return serializer(obj.scope, context=context).data + + +class VLANSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail') + site = SiteSerializer(nested=True, required=False, allow_null=True) + group = VLANGroupSerializer(nested=True, required=False, allow_null=True, default=None) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) + status = ChoiceField(choices=VLANStatusChoices, required=False) + role = RoleSerializer(nested=True, required=False, allow_null=True) + l2vpn_termination = L2VPNTerminationSerializer(nested=True, read_only=True, allow_null=True) + + # Related object counts + prefix_count = RelatedObjectCountField('prefixes') + + class Meta: + model = VLAN + fields = [ + 'id', 'url', 'display', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', + 'comments', 'l2vpn_termination', 'tags', 'custom_fields', 'created', 'last_updated', 'prefix_count', + ] + brief_fields = ('id', 'url', 'display', 'vid', 'name', 'description') + + +class AvailableVLANSerializer(serializers.Serializer): + """ + Representation of a VLAN which does not exist in the database. + """ + vid = serializers.IntegerField(read_only=True) + group = VLANGroupSerializer(nested=True, read_only=True) + + def to_representation(self, instance): + return { + 'vid': instance, + 'group': VLANGroupSerializer( + self.context['group'], + nested=True, + context={'request': self.context['request']} + ).data, + } + + +class CreateAvailableVLANSerializer(NetBoxModelSerializer): + site = SiteSerializer(nested=True, required=False, allow_null=True) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) + status = ChoiceField(choices=VLANStatusChoices, required=False) + role = RoleSerializer(nested=True, required=False, allow_null=True) + + class Meta: + model = VLAN + fields = [ + 'name', 'site', 'tenant', 'status', 'role', 'description', 'tags', 'custom_fields', + ] + + def validate(self, data): + # Bypass model validation since we don't have a VID yet + return data diff --git a/netbox/ipam/api/serializers_/vrfs.py b/netbox/ipam/api/serializers_/vrfs.py new file mode 100644 index 000000000..5e5d0e725 --- /dev/null +++ b/netbox/ipam/api/serializers_/vrfs.py @@ -0,0 +1,55 @@ +from rest_framework import serializers + +from ipam.models import RouteTarget, VRF +from netbox.api.fields import RelatedObjectCountField, SerializedPKRelatedField +from netbox.api.serializers import NetBoxModelSerializer +from tenancy.api.serializers_.tenants import TenantSerializer +from ..nested_serializers import * + +__all__ = ( + 'RouteTargetSerializer', + 'VRFSerializer', +) + + +class VRFSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vrf-detail') + tenant = TenantSerializer(nested=True, required=False, allow_null=True) + import_targets = SerializedPKRelatedField( + queryset=RouteTarget.objects.all(), + serializer=NestedRouteTargetSerializer, + required=False, + many=True + ) + export_targets = SerializedPKRelatedField( + queryset=RouteTarget.objects.all(), + serializer=NestedRouteTargetSerializer, + required=False, + many=True + ) + + # Related object counts + ipaddress_count = RelatedObjectCountField('ip_addresses') + prefix_count = RelatedObjectCountField('prefixes') + + class Meta: + model = VRF + fields = [ + 'id', 'url', 'display', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'comments', + 'import_targets', 'export_targets', 'tags', 'custom_fields', 'created', 'last_updated', 'ipaddress_count', + 'prefix_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'rd', 'description', 'prefix_count') + + +class RouteTargetSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:routetarget-detail') + tenant = TenantSerializer(nested=True, required=False, allow_null=True) + + class Meta: + model = RouteTarget + fields = [ + 'id', 'url', 'display', 'name', 'tenant', 'description', 'comments', 'tags', 'custom_fields', 'created', + 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') diff --git a/netbox/tenancy/api/serializers.py b/netbox/tenancy/api/serializers.py index 103eb492b..642397733 100644 --- a/netbox/tenancy/api/serializers.py +++ b/netbox/tenancy/api/serializers.py @@ -1,123 +1,3 @@ -from django.contrib.auth.models import ContentType -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, RelatedObjectCountField -from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer -from netbox.constants import NESTED_SERIALIZER_PREFIX -from tenancy.choices import ContactPriorityChoices -from tenancy.models import * -from utilities.api import get_serializer_for_model +from .serializers_.tenants import * +from .serializers_.contacts import * from .nested_serializers import * - - -# -# Tenants -# - -class TenantGroupSerializer(NestedGroupModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenantgroup-detail') - parent = NestedTenantGroupSerializer(required=False, allow_null=True) - tenant_count = serializers.IntegerField(read_only=True) - - class Meta: - model = TenantGroup - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created', - 'last_updated', 'tenant_count', '_depth', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'tenant_count', '_depth') - - -class TenantSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenant-detail') - group = TenantGroupSerializer(nested=True, required=False, allow_null=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 - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'group', 'description', 'comments', 'tags', 'custom_fields', - 'created', 'last_updated', 'circuit_count', 'device_count', 'ipaddress_count', 'prefix_count', 'rack_count', - 'site_count', 'virtualmachine_count', 'vlan_count', 'vrf_count', 'cluster_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description') - - -# -# Contacts -# - -class ContactGroupSerializer(NestedGroupModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactgroup-detail') - parent = NestedContactGroupSerializer(required=False, allow_null=True, default=None) - contact_count = serializers.IntegerField(read_only=True) - - class Meta: - model = ContactGroup - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created', - 'last_updated', 'contact_count', '_depth', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'contact_count', '_depth') - - -class ContactRoleSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactrole-detail') - - class Meta: - model = ContactRole - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description') - - -class ContactSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contact-detail') - group = ContactGroupSerializer(nested=True, required=False, allow_null=True, default=None) - - class Meta: - model = Contact - fields = [ - 'id', 'url', 'display', 'group', 'name', 'title', 'phone', 'email', 'address', 'link', 'description', - 'comments', 'tags', 'custom_fields', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -class ContactAssignmentSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactassignment-detail') - content_type = ContentTypeField( - queryset=ContentType.objects.all() - ) - object = serializers.SerializerMethodField(read_only=True) - contact = ContactSerializer(nested=True) - role = ContactRoleSerializer(nested=True, required=False, allow_null=True) - priority = ChoiceField(choices=ContactPriorityChoices, allow_blank=True, required=False, default=lambda: '') - - class Meta: - model = ContactAssignment - fields = [ - 'id', 'url', 'display', 'content_type', 'object_id', 'object', 'contact', 'role', 'priority', 'tags', - 'custom_fields', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'contact', 'role', 'priority') - - @extend_schema_field(OpenApiTypes.OBJECT) - def get_object(self, instance): - serializer = get_serializer_for_model(instance.content_type.model_class(), prefix=NESTED_SERIALIZER_PREFIX) - context = {'request': self.context['request']} - return serializer(instance.object, context=context).data diff --git a/netbox/tenancy/api/serializers_/__init__.py b/netbox/tenancy/api/serializers_/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/tenancy/api/serializers_/contacts.py b/netbox/tenancy/api/serializers_/contacts.py new file mode 100644 index 000000000..ddac48815 --- /dev/null +++ b/netbox/tenancy/api/serializers_/contacts.py @@ -0,0 +1,82 @@ +from django.contrib.auth.models import ContentType +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.serializers import NestedGroupModelSerializer, NetBoxModelSerializer +from netbox.constants import NESTED_SERIALIZER_PREFIX +from tenancy.choices import ContactPriorityChoices +from tenancy.models import ContactAssignment, Contact, ContactGroup, ContactRole +from utilities.api import get_serializer_for_model +from ..nested_serializers import * + +__all__ = ( + 'ContactAssignmentSerializer', + 'ContactGroupSerializer', + 'ContactRoleSerializer', + 'ContactSerializer', +) + + +class ContactGroupSerializer(NestedGroupModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactgroup-detail') + parent = NestedContactGroupSerializer(required=False, allow_null=True, default=None) + contact_count = serializers.IntegerField(read_only=True) + + class Meta: + model = ContactGroup + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created', + 'last_updated', 'contact_count', '_depth', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'contact_count', '_depth') + + +class ContactRoleSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactrole-detail') + + class Meta: + model = ContactRole + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description') + + +class ContactSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contact-detail') + group = ContactGroupSerializer(nested=True, required=False, allow_null=True, default=None) + + class Meta: + model = Contact + fields = [ + 'id', 'url', 'display', 'group', 'name', 'title', 'phone', 'email', 'address', 'link', 'description', + 'comments', 'tags', 'custom_fields', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') + + +class ContactAssignmentSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactassignment-detail') + content_type = ContentTypeField( + queryset=ContentType.objects.all() + ) + object = serializers.SerializerMethodField(read_only=True) + contact = ContactSerializer(nested=True) + role = ContactRoleSerializer(nested=True, required=False, allow_null=True) + priority = ChoiceField(choices=ContactPriorityChoices, allow_blank=True, required=False, default=lambda: '') + + class Meta: + model = ContactAssignment + fields = [ + 'id', 'url', 'display', 'content_type', 'object_id', 'object', 'contact', 'role', 'priority', 'tags', + 'custom_fields', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'contact', 'role', 'priority') + + @extend_schema_field(OpenApiTypes.OBJECT) + def get_object(self, instance): + serializer = get_serializer_for_model(instance.content_type.model_class(), prefix=NESTED_SERIALIZER_PREFIX) + context = {'request': self.context['request']} + return serializer(instance.object, context=context).data diff --git a/netbox/tenancy/api/serializers_/tenants.py b/netbox/tenancy/api/serializers_/tenants.py new file mode 100644 index 000000000..3ee238c90 --- /dev/null +++ b/netbox/tenancy/api/serializers_/tenants.py @@ -0,0 +1,51 @@ +from rest_framework import serializers + +from netbox.api.fields import RelatedObjectCountField +from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer +from tenancy.models import Tenant, TenantGroup +from ..nested_serializers import * + +__all__ = ( + 'TenantGroupSerializer', + 'TenantSerializer', +) + + +class TenantGroupSerializer(NestedGroupModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenantgroup-detail') + parent = NestedTenantGroupSerializer(required=False, allow_null=True) + tenant_count = serializers.IntegerField(read_only=True) + + class Meta: + model = TenantGroup + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created', + 'last_updated', 'tenant_count', '_depth', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'tenant_count', '_depth') + + +class TenantSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenant-detail') + group = TenantGroupSerializer(nested=True, required=False, allow_null=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 + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'group', 'description', 'comments', 'tags', 'custom_fields', + 'created', 'last_updated', 'circuit_count', 'device_count', 'ipaddress_count', 'prefix_count', 'rack_count', + 'site_count', 'virtualmachine_count', 'vlan_count', 'vrf_count', 'cluster_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description') diff --git a/netbox/users/api/serializers.py b/netbox/users/api/serializers.py index 49150f9c9..ef3f66a7d 100644 --- a/netbox/users/api/serializers.py +++ b/netbox/users/api/serializers.py @@ -1,190 +1,4 @@ -from django.conf import settings -from django.contrib.auth import authenticate -from django.contrib.auth import get_user_model -from django.contrib.auth.models import Group -from django.contrib.contenttypes.models import ContentType -from drf_spectacular.utils import extend_schema_field -from drf_spectacular.types import OpenApiTypes -from rest_framework import serializers -from rest_framework.exceptions import AuthenticationFailed, PermissionDenied - -from netbox.api.fields import ContentTypeField, IPNetworkSerializer, SerializedPKRelatedField -from netbox.api.serializers import ValidatedModelSerializer -from users.models import ObjectPermission, Token +from .serializers_.users import * +from .serializers_.permissions import * +from .serializers_.tokens import * from .nested_serializers import * - - -__all__ = ( - 'GroupSerializer', - 'ObjectPermissionSerializer', - 'TokenSerializer', - 'UserSerializer', -) - - -class UserSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='users-api:user-detail') - groups = SerializedPKRelatedField( - queryset=Group.objects.all(), - serializer=NestedGroupSerializer, - required=False, - many=True - ) - - class Meta: - model = get_user_model() - fields = ( - 'id', 'url', 'display', 'username', 'password', 'first_name', 'last_name', 'email', 'is_staff', 'is_active', - 'date_joined', 'last_login', 'groups', - ) - brief_fields = ('id', 'url', 'display', 'username') - extra_kwargs = { - 'password': {'write_only': True} - } - - def create(self, validated_data): - """ - Extract the password from validated data and set it separately to ensure proper hash generation. - """ - password = validated_data.pop('password') - user = super().create(validated_data) - user.set_password(password) - user.save() - - return user - - def update(self, instance, validated_data): - """ - Ensure proper updated password hash generation. - """ - password = validated_data.pop('password', None) - if password is not None: - instance.set_password(password) - - return super().update(instance, validated_data) - - @extend_schema_field(OpenApiTypes.STR) - def get_display(self, obj): - if full_name := obj.get_full_name(): - return f"{obj.username} ({full_name})" - return obj.username - - -class GroupSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='users-api:group-detail') - user_count = serializers.IntegerField(read_only=True) - - class Meta: - model = Group - fields = ('id', 'url', 'display', 'name', 'user_count') - brief_fields = ('id', 'url', 'display', 'name') - - -class TokenSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='users-api:token-detail') - key = serializers.CharField( - min_length=40, - max_length=40, - allow_blank=True, - required=False, - write_only=not settings.ALLOW_TOKEN_RETRIEVAL - ) - user = UserSerializer(nested=True) - allowed_ips = serializers.ListField( - child=IPNetworkSerializer(), - required=False, - allow_empty=True, - default=[] - ) - - class Meta: - model = Token - fields = ( - 'id', 'url', 'display', 'user', 'created', 'expires', 'last_used', 'key', 'write_enabled', 'description', - 'allowed_ips', - ) - brief_fields = ('id', 'url', 'display', 'key', 'write_enabled', 'description') - - def to_internal_value(self, data): - if 'key' not in data: - data['key'] = Token.generate_key() - return super().to_internal_value(data) - - def validate(self, data): - - # If the Token is being created on behalf of another user, enforce the grant_token permission. - request = self.context.get('request') - token_user = data.get('user') - if token_user and token_user != request.user and not request.user.has_perm('users.grant_token'): - raise PermissionDenied("This user does not have permission to create tokens for other users.") - - return super().validate(data) - - -class TokenProvisionSerializer(TokenSerializer): - user = UserSerializer( - nested=True, - read_only=True - ) - username = serializers.CharField( - write_only=True - ) - password = serializers.CharField( - write_only=True - ) - last_used = serializers.DateTimeField( - read_only=True - ) - key = serializers.CharField( - read_only=True - ) - - class Meta: - model = Token - fields = ( - 'id', 'url', 'display', 'user', 'created', 'expires', 'last_used', 'key', 'write_enabled', 'description', - 'allowed_ips', 'username', 'password', - ) - - def validate(self, data): - # Validate the username and password - username = data.pop('username') - password = data.pop('password') - user = authenticate(request=self.context.get('request'), username=username, password=password) - if user is None: - raise AuthenticationFailed("Invalid username/password") - - # Inject the user into the validated data - data['user'] = user - - return data - - -class ObjectPermissionSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='users-api:objectpermission-detail') - object_types = ContentTypeField( - queryset=ContentType.objects.all(), - many=True - ) - groups = SerializedPKRelatedField( - queryset=Group.objects.all(), - serializer=NestedGroupSerializer, - required=False, - many=True - ) - users = SerializedPKRelatedField( - queryset=get_user_model().objects.all(), - serializer=NestedUserSerializer, - required=False, - many=True - ) - - class Meta: - model = ObjectPermission - fields = ( - 'id', 'url', 'display', 'name', 'description', 'enabled', 'object_types', 'groups', 'users', 'actions', - 'constraints', - ) - brief_fields = ( - 'id', 'url', 'display', 'name', 'description', 'enabled', 'object_types', 'groups', 'users', 'actions', - ) diff --git a/netbox/users/api/serializers_/__init__.py b/netbox/users/api/serializers_/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/users/api/serializers_/permissions.py b/netbox/users/api/serializers_/permissions.py new file mode 100644 index 000000000..a04c032f1 --- /dev/null +++ b/netbox/users/api/serializers_/permissions.py @@ -0,0 +1,43 @@ +from django.contrib.auth import get_user_model +from django.contrib.auth.models import Group +from django.contrib.contenttypes.models import ContentType +from rest_framework import serializers + +from netbox.api.fields import ContentTypeField, SerializedPKRelatedField +from netbox.api.serializers import ValidatedModelSerializer +from users.models import ObjectPermission +from ..nested_serializers import * + +__all__ = ( + 'ObjectPermissionSerializer', +) + + +class ObjectPermissionSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='users-api:objectpermission-detail') + object_types = ContentTypeField( + queryset=ContentType.objects.all(), + many=True + ) + groups = SerializedPKRelatedField( + queryset=Group.objects.all(), + serializer=NestedGroupSerializer, + required=False, + many=True + ) + users = SerializedPKRelatedField( + queryset=get_user_model().objects.all(), + serializer=NestedUserSerializer, + required=False, + many=True + ) + + class Meta: + model = ObjectPermission + fields = ( + 'id', 'url', 'display', 'name', 'description', 'enabled', 'object_types', 'groups', 'users', 'actions', + 'constraints', + ) + brief_fields = ( + 'id', 'url', 'display', 'name', 'description', 'enabled', 'object_types', 'groups', 'users', 'actions', + ) diff --git a/netbox/users/api/serializers_/tokens.py b/netbox/users/api/serializers_/tokens.py new file mode 100644 index 000000000..65e1e7111 --- /dev/null +++ b/netbox/users/api/serializers_/tokens.py @@ -0,0 +1,94 @@ +from django.conf import settings +from django.contrib.auth import authenticate +from rest_framework import serializers +from rest_framework.exceptions import AuthenticationFailed, PermissionDenied + +from netbox.api.fields import IPNetworkSerializer +from netbox.api.serializers import ValidatedModelSerializer +from users.models import Token +from .users import * + +__all__ = ( + 'TokenProvisionSerializer', + 'TokenSerializer', +) + + +class TokenSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='users-api:token-detail') + key = serializers.CharField( + min_length=40, + max_length=40, + allow_blank=True, + required=False, + write_only=not settings.ALLOW_TOKEN_RETRIEVAL + ) + user = UserSerializer(nested=True) + allowed_ips = serializers.ListField( + child=IPNetworkSerializer(), + required=False, + allow_empty=True, + default=[] + ) + + class Meta: + model = Token + fields = ( + 'id', 'url', 'display', 'user', 'created', 'expires', 'last_used', 'key', 'write_enabled', 'description', + 'allowed_ips', + ) + brief_fields = ('id', 'url', 'display', 'key', 'write_enabled', 'description') + + def to_internal_value(self, data): + if 'key' not in data: + data['key'] = Token.generate_key() + return super().to_internal_value(data) + + def validate(self, data): + + # If the Token is being created on behalf of another user, enforce the grant_token permission. + request = self.context.get('request') + token_user = data.get('user') + if token_user and token_user != request.user and not request.user.has_perm('users.grant_token'): + raise PermissionDenied("This user does not have permission to create tokens for other users.") + + return super().validate(data) + + +class TokenProvisionSerializer(TokenSerializer): + user = UserSerializer( + nested=True, + read_only=True + ) + username = serializers.CharField( + write_only=True + ) + password = serializers.CharField( + write_only=True + ) + last_used = serializers.DateTimeField( + read_only=True + ) + key = serializers.CharField( + read_only=True + ) + + class Meta: + model = Token + fields = ( + 'id', 'url', 'display', 'user', 'created', 'expires', 'last_used', 'key', 'write_enabled', 'description', + 'allowed_ips', 'username', 'password', + ) + + def validate(self, data): + # Validate the username and password + username = data.pop('username') + password = data.pop('password') + user = authenticate(request=self.context.get('request'), username=username, password=password) + if user is None: + raise AuthenticationFailed("Invalid username/password") + + # Inject the user into the validated data + data['user'] = user + + return data diff --git a/netbox/users/api/serializers_/users.py b/netbox/users/api/serializers_/users.py new file mode 100644 index 000000000..40f3b3392 --- /dev/null +++ b/netbox/users/api/serializers_/users.py @@ -0,0 +1,72 @@ +from django.contrib.auth import get_user_model +from django.contrib.auth.models import Group +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import extend_schema_field +from rest_framework import serializers + +from netbox.api.fields import SerializedPKRelatedField +from netbox.api.serializers import ValidatedModelSerializer +from ..nested_serializers import * + +__all__ = ( + 'GroupSerializer', + 'UserSerializer', +) + + +class UserSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='users-api:user-detail') + groups = SerializedPKRelatedField( + queryset=Group.objects.all(), + serializer=NestedGroupSerializer, + required=False, + many=True + ) + + class Meta: + model = get_user_model() + fields = ( + 'id', 'url', 'display', 'username', 'password', 'first_name', 'last_name', 'email', 'is_staff', 'is_active', + 'date_joined', 'last_login', 'groups', + ) + brief_fields = ('id', 'url', 'display', 'username') + extra_kwargs = { + 'password': {'write_only': True} + } + + def create(self, validated_data): + """ + Extract the password from validated data and set it separately to ensure proper hash generation. + """ + password = validated_data.pop('password') + user = super().create(validated_data) + user.set_password(password) + user.save() + + return user + + def update(self, instance, validated_data): + """ + Ensure proper updated password hash generation. + """ + password = validated_data.pop('password', None) + if password is not None: + instance.set_password(password) + + return super().update(instance, validated_data) + + @extend_schema_field(OpenApiTypes.STR) + def get_display(self, obj): + if full_name := obj.get_full_name(): + return f"{obj.username} ({full_name})" + return obj.username + + +class GroupSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='users-api:group-detail') + user_count = serializers.IntegerField(read_only=True) + + class Meta: + model = Group + fields = ('id', 'url', 'display', 'name', 'user_count') + brief_fields = ('id', 'url', 'display', 'name') diff --git a/netbox/virtualization/api/serializers.py b/netbox/virtualization/api/serializers.py index 9f07f6f90..ad698fe2f 100644 --- a/netbox/virtualization/api/serializers.py +++ b/netbox/virtualization/api/serializers.py @@ -1,187 +1,3 @@ -from drf_spectacular.utils import extend_schema_field -from rest_framework import serializers - -from dcim.api.serializers import DeviceSerializer, DeviceRoleSerializer, PlatformSerializer, SiteSerializer -from dcim.choices import InterfaceModeChoices -from extras.api.serializers import ConfigTemplateSerializer -from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer, NestedVRFSerializer -from ipam.models import VLAN -from netbox.api.fields import ChoiceField, RelatedObjectCountField, SerializedPKRelatedField -from netbox.api.serializers import NetBoxModelSerializer -from tenancy.api.serializers import TenantSerializer -from virtualization.choices import * -from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualDisk, VirtualMachine, VMInterface -from vpn.api.nested_serializers import NestedL2VPNTerminationSerializer +from .serializers_.clusters import * +from .serializers_.virtualmachines import * from .nested_serializers import * - - -# -# Clusters -# - -class ClusterTypeSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustertype-detail') - - # Related object counts - cluster_count = RelatedObjectCountField('clusters') - - class Meta: - model = ClusterType - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated', - 'cluster_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'cluster_count') - - -class ClusterGroupSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustergroup-detail') - - # Related object counts - cluster_count = RelatedObjectCountField('clusters') - - class Meta: - model = ClusterGroup - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated', - 'cluster_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'cluster_count') - - -class ClusterSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail') - type = ClusterTypeSerializer(nested=True) - group = ClusterGroupSerializer(nested=True, required=False, allow_null=True, default=None) - status = ChoiceField(choices=ClusterStatusChoices, required=False) - tenant = TenantSerializer(nested=True, required=False, allow_null=True) - site = SiteSerializer(nested=True, required=False, allow_null=True, default=None) - - # Related object counts - device_count = RelatedObjectCountField('devices') - virtualmachine_count = RelatedObjectCountField('virtual_machines') - - class Meta: - model = Cluster - fields = [ - 'id', 'url', 'display', 'name', 'type', 'group', 'status', 'tenant', 'site', 'description', 'comments', - 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description', 'virtualmachine_count') - - -# -# Virtual machines -# - -class VirtualMachineSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:virtualmachine-detail') - status = ChoiceField(choices=VirtualMachineStatusChoices, required=False) - site = SiteSerializer(nested=True, required=False, allow_null=True) - cluster = ClusterSerializer(nested=True, required=False, allow_null=True) - device = DeviceSerializer(nested=True, required=False, allow_null=True) - role = DeviceRoleSerializer(nested=True, required=False, allow_null=True) - tenant = TenantSerializer(nested=True, required=False, allow_null=True) - platform = PlatformSerializer(nested=True, required=False, allow_null=True) - primary_ip = NestedIPAddressSerializer(read_only=True) - primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True) - primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True) - config_template = ConfigTemplateSerializer(nested=True, required=False, allow_null=True, default=None) - - # Counter fields - interface_count = serializers.IntegerField(read_only=True) - virtual_disk_count = serializers.IntegerField(read_only=True) - - class Meta: - model = VirtualMachine - fields = [ - 'id', 'url', 'display', 'name', 'status', 'site', 'cluster', 'device', 'role', 'tenant', 'platform', - 'primary_ip', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'description', 'comments', - 'config_template', 'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated', - 'interface_count', 'virtual_disk_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - validators = [] - - -class VirtualMachineWithConfigContextSerializer(VirtualMachineSerializer): - config_context = serializers.SerializerMethodField() - - class Meta(VirtualMachineSerializer.Meta): - fields = [ - 'id', 'url', 'display', 'name', 'status', 'site', 'cluster', 'device', 'role', 'tenant', 'platform', - 'primary_ip', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'description', 'comments', - 'config_template', 'local_context_data', 'tags', 'custom_fields', 'config_context', 'created', - 'last_updated', 'interface_count', 'virtual_disk_count', - ] - - @extend_schema_field(serializers.JSONField(allow_null=True)) - def get_config_context(self, obj): - return obj.get_config_context() - - -# -# VM interfaces -# - -class VMInterfaceSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:vminterface-detail') - virtual_machine = VirtualMachineSerializer(nested=True) - parent = NestedVMInterfaceSerializer(required=False, allow_null=True) - bridge = NestedVMInterfaceSerializer(required=False, allow_null=True) - mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False) - untagged_vlan = NestedVLANSerializer(required=False, allow_null=True) - tagged_vlans = SerializedPKRelatedField( - queryset=VLAN.objects.all(), - serializer=NestedVLANSerializer, - required=False, - many=True - ) - vrf = NestedVRFSerializer(required=False, allow_null=True) - l2vpn_termination = NestedL2VPNTerminationSerializer(read_only=True, allow_null=True) - count_ipaddresses = serializers.IntegerField(read_only=True) - count_fhrp_groups = serializers.IntegerField(read_only=True) - mac_address = serializers.CharField( - required=False, - default=None, - allow_null=True - ) - - class Meta: - model = VMInterface - fields = [ - 'id', 'url', 'display', 'virtual_machine', 'name', 'enabled', 'parent', 'bridge', 'mtu', 'mac_address', - 'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'vrf', 'l2vpn_termination', 'tags', 'custom_fields', - 'created', 'last_updated', 'count_ipaddresses', 'count_fhrp_groups', - ] - brief_fields = ('id', 'url', 'display', 'virtual_machine', 'name', 'description') - - def validate(self, data): - - # Validate many-to-many VLAN assignments - virtual_machine = self.instance.virtual_machine if self.instance else data.get('virtual_machine') - for vlan in data.get('tagged_vlans', []): - if vlan.site not in [virtual_machine.site, None]: - raise serializers.ValidationError({ - 'tagged_vlans': f"VLAN {vlan} must belong to the same site as the interface's parent virtual " - f"machine, or it must be global." - }) - - return super().validate(data) - - -# -# Virtual Disk -# - -class VirtualDiskSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:virtualdisk-detail') - virtual_machine = VirtualMachineSerializer(nested=True) - - class Meta: - model = VirtualDisk - fields = [ - 'id', 'url', 'display', 'virtual_machine', 'name', 'description', 'size', 'tags', 'custom_fields', - 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'virtual_machine', 'name', 'description', 'size') diff --git a/netbox/virtualization/api/serializers_/__init__.py b/netbox/virtualization/api/serializers_/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/virtualization/api/serializers_/clusters.py b/netbox/virtualization/api/serializers_/clusters.py new file mode 100644 index 000000000..5765ff614 --- /dev/null +++ b/netbox/virtualization/api/serializers_/clusters.py @@ -0,0 +1,65 @@ +from rest_framework import serializers + +from dcim.api.serializers_.sites import SiteSerializer +from netbox.api.fields import ChoiceField, RelatedObjectCountField +from netbox.api.serializers import NetBoxModelSerializer +from tenancy.api.serializers_.tenants import TenantSerializer +from virtualization.choices import * +from virtualization.models import Cluster, ClusterGroup, ClusterType + +__all__ = ( + 'ClusterGroupSerializer', + 'ClusterSerializer', + 'ClusterTypeSerializer', +) + + +class ClusterTypeSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustertype-detail') + + # Related object counts + cluster_count = RelatedObjectCountField('clusters') + + class Meta: + model = ClusterType + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated', + 'cluster_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'cluster_count') + + +class ClusterGroupSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustergroup-detail') + + # Related object counts + cluster_count = RelatedObjectCountField('clusters') + + class Meta: + model = ClusterGroup + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated', + 'cluster_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'cluster_count') + + +class ClusterSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail') + type = ClusterTypeSerializer(nested=True) + group = ClusterGroupSerializer(nested=True, required=False, allow_null=True, default=None) + status = ChoiceField(choices=ClusterStatusChoices, required=False) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) + site = SiteSerializer(nested=True, required=False, allow_null=True, default=None) + + # Related object counts + device_count = RelatedObjectCountField('devices') + virtualmachine_count = RelatedObjectCountField('virtual_machines') + + class Meta: + model = Cluster + fields = [ + 'id', 'url', 'display', 'name', 'type', 'group', 'status', 'tenant', 'site', 'description', 'comments', + 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description', 'virtualmachine_count') diff --git a/netbox/virtualization/api/serializers_/virtualmachines.py b/netbox/virtualization/api/serializers_/virtualmachines.py new file mode 100644 index 000000000..b9780cec7 --- /dev/null +++ b/netbox/virtualization/api/serializers_/virtualmachines.py @@ -0,0 +1,143 @@ +from drf_spectacular.utils import extend_schema_field +from rest_framework import serializers + +from dcim.api.serializers_.devices import DeviceSerializer +from dcim.api.serializers_.platforms import PlatformSerializer +from dcim.api.serializers_.roles import DeviceRoleSerializer +from dcim.api.serializers_.sites import SiteSerializer +from dcim.choices import InterfaceModeChoices +from extras.api.serializers_.provisioning import ConfigTemplateSerializer +from ipam.api.nested_serializers import NestedVLANSerializer +from ipam.api.serializers_.ip import IPAddressSerializer +from ipam.api.serializers_.vlans import VLANSerializer +from ipam.api.serializers_.vrfs import VRFSerializer +from ipam.models import VLAN +from netbox.api.fields import ChoiceField, SerializedPKRelatedField +from netbox.api.serializers import NetBoxModelSerializer +from tenancy.api.serializers_.tenants import TenantSerializer +from virtualization.choices import * +from virtualization.models import VirtualDisk, VirtualMachine, VMInterface +from vpn.api.serializers_.l2vpn import L2VPNTerminationSerializer +from ..nested_serializers import * + +from .clusters import ClusterSerializer + +__all__ = ( + 'VMInterfaceSerializer', + 'VirtualDiskSerializer', + 'VirtualMachineSerializer', + 'VirtualMachineWithConfigContextSerializer', +) + + +class VirtualMachineSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:virtualmachine-detail') + status = ChoiceField(choices=VirtualMachineStatusChoices, required=False) + site = SiteSerializer(nested=True, required=False, allow_null=True) + cluster = ClusterSerializer(nested=True, required=False, allow_null=True) + device = DeviceSerializer(nested=True, required=False, allow_null=True) + role = DeviceRoleSerializer(nested=True, required=False, allow_null=True) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) + platform = PlatformSerializer(nested=True, required=False, allow_null=True) + primary_ip = IPAddressSerializer(nested=True, read_only=True) + primary_ip4 = IPAddressSerializer(nested=True, required=False, allow_null=True) + primary_ip6 = IPAddressSerializer(nested=True, required=False, allow_null=True) + config_template = ConfigTemplateSerializer(nested=True, required=False, allow_null=True, default=None) + + # Counter fields + interface_count = serializers.IntegerField(read_only=True) + virtual_disk_count = serializers.IntegerField(read_only=True) + + class Meta: + model = VirtualMachine + fields = [ + 'id', 'url', 'display', 'name', 'status', 'site', 'cluster', 'device', 'role', 'tenant', 'platform', + 'primary_ip', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'description', 'comments', + 'config_template', 'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated', + 'interface_count', 'virtual_disk_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') + validators = [] + + +class VirtualMachineWithConfigContextSerializer(VirtualMachineSerializer): + config_context = serializers.SerializerMethodField() + + class Meta(VirtualMachineSerializer.Meta): + fields = [ + 'id', 'url', 'display', 'name', 'status', 'site', 'cluster', 'device', 'role', 'tenant', 'platform', + 'primary_ip', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'description', 'comments', + 'config_template', 'local_context_data', 'tags', 'custom_fields', 'config_context', 'created', + 'last_updated', 'interface_count', 'virtual_disk_count', + ] + + @extend_schema_field(serializers.JSONField(allow_null=True)) + def get_config_context(self, obj): + return obj.get_config_context() + + +# +# VM interfaces +# + +class VMInterfaceSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:vminterface-detail') + virtual_machine = VirtualMachineSerializer(nested=True) + parent = NestedVMInterfaceSerializer(required=False, allow_null=True) + bridge = NestedVMInterfaceSerializer(required=False, allow_null=True) + mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False) + untagged_vlan = VLANSerializer(nested=True, required=False, allow_null=True) + tagged_vlans = SerializedPKRelatedField( + queryset=VLAN.objects.all(), + serializer=NestedVLANSerializer, + required=False, + many=True + ) + vrf = VRFSerializer(nested=True, required=False, allow_null=True) + l2vpn_termination = L2VPNTerminationSerializer(nested=True, read_only=True, allow_null=True) + count_ipaddresses = serializers.IntegerField(read_only=True) + count_fhrp_groups = serializers.IntegerField(read_only=True) + mac_address = serializers.CharField( + required=False, + default=None, + allow_null=True + ) + + class Meta: + model = VMInterface + fields = [ + 'id', 'url', 'display', 'virtual_machine', 'name', 'enabled', 'parent', 'bridge', 'mtu', 'mac_address', + 'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'vrf', 'l2vpn_termination', 'tags', 'custom_fields', + 'created', 'last_updated', 'count_ipaddresses', 'count_fhrp_groups', + ] + brief_fields = ('id', 'url', 'display', 'virtual_machine', 'name', 'description') + + def validate(self, data): + + # Validate many-to-many VLAN assignments + virtual_machine = self.instance.virtual_machine if self.instance else data.get('virtual_machine') + for vlan in data.get('tagged_vlans', []): + if vlan.site not in [virtual_machine.site, None]: + raise serializers.ValidationError({ + 'tagged_vlans': f"VLAN {vlan} must belong to the same site as the interface's parent virtual " + f"machine, or it must be global." + }) + + return super().validate(data) + + +# +# Virtual Disk +# + +class VirtualDiskSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:virtualdisk-detail') + virtual_machine = VirtualMachineSerializer(nested=True) + + class Meta: + model = VirtualDisk + fields = [ + 'id', 'url', 'display', 'virtual_machine', 'name', 'description', 'size', 'tags', 'custom_fields', + 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'virtual_machine', 'name', 'description', 'size') diff --git a/netbox/vpn/api/serializers.py b/netbox/vpn/api/serializers.py index 7ba374fc3..8a7e21e63 100644 --- a/netbox/vpn/api/serializers.py +++ b/netbox/vpn/api/serializers.py @@ -1,297 +1,4 @@ -from django.contrib.contenttypes.models import ContentType -from drf_spectacular.utils import extend_schema_field -from rest_framework import serializers - -from ipam.api.serializers import IPAddressSerializer -from ipam.api.nested_serializers import NestedRouteTargetSerializer -from ipam.models import RouteTarget -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.serializers import TenantSerializer -from utilities.api import get_serializer_for_model -from vpn.choices import * -from vpn.models import * +from .serializers_.crypto import * +from .serializers_.tunnels import * +from .serializers_.l2vpn import * from .nested_serializers import * - -__all__ = ( - 'IKEPolicySerializer', - 'IKEProposalSerializer', - 'IPSecPolicySerializer', - 'IPSecProfileSerializer', - 'IPSecProposalSerializer', - 'L2VPNSerializer', - 'L2VPNTerminationSerializer', - 'TunnelGroupSerializer', - 'TunnelSerializer', - 'TunnelTerminationSerializer', -) - - -class IKEProposalSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField( - view_name='vpn-api:ikeproposal-detail' - ) - authentication_method = ChoiceField( - choices=AuthenticationMethodChoices - ) - encryption_algorithm = ChoiceField( - choices=EncryptionAlgorithmChoices - ) - authentication_algorithm = ChoiceField( - choices=AuthenticationAlgorithmChoices - ) - group = ChoiceField( - choices=DHGroupChoices - ) - - class Meta: - model = IKEProposal - fields = ( - 'id', 'url', 'display', 'name', 'description', 'authentication_method', 'encryption_algorithm', - 'authentication_algorithm', 'group', 'sa_lifetime', 'comments', 'tags', 'custom_fields', 'created', - 'last_updated', - ) - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -class IKEPolicySerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField( - view_name='vpn-api:ikepolicy-detail' - ) - version = ChoiceField( - choices=IKEVersionChoices - ) - mode = ChoiceField( - choices=IKEModeChoices - ) - proposals = SerializedPKRelatedField( - queryset=IKEProposal.objects.all(), - serializer=NestedIKEProposalSerializer, - required=False, - many=True - ) - - class Meta: - model = IKEPolicy - fields = ( - 'id', 'url', 'display', 'name', 'description', 'version', 'mode', 'proposals', 'preshared_key', 'comments', - 'tags', 'custom_fields', 'created', 'last_updated', - ) - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -class IPSecProposalSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField( - view_name='vpn-api:ipsecproposal-detail' - ) - encryption_algorithm = ChoiceField( - choices=EncryptionAlgorithmChoices - ) - authentication_algorithm = ChoiceField( - choices=AuthenticationAlgorithmChoices - ) - - class Meta: - model = IPSecProposal - fields = ( - 'id', 'url', 'display', 'name', 'description', 'encryption_algorithm', 'authentication_algorithm', - 'sa_lifetime_seconds', 'sa_lifetime_data', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', - ) - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -class IPSecPolicySerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField( - view_name='vpn-api:ipsecpolicy-detail' - ) - proposals = SerializedPKRelatedField( - queryset=IPSecProposal.objects.all(), - serializer=NestedIPSecProposalSerializer, - required=False, - many=True - ) - pfs_group = ChoiceField( - choices=DHGroupChoices, - required=False - ) - - class Meta: - model = IPSecPolicy - fields = ( - 'id', 'url', 'display', 'name', 'description', 'proposals', 'pfs_group', 'comments', 'tags', - 'custom_fields', 'created', 'last_updated', - ) - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -class IPSecProfileSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField( - view_name='vpn-api:ipsecprofile-detail' - ) - mode = ChoiceField( - choices=IPSecModeChoices - ) - ike_policy = IKEPolicySerializer( - nested=True - ) - ipsec_policy = IPSecPolicySerializer( - nested=True - ) - - class Meta: - model = IPSecProfile - fields = ( - 'id', 'url', 'display', 'name', 'description', 'mode', 'ike_policy', 'ipsec_policy', 'comments', 'tags', - 'custom_fields', 'created', 'last_updated', - ) - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -# -# Tunnels -# - -class TunnelGroupSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='vpn-api:tunnelgroup-detail') - - # Related object counts - tunnel_count = RelatedObjectCountField('tunnels') - - class Meta: - model = TunnelGroup - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated', - 'tunnel_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'tunnel_count') - - -class TunnelSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField( - view_name='vpn-api:tunnel-detail' - ) - status = ChoiceField( - choices=TunnelStatusChoices - ) - group = TunnelGroupSerializer( - nested=True, - required=False, - allow_null=True - ) - encapsulation = ChoiceField( - choices=TunnelEncapsulationChoices - ) - ipsec_profile = IPSecProfileSerializer( - nested=True, - required=False, - allow_null=True - ) - tenant = TenantSerializer( - nested=True, - required=False, - 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', 'terminations_count', - ) - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -class TunnelTerminationSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField( - view_name='vpn-api:tunneltermination-detail' - ) - tunnel = TunnelSerializer( - nested=True - ) - role = ChoiceField( - choices=TunnelTerminationRoleChoices - ) - termination_type = ContentTypeField( - queryset=ContentType.objects.all() - ) - termination = serializers.SerializerMethodField( - read_only=True - ) - outside_ip = IPAddressSerializer( - nested=True, - required=False, - allow_null=True - ) - - class Meta: - model = TunnelTermination - fields = ( - 'id', 'url', 'display', 'tunnel', 'role', 'termination_type', 'termination_id', 'termination', 'outside_ip', - 'tags', 'custom_fields', 'created', 'last_updated', - ) - brief_fields = ('id', 'url', 'display') - - @extend_schema_field(serializers.JSONField(allow_null=True)) - def get_termination(self, obj): - serializer = get_serializer_for_model(obj.termination, prefix=NESTED_SERIALIZER_PREFIX) - context = {'request': self.context['request']} - return serializer(obj.termination, context=context).data - - -# -# L2VPN -# - -class L2VPNSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='vpn-api:l2vpn-detail') - type = ChoiceField(choices=L2VPNTypeChoices, required=False) - import_targets = SerializedPKRelatedField( - queryset=RouteTarget.objects.all(), - serializer=NestedRouteTargetSerializer, - required=False, - many=True - ) - export_targets = SerializedPKRelatedField( - queryset=RouteTarget.objects.all(), - serializer=NestedRouteTargetSerializer, - required=False, - many=True - ) - tenant = TenantSerializer(nested=True, required=False, allow_null=True) - - class Meta: - model = L2VPN - fields = [ - 'id', 'url', 'display', 'identifier', 'name', 'slug', 'type', 'import_targets', 'export_targets', - 'description', 'comments', 'tenant', 'tags', 'custom_fields', 'created', 'last_updated' - ] - brief_fields = ('id', 'url', 'display', 'identifier', 'name', 'slug', 'type', 'description') - - -class L2VPNTerminationSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='vpn-api:l2vpntermination-detail') - l2vpn = L2VPNSerializer( - nested=True - ) - assigned_object_type = ContentTypeField( - queryset=ContentType.objects.all() - ) - assigned_object = serializers.SerializerMethodField(read_only=True) - - class Meta: - model = L2VPNTermination - fields = [ - 'id', 'url', 'display', 'l2vpn', 'assigned_object_type', 'assigned_object_id', - 'assigned_object', 'tags', 'custom_fields', 'created', 'last_updated' - ] - brief_fields = ('id', 'url', 'display', 'l2vpn') - - @extend_schema_field(serializers.JSONField(allow_null=True)) - def get_assigned_object(self, instance): - serializer = get_serializer_for_model(instance.assigned_object, prefix=NESTED_SERIALIZER_PREFIX) - context = {'request': self.context['request']} - return serializer(instance.assigned_object, context=context).data diff --git a/netbox/vpn/api/serializers_/__init__.py b/netbox/vpn/api/serializers_/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/vpn/api/serializers_/crypto.py b/netbox/vpn/api/serializers_/crypto.py new file mode 100644 index 000000000..ed03fbc3a --- /dev/null +++ b/netbox/vpn/api/serializers_/crypto.py @@ -0,0 +1,135 @@ +from rest_framework import serializers + +from netbox.api.fields import ChoiceField, SerializedPKRelatedField +from netbox.api.serializers import NetBoxModelSerializer +from vpn.choices import * +from vpn.models import IKEPolicy, IKEProposal, IPSecPolicy, IPSecProfile, IPSecProposal +from ..nested_serializers import * + +__all__ = ( + 'IKEPolicySerializer', + 'IKEProposalSerializer', + 'IPSecPolicySerializer', + 'IPSecProfileSerializer', + 'IPSecProposalSerializer', +) + + +class IKEProposalSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField( + view_name='vpn-api:ikeproposal-detail' + ) + authentication_method = ChoiceField( + choices=AuthenticationMethodChoices + ) + encryption_algorithm = ChoiceField( + choices=EncryptionAlgorithmChoices + ) + authentication_algorithm = ChoiceField( + choices=AuthenticationAlgorithmChoices + ) + group = ChoiceField( + choices=DHGroupChoices + ) + + class Meta: + model = IKEProposal + fields = ( + 'id', 'url', 'display', 'name', 'description', 'authentication_method', 'encryption_algorithm', + 'authentication_algorithm', 'group', 'sa_lifetime', 'comments', 'tags', 'custom_fields', 'created', + 'last_updated', + ) + brief_fields = ('id', 'url', 'display', 'name', 'description') + + +class IKEPolicySerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField( + view_name='vpn-api:ikepolicy-detail' + ) + version = ChoiceField( + choices=IKEVersionChoices + ) + mode = ChoiceField( + choices=IKEModeChoices + ) + proposals = SerializedPKRelatedField( + queryset=IKEProposal.objects.all(), + serializer=NestedIKEProposalSerializer, + required=False, + many=True + ) + + class Meta: + model = IKEPolicy + fields = ( + 'id', 'url', 'display', 'name', 'description', 'version', 'mode', 'proposals', 'preshared_key', 'comments', + 'tags', 'custom_fields', 'created', 'last_updated', + ) + brief_fields = ('id', 'url', 'display', 'name', 'description') + + +class IPSecProposalSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField( + view_name='vpn-api:ipsecproposal-detail' + ) + encryption_algorithm = ChoiceField( + choices=EncryptionAlgorithmChoices + ) + authentication_algorithm = ChoiceField( + choices=AuthenticationAlgorithmChoices + ) + + class Meta: + model = IPSecProposal + fields = ( + 'id', 'url', 'display', 'name', 'description', 'encryption_algorithm', 'authentication_algorithm', + 'sa_lifetime_seconds', 'sa_lifetime_data', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', + ) + brief_fields = ('id', 'url', 'display', 'name', 'description') + + +class IPSecPolicySerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField( + view_name='vpn-api:ipsecpolicy-detail' + ) + proposals = SerializedPKRelatedField( + queryset=IPSecProposal.objects.all(), + serializer=NestedIPSecProposalSerializer, + required=False, + many=True + ) + pfs_group = ChoiceField( + choices=DHGroupChoices, + required=False + ) + + class Meta: + model = IPSecPolicy + fields = ( + 'id', 'url', 'display', 'name', 'description', 'proposals', 'pfs_group', 'comments', 'tags', + 'custom_fields', 'created', 'last_updated', + ) + brief_fields = ('id', 'url', 'display', 'name', 'description') + + +class IPSecProfileSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField( + view_name='vpn-api:ipsecprofile-detail' + ) + mode = ChoiceField( + choices=IPSecModeChoices + ) + ike_policy = IKEPolicySerializer( + nested=True + ) + ipsec_policy = IPSecPolicySerializer( + nested=True + ) + + class Meta: + model = IPSecProfile + fields = ( + 'id', 'url', 'display', 'name', 'description', 'mode', 'ike_policy', 'ipsec_policy', 'comments', 'tags', + 'custom_fields', 'created', 'last_updated', + ) + brief_fields = ('id', 'url', 'display', 'name', 'description') diff --git a/netbox/vpn/api/serializers_/l2vpn.py b/netbox/vpn/api/serializers_/l2vpn.py new file mode 100644 index 000000000..00523910b --- /dev/null +++ b/netbox/vpn/api/serializers_/l2vpn.py @@ -0,0 +1,69 @@ +from django.contrib.contenttypes.models import ContentType +from drf_spectacular.utils import extend_schema_field +from rest_framework import serializers + +from ipam.api.nested_serializers import NestedRouteTargetSerializer +from ipam.models import RouteTarget +from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField +from netbox.api.serializers import NetBoxModelSerializer +from netbox.constants import NESTED_SERIALIZER_PREFIX +from tenancy.api.serializers_.tenants import TenantSerializer +from utilities.api import get_serializer_for_model +from vpn.choices import * +from vpn.models import L2VPN, L2VPNTermination + +__all__ = ( + 'L2VPNSerializer', + 'L2VPNTerminationSerializer', +) + + +class L2VPNSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='vpn-api:l2vpn-detail') + type = ChoiceField(choices=L2VPNTypeChoices, required=False) + import_targets = SerializedPKRelatedField( + queryset=RouteTarget.objects.all(), + serializer=NestedRouteTargetSerializer, + required=False, + many=True + ) + export_targets = SerializedPKRelatedField( + queryset=RouteTarget.objects.all(), + serializer=NestedRouteTargetSerializer, + required=False, + many=True + ) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) + + class Meta: + model = L2VPN + fields = [ + 'id', 'url', 'display', 'identifier', 'name', 'slug', 'type', 'import_targets', 'export_targets', + 'description', 'comments', 'tenant', 'tags', 'custom_fields', 'created', 'last_updated' + ] + brief_fields = ('id', 'url', 'display', 'identifier', 'name', 'slug', 'type', 'description') + + +class L2VPNTerminationSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='vpn-api:l2vpntermination-detail') + l2vpn = L2VPNSerializer( + nested=True + ) + assigned_object_type = ContentTypeField( + queryset=ContentType.objects.all() + ) + assigned_object = serializers.SerializerMethodField(read_only=True) + + class Meta: + model = L2VPNTermination + fields = [ + 'id', 'url', 'display', 'l2vpn', 'assigned_object_type', 'assigned_object_id', + 'assigned_object', 'tags', 'custom_fields', 'created', 'last_updated' + ] + brief_fields = ('id', 'url', 'display', 'l2vpn') + + @extend_schema_field(serializers.JSONField(allow_null=True)) + def get_assigned_object(self, instance): + serializer = get_serializer_for_model(instance.assigned_object, prefix=NESTED_SERIALIZER_PREFIX) + context = {'request': self.context['request']} + return serializer(instance.assigned_object, context=context).data diff --git a/netbox/vpn/api/serializers_/tunnels.py b/netbox/vpn/api/serializers_/tunnels.py new file mode 100644 index 000000000..6d4f334f0 --- /dev/null +++ b/netbox/vpn/api/serializers_/tunnels.py @@ -0,0 +1,114 @@ +from django.contrib.contenttypes.models import ContentType +from drf_spectacular.utils import extend_schema_field +from rest_framework import serializers + +from ipam.api.serializers_.ip import IPAddressSerializer +from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField +from netbox.api.serializers import NetBoxModelSerializer +from netbox.constants import NESTED_SERIALIZER_PREFIX +from tenancy.api.serializers_.tenants import TenantSerializer +from utilities.api import get_serializer_for_model +from vpn.choices import * +from vpn.models import Tunnel, TunnelGroup, TunnelTermination + +from .crypto import IPSecProfileSerializer + +__all__ = ( + 'TunnelGroupSerializer', + 'TunnelSerializer', + 'TunnelTerminationSerializer', +) + + +# +# Tunnels +# + +class TunnelGroupSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='vpn-api:tunnelgroup-detail') + + # Related object counts + tunnel_count = RelatedObjectCountField('tunnels') + + class Meta: + model = TunnelGroup + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated', + 'tunnel_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'tunnel_count') + + +class TunnelSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField( + view_name='vpn-api:tunnel-detail' + ) + status = ChoiceField( + choices=TunnelStatusChoices + ) + group = TunnelGroupSerializer( + nested=True, + required=False, + allow_null=True + ) + encapsulation = ChoiceField( + choices=TunnelEncapsulationChoices + ) + ipsec_profile = IPSecProfileSerializer( + nested=True, + required=False, + allow_null=True + ) + tenant = TenantSerializer( + nested=True, + required=False, + 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', 'terminations_count', + ) + brief_fields = ('id', 'url', 'display', 'name', 'description') + + +class TunnelTerminationSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField( + view_name='vpn-api:tunneltermination-detail' + ) + tunnel = TunnelSerializer( + nested=True + ) + role = ChoiceField( + choices=TunnelTerminationRoleChoices + ) + termination_type = ContentTypeField( + queryset=ContentType.objects.all() + ) + termination = serializers.SerializerMethodField( + read_only=True + ) + outside_ip = IPAddressSerializer( + nested=True, + required=False, + allow_null=True + ) + + class Meta: + model = TunnelTermination + fields = ( + 'id', 'url', 'display', 'tunnel', 'role', 'termination_type', 'termination_id', 'termination', 'outside_ip', + 'tags', 'custom_fields', 'created', 'last_updated', + ) + brief_fields = ('id', 'url', 'display') + + @extend_schema_field(serializers.JSONField(allow_null=True)) + def get_termination(self, obj): + serializer = get_serializer_for_model(obj.termination, prefix=NESTED_SERIALIZER_PREFIX) + context = {'request': self.context['request']} + return serializer(obj.termination, context=context).data diff --git a/netbox/wireless/api/serializers.py b/netbox/wireless/api/serializers.py index 94fd0a94b..8c864d059 100644 --- a/netbox/wireless/api/serializers.py +++ b/netbox/wireless/api/serializers.py @@ -1,67 +1,3 @@ -from rest_framework import serializers - -from dcim.choices import LinkStatusChoices -from dcim.api.serializers import InterfaceSerializer -from ipam.api.serializers import VLANSerializer -from netbox.api.fields import ChoiceField -from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer -from tenancy.api.serializers import TenantSerializer -from wireless.choices import * -from wireless.models import * +from .serializers_.wirelesslans import * +from .serializers_.wirelesslinks import * from .nested_serializers import * - -__all__ = ( - 'WirelessLANGroupSerializer', - 'WirelessLANSerializer', - 'WirelessLinkSerializer', -) - - -class WirelessLANGroupSerializer(NestedGroupModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslangroup-detail') - parent = NestedWirelessLANGroupSerializer(required=False, allow_null=True, default=None) - wirelesslan_count = serializers.IntegerField(read_only=True) - - class Meta: - model = WirelessLANGroup - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created', - 'last_updated', 'wirelesslan_count', '_depth', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'wirelesslan_count', '_depth') - - -class WirelessLANSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslan-detail') - group = WirelessLANGroupSerializer(nested=True, required=False, allow_null=True) - status = ChoiceField(choices=WirelessLANStatusChoices, required=False, allow_blank=True) - vlan = VLANSerializer(nested=True, required=False, allow_null=True) - tenant = TenantSerializer(nested=True, required=False, allow_null=True) - auth_type = ChoiceField(choices=WirelessAuthTypeChoices, required=False, allow_blank=True) - auth_cipher = ChoiceField(choices=WirelessAuthCipherChoices, required=False, allow_blank=True) - - class Meta: - model = WirelessLAN - fields = [ - 'id', 'url', 'display', 'ssid', 'description', 'group', 'status', 'vlan', 'tenant', 'auth_type', - 'auth_cipher', 'auth_psk', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'ssid', 'description') - - -class WirelessLinkSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslink-detail') - status = ChoiceField(choices=LinkStatusChoices, required=False) - interface_a = InterfaceSerializer(nested=True) - interface_b = InterfaceSerializer(nested=True) - tenant = TenantSerializer(nested=True, required=False, allow_null=True) - auth_type = ChoiceField(choices=WirelessAuthTypeChoices, required=False, allow_blank=True) - auth_cipher = ChoiceField(choices=WirelessAuthCipherChoices, required=False, allow_blank=True) - - class Meta: - model = WirelessLink - fields = [ - 'id', 'url', 'display', 'interface_a', 'interface_b', 'ssid', 'status', 'tenant', 'auth_type', - 'auth_cipher', 'auth_psk', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'ssid', 'description') diff --git a/netbox/wireless/api/serializers_/__init__.py b/netbox/wireless/api/serializers_/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/wireless/api/serializers_/wirelesslans.py b/netbox/wireless/api/serializers_/wirelesslans.py new file mode 100644 index 000000000..d4356c881 --- /dev/null +++ b/netbox/wireless/api/serializers_/wirelesslans.py @@ -0,0 +1,46 @@ +from rest_framework import serializers + +from ipam.api.serializers_.vlans import VLANSerializer +from netbox.api.fields import ChoiceField +from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer +from tenancy.api.serializers_.tenants import TenantSerializer +from wireless.choices import * +from wireless.models import WirelessLAN, WirelessLANGroup +from ..nested_serializers import * + +__all__ = ( + 'WirelessLANGroupSerializer', + 'WirelessLANSerializer', +) + + +class WirelessLANGroupSerializer(NestedGroupModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslangroup-detail') + parent = NestedWirelessLANGroupSerializer(required=False, allow_null=True, default=None) + wirelesslan_count = serializers.IntegerField(read_only=True) + + class Meta: + model = WirelessLANGroup + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created', + 'last_updated', 'wirelesslan_count', '_depth', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'wirelesslan_count', '_depth') + + +class WirelessLANSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslan-detail') + group = WirelessLANGroupSerializer(nested=True, required=False, allow_null=True) + status = ChoiceField(choices=WirelessLANStatusChoices, required=False, allow_blank=True) + vlan = VLANSerializer(nested=True, required=False, allow_null=True) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) + auth_type = ChoiceField(choices=WirelessAuthTypeChoices, required=False, allow_blank=True) + auth_cipher = ChoiceField(choices=WirelessAuthCipherChoices, required=False, allow_blank=True) + + class Meta: + model = WirelessLAN + fields = [ + 'id', 'url', 'display', 'ssid', 'description', 'group', 'status', 'vlan', 'tenant', 'auth_type', + 'auth_cipher', 'auth_psk', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'ssid', 'description') diff --git a/netbox/wireless/api/serializers_/wirelesslinks.py b/netbox/wireless/api/serializers_/wirelesslinks.py new file mode 100644 index 000000000..3a7f88856 --- /dev/null +++ b/netbox/wireless/api/serializers_/wirelesslinks.py @@ -0,0 +1,31 @@ +from rest_framework import serializers + +from dcim.api.serializers_.device_components import InterfaceSerializer +from dcim.choices import LinkStatusChoices +from netbox.api.fields import ChoiceField +from netbox.api.serializers import NetBoxModelSerializer +from tenancy.api.serializers_.tenants import TenantSerializer +from wireless.choices import * +from wireless.models import WirelessLink + +__all__ = ( + 'WirelessLinkSerializer', +) + + +class WirelessLinkSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslink-detail') + status = ChoiceField(choices=LinkStatusChoices, required=False) + interface_a = InterfaceSerializer(nested=True) + interface_b = InterfaceSerializer(nested=True) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) + auth_type = ChoiceField(choices=WirelessAuthTypeChoices, required=False, allow_blank=True) + auth_cipher = ChoiceField(choices=WirelessAuthCipherChoices, required=False, allow_blank=True) + + class Meta: + model = WirelessLink + fields = [ + 'id', 'url', 'display', 'interface_a', 'interface_b', 'ssid', 'status', 'tenant', 'auth_type', + 'auth_cipher', 'auth_psk', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'ssid', 'description') From ca56c8b9ef8a9c2238090b0a500e85361f9655b9 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 27 Feb 2024 14:00:42 -0500 Subject: [PATCH 03/47] Add dynamic nesting support to SerializedPKRelatedField --- netbox/circuits/api/serializers_/providers.py | 5 +- .../api/serializers_/device_components.py | 15 ++-- netbox/dcim/api/serializers_/devices.py | 2 +- netbox/dcim/api/serializers_/platforms.py | 2 +- netbox/dcim/api/serializers_/roles.py | 2 +- netbox/dcim/api/serializers_/sites.py | 5 +- netbox/extras/api/serializers.py | 3 +- .../{provisioning.py => configcontexts.py} | 78 ++++++++----------- .../api/serializers_/configtemplates.py | 30 +++++++ netbox/extras/api/serializers_/events.py | 4 +- netbox/ipam/api/serializers_/fhrpgroups.py | 4 +- netbox/ipam/api/serializers_/services.py | 5 +- netbox/ipam/api/serializers_/vrfs.py | 31 ++++---- netbox/netbox/api/fields.py | 6 +- netbox/users/api/serializers_/permissions.py | 8 +- netbox/users/api/serializers_/users.py | 24 +++--- .../api/serializers_/virtualmachines.py | 9 +-- netbox/vpn/api/serializers_/crypto.py | 7 +- netbox/vpn/api/serializers_/l2vpn.py | 8 +- 19 files changed, 137 insertions(+), 111 deletions(-) rename netbox/extras/api/serializers_/{provisioning.py => configcontexts.py} (62%) create mode 100644 netbox/extras/api/serializers_/configtemplates.py diff --git a/netbox/circuits/api/serializers_/providers.py b/netbox/circuits/api/serializers_/providers.py index 76ea37360..302c2da5a 100644 --- a/netbox/circuits/api/serializers_/providers.py +++ b/netbox/circuits/api/serializers_/providers.py @@ -1,7 +1,7 @@ from rest_framework import serializers from circuits.models import Provider, ProviderAccount, ProviderNetwork -from ipam.api.nested_serializers import NestedASNSerializer +from ipam.api.serializers_.asns import ASNSerializer from ipam.models import ASN from netbox.api.fields import RelatedObjectCountField, SerializedPKRelatedField from netbox.api.serializers import NetBoxModelSerializer @@ -24,7 +24,8 @@ class ProviderSerializer(NetBoxModelSerializer): ) asns = SerializedPKRelatedField( queryset=ASN.objects.all(), - serializer=NestedASNSerializer, + serializer=ASNSerializer, + nested=True, required=False, many=True ) diff --git a/netbox/dcim/api/serializers_/device_components.py b/netbox/dcim/api/serializers_/device_components.py index 2891a1154..a3e9d6e97 100644 --- a/netbox/dcim/api/serializers_/device_components.py +++ b/netbox/dcim/api/serializers_/device_components.py @@ -10,21 +10,21 @@ from dcim.models import ( ) from ipam.api.serializers_.vlans import VLANSerializer from ipam.api.serializers_.vrfs import VRFSerializer -from ipam.api.nested_serializers import NestedVLANSerializer from ipam.models import VLAN from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer from netbox.constants import NESTED_SERIALIZER_PREFIX from utilities.api import get_serializer_for_model from vpn.api.serializers_.l2vpn import L2VPNTerminationSerializer -from wireless.api.nested_serializers import NestedWirelessLANSerializer, NestedWirelessLinkSerializer +from wireless.api.nested_serializers import NestedWirelessLinkSerializer from wireless.choices import * from wireless.models import WirelessLAN from .base import ConnectedEndpointsSerializer from .cables import CabledObjectSerializer -from .devices import DeviceSerializer, ModuleSerializer +from .devices import DeviceSerializer, ModuleSerializer, VirtualDeviceContextSerializer from .manufacturers import ManufacturerSerializer from .roles import InventoryItemRoleSerializer +from wireless.api.serializers_.wirelesslans import WirelessLANSerializer from ..nested_serializers import * __all__ = ( @@ -173,7 +173,8 @@ class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect device = DeviceSerializer(nested=True) vdcs = SerializedPKRelatedField( queryset=VirtualDeviceContext.objects.all(), - serializer=NestedVirtualDeviceContextSerializer, + serializer=VirtualDeviceContextSerializer, + nested=True, required=False, many=True ) @@ -196,7 +197,8 @@ class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect untagged_vlan = VLANSerializer(nested=True, required=False, allow_null=True) tagged_vlans = SerializedPKRelatedField( queryset=VLAN.objects.all(), - serializer=NestedVLANSerializer, + serializer=VLANSerializer, + nested=True, required=False, many=True ) @@ -205,7 +207,8 @@ class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect wireless_link = NestedWirelessLinkSerializer(read_only=True, allow_null=True) wireless_lans = SerializedPKRelatedField( queryset=WirelessLAN.objects.all(), - serializer=NestedWirelessLANSerializer, + serializer=WirelessLANSerializer, + nested=True, required=False, many=True ) diff --git a/netbox/dcim/api/serializers_/devices.py b/netbox/dcim/api/serializers_/devices.py index 4950ab908..ca900732a 100644 --- a/netbox/dcim/api/serializers_/devices.py +++ b/netbox/dcim/api/serializers_/devices.py @@ -6,7 +6,7 @@ from rest_framework import serializers from dcim.choices import * from dcim.models import Device, DeviceBay, Module, VirtualDeviceContext -from extras.api.serializers_.provisioning import ConfigTemplateSerializer +from extras.api.serializers_.configtemplates import ConfigTemplateSerializer from ipam.api.serializers_.ip import IPAddressSerializer from netbox.api.fields import ChoiceField, RelatedObjectCountField from netbox.api.serializers import NetBoxModelSerializer diff --git a/netbox/dcim/api/serializers_/platforms.py b/netbox/dcim/api/serializers_/platforms.py index 03fa9c8f0..7365404eb 100644 --- a/netbox/dcim/api/serializers_/platforms.py +++ b/netbox/dcim/api/serializers_/platforms.py @@ -1,7 +1,7 @@ from rest_framework import serializers from dcim.models import Platform -from extras.api.serializers_.provisioning import ConfigTemplateSerializer +from extras.api.serializers_.configtemplates import ConfigTemplateSerializer from netbox.api.fields import RelatedObjectCountField from netbox.api.serializers import NetBoxModelSerializer from .manufacturers import ManufacturerSerializer diff --git a/netbox/dcim/api/serializers_/roles.py b/netbox/dcim/api/serializers_/roles.py index 300cc4895..41f8f377d 100644 --- a/netbox/dcim/api/serializers_/roles.py +++ b/netbox/dcim/api/serializers_/roles.py @@ -1,7 +1,7 @@ from rest_framework import serializers from dcim.models import DeviceRole, InventoryItemRole -from extras.api.serializers_.provisioning import ConfigTemplateSerializer +from extras.api.serializers_.configtemplates import ConfigTemplateSerializer from netbox.api.fields import RelatedObjectCountField from netbox.api.serializers import NetBoxModelSerializer diff --git a/netbox/dcim/api/serializers_/sites.py b/netbox/dcim/api/serializers_/sites.py index f1f83227d..6fb3811ba 100644 --- a/netbox/dcim/api/serializers_/sites.py +++ b/netbox/dcim/api/serializers_/sites.py @@ -3,7 +3,7 @@ from timezone_field.rest_framework import TimeZoneSerializerField from dcim.choices import * from dcim.models import Location, Region, Site, SiteGroup -from ipam.api.nested_serializers import NestedASNSerializer +from ipam.api.serializers_.asns import ASNSerializer from ipam.models import ASN from netbox.api.fields import ChoiceField, RelatedObjectCountField, SerializedPKRelatedField from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer @@ -55,7 +55,8 @@ class SiteSerializer(NetBoxModelSerializer): time_zone = TimeZoneSerializerField(required=False, allow_null=True) asns = SerializedPKRelatedField( queryset=ASN.objects.all(), - serializer=NestedASNSerializer, + serializer=ASNSerializer, + nested=True, required=False, many=True ) diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index 83fe945c2..809bd78ed 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -8,7 +8,8 @@ from .serializers_.dashboard import * from .serializers_.events import * from .serializers_.exporttemplates import * from .serializers_.journaling import * -from .serializers_.provisioning import * +from .serializers_.configcontexts import * +from .serializers_.configtemplates import * from .serializers_.savedfilters import * from .serializers_.scripts import * from .serializers_.tags import * diff --git a/netbox/extras/api/serializers_/provisioning.py b/netbox/extras/api/serializers_/configcontexts.py similarity index 62% rename from netbox/extras/api/serializers_/provisioning.py rename to netbox/extras/api/serializers_/configcontexts.py index a4593a582..e9688f254 100644 --- a/netbox/extras/api/serializers_/provisioning.py +++ b/netbox/extras/api/serializers_/configcontexts.py @@ -1,25 +1,21 @@ from rest_framework import serializers from core.api.serializers_.data import DataFileSerializer, DataSourceSerializer -from dcim.api.nested_serializers import ( - NestedDeviceRoleSerializer, NestedDeviceTypeSerializer, NestedLocationSerializer, NestedPlatformSerializer, - NestedRegionSerializer, NestedSiteSerializer, NestedSiteGroupSerializer, -) +from dcim.api.serializers_.devicetypes import DeviceTypeSerializer +from dcim.api.serializers_.platforms import PlatformSerializer +from dcim.api.serializers_.roles import DeviceRoleSerializer +from dcim.api.serializers_.sites import LocationSerializer, RegionSerializer, SiteSerializer, SiteGroupSerializer from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup -from extras.models import ConfigContext, ConfigTemplate, Tag +from extras.models import ConfigContext, Tag from netbox.api.fields import SerializedPKRelatedField from netbox.api.serializers import ValidatedModelSerializer -from netbox.api.serializers.features import TaggableModelSerializer -from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer +from tenancy.api.serializers_.tenants import TenantSerializer, TenantGroupSerializer from tenancy.models import Tenant, TenantGroup -from virtualization.api.nested_serializers import ( - NestedClusterGroupSerializer, NestedClusterSerializer, NestedClusterTypeSerializer, -) +from virtualization.api.serializers_.clusters import ClusterSerializer, ClusterGroupSerializer, ClusterTypeSerializer from virtualization.models import Cluster, ClusterGroup, ClusterType __all__ = ( 'ConfigContextSerializer', - 'ConfigTemplateSerializer', ) @@ -27,73 +23,85 @@ class ConfigContextSerializer(ValidatedModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='extras-api:configcontext-detail') regions = SerializedPKRelatedField( queryset=Region.objects.all(), - serializer=NestedRegionSerializer, + serializer=RegionSerializer, + nested=True, required=False, many=True ) site_groups = SerializedPKRelatedField( queryset=SiteGroup.objects.all(), - serializer=NestedSiteGroupSerializer, + serializer=SiteGroupSerializer, + nested=True, required=False, many=True ) sites = SerializedPKRelatedField( queryset=Site.objects.all(), - serializer=NestedSiteSerializer, + serializer=SiteSerializer, + nested=True, required=False, many=True ) locations = SerializedPKRelatedField( queryset=Location.objects.all(), - serializer=NestedLocationSerializer, + serializer=LocationSerializer, + nested=True, required=False, many=True ) device_types = SerializedPKRelatedField( queryset=DeviceType.objects.all(), - serializer=NestedDeviceTypeSerializer, + serializer=DeviceTypeSerializer, + nested=True, required=False, many=True ) roles = SerializedPKRelatedField( queryset=DeviceRole.objects.all(), - serializer=NestedDeviceRoleSerializer, + serializer=DeviceRoleSerializer, + nested=True, required=False, many=True ) platforms = SerializedPKRelatedField( queryset=Platform.objects.all(), - serializer=NestedPlatformSerializer, + serializer=PlatformSerializer, + nested=True, required=False, many=True ) cluster_types = SerializedPKRelatedField( queryset=ClusterType.objects.all(), - serializer=NestedClusterTypeSerializer, + serializer=ClusterTypeSerializer, + nested=True, required=False, many=True ) cluster_groups = SerializedPKRelatedField( queryset=ClusterGroup.objects.all(), - serializer=NestedClusterGroupSerializer, + serializer=ClusterGroupSerializer, + nested=True, required=False, many=True ) clusters = SerializedPKRelatedField( queryset=Cluster.objects.all(), - serializer=NestedClusterSerializer, + serializer=ClusterSerializer, + nested=True, required=False, many=True ) tenant_groups = SerializedPKRelatedField( queryset=TenantGroup.objects.all(), - serializer=NestedTenantGroupSerializer, + serializer=TenantGroupSerializer, + nested=True, required=False, many=True ) tenants = SerializedPKRelatedField( queryset=Tenant.objects.all(), - serializer=NestedTenantSerializer, + serializer=TenantSerializer, + nested=True, required=False, many=True ) @@ -121,27 +129,3 @@ class ConfigContextSerializer(ValidatedModelSerializer): 'created', 'last_updated', ] brief_fields = ('id', 'url', 'display', 'name', 'description') - - -# -# Config templates -# - -class ConfigTemplateSerializer(TaggableModelSerializer, ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='extras-api:configtemplate-detail') - data_source = DataSourceSerializer( - nested=True, - required=False - ) - data_file = DataFileSerializer( - nested=True, - required=False - ) - - class Meta: - model = ConfigTemplate - fields = [ - 'id', 'url', 'display', 'name', 'description', 'environment_params', 'template_code', 'data_source', - 'data_path', 'data_file', 'data_synced', 'tags', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') diff --git a/netbox/extras/api/serializers_/configtemplates.py b/netbox/extras/api/serializers_/configtemplates.py new file mode 100644 index 000000000..935214478 --- /dev/null +++ b/netbox/extras/api/serializers_/configtemplates.py @@ -0,0 +1,30 @@ +from rest_framework import serializers + +from core.api.serializers_.data import DataFileSerializer, DataSourceSerializer +from extras.models import ConfigTemplate +from netbox.api.serializers import ValidatedModelSerializer +from netbox.api.serializers.features import TaggableModelSerializer + +__all__ = ( + 'ConfigTemplateSerializer', +) + + +class ConfigTemplateSerializer(TaggableModelSerializer, ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='extras-api:configtemplate-detail') + data_source = DataSourceSerializer( + nested=True, + required=False + ) + data_file = DataFileSerializer( + nested=True, + required=False + ) + + class Meta: + model = ConfigTemplate + fields = [ + 'id', 'url', 'display', 'name', 'description', 'environment_params', 'template_code', 'data_source', + 'data_path', 'data_file', 'data_synced', 'tags', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') diff --git a/netbox/extras/api/serializers_/events.py b/netbox/extras/api/serializers_/events.py index c95f628db..6c64dd335 100644 --- a/netbox/extras/api/serializers_/events.py +++ b/netbox/extras/api/serializers_/events.py @@ -9,7 +9,7 @@ from netbox.api.fields import ChoiceField, ContentTypeField from netbox.api.serializers import NetBoxModelSerializer from netbox.constants import NESTED_SERIALIZER_PREFIX from utilities.api import get_serializer_for_model -from ..nested_serializers import * +from .scripts import ScriptSerializer __all__ = ( 'EventRuleSerializer', @@ -49,7 +49,7 @@ class EventRuleSerializer(NetBoxModelSerializer): if instance.action_type == EventRuleActionChoices.SCRIPT: script = instance.action_object instance = script.python_class() if script.python_class else None - return NestedScriptSerializer(instance, context=context).data + return ScriptSerializer(instance, nested=True, context=context).data else: serializer = get_serializer_for_model( model=instance.action_object_type.model_class(), diff --git a/netbox/ipam/api/serializers_/fhrpgroups.py b/netbox/ipam/api/serializers_/fhrpgroups.py index 1fae8f2e9..986ae64d5 100644 --- a/netbox/ipam/api/serializers_/fhrpgroups.py +++ b/netbox/ipam/api/serializers_/fhrpgroups.py @@ -7,7 +7,7 @@ from netbox.api.fields import ContentTypeField from netbox.api.serializers import NetBoxModelSerializer from netbox.constants import NESTED_SERIALIZER_PREFIX from utilities.api import get_serializer_for_model -from ..nested_serializers import * +from .ip import IPAddressSerializer __all__ = ( 'FHRPGroupAssignmentSerializer', @@ -17,7 +17,7 @@ __all__ = ( class FHRPGroupSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:fhrpgroup-detail') - ip_addresses = NestedIPAddressSerializer(many=True, read_only=True) + ip_addresses = IPAddressSerializer(nested=True, many=True, read_only=True) class Meta: model = FHRPGroup diff --git a/netbox/ipam/api/serializers_/services.py b/netbox/ipam/api/serializers_/services.py index 8904f819c..407739667 100644 --- a/netbox/ipam/api/serializers_/services.py +++ b/netbox/ipam/api/serializers_/services.py @@ -6,7 +6,7 @@ from ipam.models import IPAddress, Service, ServiceTemplate from netbox.api.fields import ChoiceField, SerializedPKRelatedField from netbox.api.serializers import NetBoxModelSerializer from virtualization.api.serializers_.virtualmachines import VirtualMachineSerializer -from ..nested_serializers import * +from .ip import IPAddressSerializer __all__ = ( 'ServiceSerializer', @@ -34,7 +34,8 @@ class ServiceSerializer(NetBoxModelSerializer): protocol = ChoiceField(choices=ServiceProtocolChoices, required=False) ipaddresses = SerializedPKRelatedField( queryset=IPAddress.objects.all(), - serializer=NestedIPAddressSerializer, + serializer=IPAddressSerializer, + nested=True, required=False, many=True ) diff --git a/netbox/ipam/api/serializers_/vrfs.py b/netbox/ipam/api/serializers_/vrfs.py index 5e5d0e725..fdb5f98ab 100644 --- a/netbox/ipam/api/serializers_/vrfs.py +++ b/netbox/ipam/api/serializers_/vrfs.py @@ -4,7 +4,6 @@ from ipam.models import RouteTarget, VRF from netbox.api.fields import RelatedObjectCountField, SerializedPKRelatedField from netbox.api.serializers import NetBoxModelSerializer from tenancy.api.serializers_.tenants import TenantSerializer -from ..nested_serializers import * __all__ = ( 'RouteTargetSerializer', @@ -12,18 +11,31 @@ __all__ = ( ) +class RouteTargetSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:routetarget-detail') + tenant = TenantSerializer(nested=True, required=False, allow_null=True) + + class Meta: + model = RouteTarget + fields = [ + 'id', 'url', 'display', 'name', 'tenant', 'description', 'comments', 'tags', 'custom_fields', 'created', + 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') + + class VRFSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vrf-detail') tenant = TenantSerializer(nested=True, required=False, allow_null=True) import_targets = SerializedPKRelatedField( queryset=RouteTarget.objects.all(), - serializer=NestedRouteTargetSerializer, + serializer=RouteTargetSerializer, required=False, many=True ) export_targets = SerializedPKRelatedField( queryset=RouteTarget.objects.all(), - serializer=NestedRouteTargetSerializer, + serializer=RouteTargetSerializer, required=False, many=True ) @@ -40,16 +52,3 @@ class VRFSerializer(NetBoxModelSerializer): 'prefix_count', ] brief_fields = ('id', 'url', 'display', 'name', 'rd', 'description', 'prefix_count') - - -class RouteTargetSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='ipam-api:routetarget-detail') - tenant = TenantSerializer(nested=True, required=False, allow_null=True) - - class Meta: - model = RouteTarget - fields = [ - 'id', 'url', 'display', 'name', 'tenant', 'description', 'comments', 'tags', 'custom_fields', 'created', - 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') diff --git a/netbox/netbox/api/fields.py b/netbox/netbox/api/fields.py index 241dce0a0..08ffd0bc4 100644 --- a/netbox/netbox/api/fields.py +++ b/netbox/netbox/api/fields.py @@ -132,13 +132,15 @@ class SerializedPKRelatedField(PrimaryKeyRelatedField): Extends PrimaryKeyRelatedField to return a serialized object on read. This is useful for representing related objects in a ManyToManyField while still allowing a set of primary keys to be written. """ - def __init__(self, serializer, **kwargs): + def __init__(self, serializer, nested=False, **kwargs): self.serializer = serializer + self.nested = nested self.pk_field = kwargs.pop('pk_field', None) + super().__init__(**kwargs) def to_representation(self, value): - return self.serializer(value, context={'request': self.context['request']}).data + return self.serializer(value, nested=self.nested, context={'request': self.context['request']}).data @extend_schema_field(OpenApiTypes.INT64) diff --git a/netbox/users/api/serializers_/permissions.py b/netbox/users/api/serializers_/permissions.py index a04c032f1..ac29d1a8b 100644 --- a/netbox/users/api/serializers_/permissions.py +++ b/netbox/users/api/serializers_/permissions.py @@ -6,7 +6,7 @@ from rest_framework import serializers from netbox.api.fields import ContentTypeField, SerializedPKRelatedField from netbox.api.serializers import ValidatedModelSerializer from users.models import ObjectPermission -from ..nested_serializers import * +from .users import GroupSerializer, UserSerializer __all__ = ( 'ObjectPermissionSerializer', @@ -21,13 +21,15 @@ class ObjectPermissionSerializer(ValidatedModelSerializer): ) groups = SerializedPKRelatedField( queryset=Group.objects.all(), - serializer=NestedGroupSerializer, + serializer=GroupSerializer, + nested=True, required=False, many=True ) users = SerializedPKRelatedField( queryset=get_user_model().objects.all(), - serializer=NestedUserSerializer, + serializer=UserSerializer, + nested=True, required=False, many=True ) diff --git a/netbox/users/api/serializers_/users.py b/netbox/users/api/serializers_/users.py index 40f3b3392..b34ddda25 100644 --- a/netbox/users/api/serializers_/users.py +++ b/netbox/users/api/serializers_/users.py @@ -6,7 +6,6 @@ from rest_framework import serializers from netbox.api.fields import SerializedPKRelatedField from netbox.api.serializers import ValidatedModelSerializer -from ..nested_serializers import * __all__ = ( 'GroupSerializer', @@ -14,11 +13,22 @@ __all__ = ( ) +class GroupSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='users-api:group-detail') + user_count = serializers.IntegerField(read_only=True) + + class Meta: + model = Group + fields = ('id', 'url', 'display', 'name', 'user_count') + brief_fields = ('id', 'url', 'display', 'name') + + class UserSerializer(ValidatedModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='users-api:user-detail') groups = SerializedPKRelatedField( queryset=Group.objects.all(), - serializer=NestedGroupSerializer, + serializer=GroupSerializer, + nested=True, required=False, many=True ) @@ -60,13 +70,3 @@ class UserSerializer(ValidatedModelSerializer): if full_name := obj.get_full_name(): return f"{obj.username} ({full_name})" return obj.username - - -class GroupSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='users-api:group-detail') - user_count = serializers.IntegerField(read_only=True) - - class Meta: - model = Group - fields = ('id', 'url', 'display', 'name', 'user_count') - brief_fields = ('id', 'url', 'display', 'name') diff --git a/netbox/virtualization/api/serializers_/virtualmachines.py b/netbox/virtualization/api/serializers_/virtualmachines.py index b9780cec7..689ed3179 100644 --- a/netbox/virtualization/api/serializers_/virtualmachines.py +++ b/netbox/virtualization/api/serializers_/virtualmachines.py @@ -6,8 +6,7 @@ from dcim.api.serializers_.platforms import PlatformSerializer from dcim.api.serializers_.roles import DeviceRoleSerializer from dcim.api.serializers_.sites import SiteSerializer from dcim.choices import InterfaceModeChoices -from extras.api.serializers_.provisioning import ConfigTemplateSerializer -from ipam.api.nested_serializers import NestedVLANSerializer +from extras.api.serializers_.configtemplates import ConfigTemplateSerializer from ipam.api.serializers_.ip import IPAddressSerializer from ipam.api.serializers_.vlans import VLANSerializer from ipam.api.serializers_.vrfs import VRFSerializer @@ -18,9 +17,8 @@ from tenancy.api.serializers_.tenants import TenantSerializer from virtualization.choices import * from virtualization.models import VirtualDisk, VirtualMachine, VMInterface from vpn.api.serializers_.l2vpn import L2VPNTerminationSerializer -from ..nested_serializers import * - from .clusters import ClusterSerializer +from ..nested_serializers import * __all__ = ( 'VMInterfaceSerializer', @@ -89,7 +87,8 @@ class VMInterfaceSerializer(NetBoxModelSerializer): untagged_vlan = VLANSerializer(nested=True, required=False, allow_null=True) tagged_vlans = SerializedPKRelatedField( queryset=VLAN.objects.all(), - serializer=NestedVLANSerializer, + serializer=VLANSerializer, + nested=True, required=False, many=True ) diff --git a/netbox/vpn/api/serializers_/crypto.py b/netbox/vpn/api/serializers_/crypto.py index ed03fbc3a..3ee30b754 100644 --- a/netbox/vpn/api/serializers_/crypto.py +++ b/netbox/vpn/api/serializers_/crypto.py @@ -4,7 +4,6 @@ from netbox.api.fields import ChoiceField, SerializedPKRelatedField from netbox.api.serializers import NetBoxModelSerializer from vpn.choices import * from vpn.models import IKEPolicy, IKEProposal, IPSecPolicy, IPSecProfile, IPSecProposal -from ..nested_serializers import * __all__ = ( 'IKEPolicySerializer', @@ -54,7 +53,8 @@ class IKEPolicySerializer(NetBoxModelSerializer): ) proposals = SerializedPKRelatedField( queryset=IKEProposal.objects.all(), - serializer=NestedIKEProposalSerializer, + serializer=IKEProposalSerializer, + nested=True, required=False, many=True ) @@ -94,7 +94,8 @@ class IPSecPolicySerializer(NetBoxModelSerializer): ) proposals = SerializedPKRelatedField( queryset=IPSecProposal.objects.all(), - serializer=NestedIPSecProposalSerializer, + serializer=IPSecProposalSerializer, + nested=True, required=False, many=True ) diff --git a/netbox/vpn/api/serializers_/l2vpn.py b/netbox/vpn/api/serializers_/l2vpn.py index 00523910b..1c2a1cb38 100644 --- a/netbox/vpn/api/serializers_/l2vpn.py +++ b/netbox/vpn/api/serializers_/l2vpn.py @@ -2,7 +2,7 @@ from django.contrib.contenttypes.models import ContentType from drf_spectacular.utils import extend_schema_field from rest_framework import serializers -from ipam.api.nested_serializers import NestedRouteTargetSerializer +from ipam.api.serializers_.vrfs import RouteTargetSerializer from ipam.models import RouteTarget from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField from netbox.api.serializers import NetBoxModelSerializer @@ -23,13 +23,15 @@ class L2VPNSerializer(NetBoxModelSerializer): type = ChoiceField(choices=L2VPNTypeChoices, required=False) import_targets = SerializedPKRelatedField( queryset=RouteTarget.objects.all(), - serializer=NestedRouteTargetSerializer, + serializer=RouteTargetSerializer, + nested=True, required=False, many=True ) export_targets = SerializedPKRelatedField( queryset=RouteTarget.objects.all(), - serializer=NestedRouteTargetSerializer, + serializer=RouteTargetSerializer, + nested=True, required=False, many=True ) From 78e284c14f05eeb18d67ba90ed80a6b75e9d8cc6 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 27 Feb 2024 14:56:36 -0500 Subject: [PATCH 04/47] Initialize dynamically-resolved serializers with nested=True --- netbox/dcim/api/serializers_/base.py | 5 ++--- netbox/dcim/api/serializers_/cables.py | 13 ++++++------- .../dcim/api/serializers_/device_components.py | 7 +++---- .../api/serializers_/devicetype_components.py | 5 ++--- netbox/dcim/api/views.py | 11 ++++------- netbox/extras/api/customfields.py | 18 +++++++----------- netbox/extras/api/serializers_/attachments.py | 6 +++--- netbox/extras/api/serializers_/bookmarks.py | 6 +++--- .../extras/api/serializers_/change_logging.py | 8 ++------ netbox/extras/api/serializers_/events.py | 8 ++------ netbox/extras/api/serializers_/journaling.py | 8 ++------ netbox/ipam/api/serializers_/fhrpgroups.py | 5 ++--- netbox/ipam/api/serializers_/ip.py | 12 +++++------- netbox/ipam/api/serializers_/vlans.py | 7 ++----- netbox/netbox/api/serializers/generic.py | 6 ++---- netbox/netbox/constants.py | 1 + netbox/tenancy/api/serializers_/contacts.py | 5 ++--- netbox/vpn/api/serializers_/l2vpn.py | 5 ++--- netbox/vpn/api/serializers_/tunnels.py | 6 ++---- 19 files changed, 54 insertions(+), 88 deletions(-) diff --git a/netbox/dcim/api/serializers_/base.py b/netbox/dcim/api/serializers_/base.py index 3ce44ebcf..48f4967e3 100644 --- a/netbox/dcim/api/serializers_/base.py +++ b/netbox/dcim/api/serializers_/base.py @@ -2,7 +2,6 @@ from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import extend_schema_field from rest_framework import serializers -from netbox.constants import NESTED_SERIALIZER_PREFIX from utilities.api import get_serializer_for_model __all__ = ( @@ -29,9 +28,9 @@ class ConnectedEndpointsSerializer(serializers.ModelSerializer): Return the appropriate serializer for the type of connected object. """ if endpoints := obj.connected_endpoints: - serializer = get_serializer_for_model(endpoints[0], prefix=NESTED_SERIALIZER_PREFIX) + serializer = get_serializer_for_model(endpoints[0]) context = {'request': self.context['request']} - return serializer(endpoints, many=True, context=context).data + return serializer(endpoints, nested=True, many=True, context=context).data @extend_schema_field(serializers.BooleanField) def get_connected_endpoints_reachable(self, obj): diff --git a/netbox/dcim/api/serializers_/cables.py b/netbox/dcim/api/serializers_/cables.py index 099cd8945..94a125d0c 100644 --- a/netbox/dcim/api/serializers_/cables.py +++ b/netbox/dcim/api/serializers_/cables.py @@ -8,7 +8,6 @@ from dcim.constants import * from dcim.models import Cable, CablePath, CableTermination from netbox.api.fields import ChoiceField, ContentTypeField from netbox.api.serializers import GenericObjectSerializer, NetBoxModelSerializer -from netbox.constants import NESTED_SERIALIZER_PREFIX from tenancy.api.serializers_.tenants import TenantSerializer from utilities.api import get_serializer_for_model @@ -67,9 +66,9 @@ class CableTerminationSerializer(NetBoxModelSerializer): @extend_schema_field(serializers.JSONField(allow_null=True)) def get_termination(self, obj): - serializer = get_serializer_for_model(obj.termination, prefix=NESTED_SERIALIZER_PREFIX) + serializer = get_serializer_for_model(obj.termination) context = {'request': self.context['request']} - return serializer(obj.termination, context=context).data + return serializer(obj.termination, nested=True, context=context).data class CablePathSerializer(serializers.ModelSerializer): @@ -83,9 +82,9 @@ class CablePathSerializer(serializers.ModelSerializer): def get_path(self, obj): ret = [] for nodes in obj.path_objects: - serializer = get_serializer_for_model(nodes[0], prefix=NESTED_SERIALIZER_PREFIX) + serializer = get_serializer_for_model(nodes[0]) context = {'request': self.context['request']} - ret.append(serializer(nodes, context=context, many=True).data) + ret.append(serializer(nodes, nested=True, many=True, context=context).data) return ret @@ -118,9 +117,9 @@ class CabledObjectSerializer(serializers.ModelSerializer): return [] # Return serialized peer termination objects - serializer = get_serializer_for_model(obj.link_peers[0], prefix=NESTED_SERIALIZER_PREFIX) + serializer = get_serializer_for_model(obj.link_peers[0]) context = {'request': self.context['request']} - return serializer(obj.link_peers, context=context, many=True).data + return serializer(obj.link_peers, nested=True, many=True, context=context).data @extend_schema_field(serializers.BooleanField) def get__occupied(self, obj): diff --git a/netbox/dcim/api/serializers_/device_components.py b/netbox/dcim/api/serializers_/device_components.py index a3e9d6e97..2e473567d 100644 --- a/netbox/dcim/api/serializers_/device_components.py +++ b/netbox/dcim/api/serializers_/device_components.py @@ -13,10 +13,10 @@ from ipam.api.serializers_.vrfs import VRFSerializer from ipam.models import VLAN from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer -from netbox.constants import NESTED_SERIALIZER_PREFIX from utilities.api import get_serializer_for_model from vpn.api.serializers_.l2vpn import L2VPNTerminationSerializer from wireless.api.nested_serializers import NestedWirelessLinkSerializer +from wireless.api.serializers_.wirelesslans import WirelessLANSerializer from wireless.choices import * from wireless.models import WirelessLAN from .base import ConnectedEndpointsSerializer @@ -24,7 +24,6 @@ from .cables import CabledObjectSerializer from .devices import DeviceSerializer, ModuleSerializer, VirtualDeviceContextSerializer from .manufacturers import ManufacturerSerializer from .roles import InventoryItemRoleSerializer -from wireless.api.serializers_.wirelesslans import WirelessLANSerializer from ..nested_serializers import * __all__ = ( @@ -364,6 +363,6 @@ class InventoryItemSerializer(NetBoxModelSerializer): def get_component(self, obj): if obj.component is None: return None - serializer = get_serializer_for_model(obj.component, prefix=NESTED_SERIALIZER_PREFIX) + serializer = get_serializer_for_model(obj.component) context = {'request': self.context['request']} - return serializer(obj.component, context=context).data + return serializer(obj.component, nested=True, context=context).data diff --git a/netbox/dcim/api/serializers_/devicetype_components.py b/netbox/dcim/api/serializers_/devicetype_components.py index 958b51ece..259a5df27 100644 --- a/netbox/dcim/api/serializers_/devicetype_components.py +++ b/netbox/dcim/api/serializers_/devicetype_components.py @@ -10,7 +10,6 @@ from dcim.models import ( ) from netbox.api.fields import ChoiceField, ContentTypeField from netbox.api.serializers import ValidatedModelSerializer -from netbox.constants import NESTED_SERIALIZER_PREFIX from utilities.api import get_serializer_for_model from wireless.choices import * from .devicetypes import DeviceTypeSerializer, ModuleTypeSerializer @@ -323,6 +322,6 @@ class InventoryItemTemplateSerializer(ValidatedModelSerializer): def get_component(self, obj): if obj.component is None: return None - serializer = get_serializer_for_model(obj.component, prefix=NESTED_SERIALIZER_PREFIX) + serializer = get_serializer_for_model(obj.component) context = {'request': self.context['request']} - return serializer(obj.component, context=context).data + return serializer(obj.component, nested=True, context=context).data diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 8a674656d..668af28da 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -7,7 +7,6 @@ from rest_framework.response import Response from rest_framework.routers import APIRootView from rest_framework.viewsets import ViewSet -from circuits.models import Circuit from dcim import filtersets from dcim.constants import CABLE_TRACE_SVG_DEFAULT_WIDTH from dcim.models import * @@ -18,10 +17,8 @@ from netbox.api.metadata import ContentTypeMetadata from netbox.api.pagination import StripCountAnnotationsPaginator from netbox.api.viewsets import NetBoxModelViewSet, MPTTLockedMixin from netbox.api.viewsets.mixins import SequentialBulkCreatesMixin -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 . import serializers from .exceptions import MissingFilterException @@ -60,16 +57,16 @@ class PathEndpointMixin(object): # Serialize path objects, iterating over each three-tuple in the path for near_ends, cable, far_ends in obj.trace(): if near_ends: - serializer_a = get_serializer_for_model(near_ends[0], prefix=NESTED_SERIALIZER_PREFIX) - near_ends = serializer_a(near_ends, many=True, context={'request': request}).data + serializer_a = get_serializer_for_model(near_ends[0]) + near_ends = serializer_a(near_ends, nested=True, many=True, context={'request': request}).data else: # Path is split; stop here break if cable: cable = serializers.TracedCableSerializer(cable[0], context={'request': request}).data if far_ends: - serializer_b = get_serializer_for_model(far_ends[0], prefix=NESTED_SERIALIZER_PREFIX) - far_ends = serializer_b(far_ends, many=True, context={'request': request}).data + serializer_b = get_serializer_for_model(far_ends[0]) + far_ends = serializer_b(far_ends, nested=True, many=True, context={'request': request}).data path.append((near_ends, cable, far_ends)) diff --git a/netbox/extras/api/customfields.py b/netbox/extras/api/customfields.py index 6cd3a245e..fd99ce703 100644 --- a/netbox/extras/api/customfields.py +++ b/netbox/extras/api/customfields.py @@ -1,13 +1,12 @@ from django.contrib.contenttypes.models import ContentType from django.utils.translation import gettext as _ -from drf_spectacular.utils import extend_schema_field from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import extend_schema_field from rest_framework.fields import Field from rest_framework.serializers import ValidationError from extras.choices import CustomFieldTypeChoices from extras.models import CustomField -from netbox.constants import NESTED_SERIALIZER_PREFIX from utilities.api import get_serializer_for_model @@ -58,11 +57,11 @@ class CustomFieldsDataField(Field): for cf in self._get_custom_fields(): value = cf.deserialize(obj.get(cf.name)) if value is not None and cf.type == CustomFieldTypeChoices.TYPE_OBJECT: - serializer = get_serializer_for_model(cf.object_type.model_class(), prefix=NESTED_SERIALIZER_PREFIX) - value = serializer(value, context=self.parent.context).data + serializer = get_serializer_for_model(cf.object_type.model_class()) + value = serializer(value, nested=True, context=self.parent.context).data elif value is not None and cf.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT: - serializer = get_serializer_for_model(cf.object_type.model_class(), prefix=NESTED_SERIALIZER_PREFIX) - value = serializer(value, many=True, context=self.parent.context).data + serializer = get_serializer_for_model(cf.object_type.model_class()) + value = serializer(value, nested=True, many=True, context=self.parent.context).data data[cf.name] = value return data @@ -80,12 +79,9 @@ class CustomFieldsDataField(Field): CustomFieldTypeChoices.TYPE_OBJECT, CustomFieldTypeChoices.TYPE_MULTIOBJECT ): - serializer_class = get_serializer_for_model( - model=cf.object_type.model_class(), - prefix=NESTED_SERIALIZER_PREFIX - ) + serializer_class = get_serializer_for_model(cf.object_type.model_class()) many = cf.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT - serializer = serializer_class(data=data[cf.name], many=many, context=self.parent.context) + serializer = serializer_class(data=data[cf.name], nested=True, many=many, context=self.parent.context) if serializer.is_valid(): data[cf.name] = [obj['id'] for obj in serializer.data] if many else serializer.data['id'] else: diff --git a/netbox/extras/api/serializers_/attachments.py b/netbox/extras/api/serializers_/attachments.py index 2a6730557..e26d8516b 100644 --- a/netbox/extras/api/serializers_/attachments.py +++ b/netbox/extras/api/serializers_/attachments.py @@ -6,7 +6,6 @@ from core.models import ContentType from extras.models import ImageAttachment from netbox.api.fields import ContentTypeField from netbox.api.serializers import ValidatedModelSerializer -from netbox.constants import NESTED_SERIALIZER_PREFIX from utilities.api import get_serializer_for_model __all__ = ( @@ -46,5 +45,6 @@ class ImageAttachmentSerializer(ValidatedModelSerializer): @extend_schema_field(serializers.JSONField(allow_null=True)) def get_parent(self, obj): - serializer = get_serializer_for_model(obj.parent, prefix=NESTED_SERIALIZER_PREFIX) - return serializer(obj.parent, context={'request': self.context['request']}).data + serializer = get_serializer_for_model(obj.parent) + context = {'request': self.context['request']} + return serializer(obj.parent, nested=True, context=context).data diff --git a/netbox/extras/api/serializers_/bookmarks.py b/netbox/extras/api/serializers_/bookmarks.py index e8b34fb0f..8140d2d84 100644 --- a/netbox/extras/api/serializers_/bookmarks.py +++ b/netbox/extras/api/serializers_/bookmarks.py @@ -5,7 +5,6 @@ from core.models import ContentType from extras.models import Bookmark from netbox.api.fields import ContentTypeField from netbox.api.serializers import ValidatedModelSerializer -from netbox.constants import NESTED_SERIALIZER_PREFIX from users.api.serializers_.users import UserSerializer from utilities.api import get_serializer_for_model @@ -31,5 +30,6 @@ class BookmarkSerializer(ValidatedModelSerializer): @extend_schema_field(serializers.JSONField(allow_null=True)) def get_object(self, instance): - serializer = get_serializer_for_model(instance.object, prefix=NESTED_SERIALIZER_PREFIX) - return serializer(instance.object, context={'request': self.context['request']}).data + serializer = get_serializer_for_model(instance.object) + context = {'request': self.context['request']} + return serializer(instance.object, nested=True, context=context).data diff --git a/netbox/extras/api/serializers_/change_logging.py b/netbox/extras/api/serializers_/change_logging.py index bf63a777f..32585637c 100644 --- a/netbox/extras/api/serializers_/change_logging.py +++ b/netbox/extras/api/serializers_/change_logging.py @@ -6,7 +6,6 @@ from extras.models import ObjectChange from netbox.api.exceptions import SerializerNotFound from netbox.api.fields import ChoiceField, ContentTypeField from netbox.api.serializers import BaseModelSerializer -from netbox.constants import NESTED_SERIALIZER_PREFIX from users.api.serializers_.users import UserSerializer from utilities.api import get_serializer_for_model @@ -48,12 +47,9 @@ class ObjectChangeSerializer(BaseModelSerializer): return None try: - serializer = get_serializer_for_model(obj.changed_object, prefix=NESTED_SERIALIZER_PREFIX) + serializer = get_serializer_for_model(obj.changed_object) except SerializerNotFound: return obj.object_repr - context = { - 'request': self.context['request'] - } - data = serializer(obj.changed_object, context=context).data + data = serializer(obj.changed_object, nested=True, context={'request': self.context['request']}).data return data diff --git a/netbox/extras/api/serializers_/events.py b/netbox/extras/api/serializers_/events.py index 6c64dd335..6f369d63d 100644 --- a/netbox/extras/api/serializers_/events.py +++ b/netbox/extras/api/serializers_/events.py @@ -7,7 +7,6 @@ from extras.choices import * from extras.models import EventRule, Webhook from netbox.api.fields import ChoiceField, ContentTypeField from netbox.api.serializers import NetBoxModelSerializer -from netbox.constants import NESTED_SERIALIZER_PREFIX from utilities.api import get_serializer_for_model from .scripts import ScriptSerializer @@ -51,11 +50,8 @@ class EventRuleSerializer(NetBoxModelSerializer): instance = script.python_class() if script.python_class else None return ScriptSerializer(instance, nested=True, context=context).data else: - serializer = get_serializer_for_model( - model=instance.action_object_type.model_class(), - prefix=NESTED_SERIALIZER_PREFIX - ) - return serializer(instance.action_object, context=context).data + serializer = get_serializer_for_model(instance.action_object_type.model_class()) + return serializer(instance.action_object, nested=True, context=context).data # diff --git a/netbox/extras/api/serializers_/journaling.py b/netbox/extras/api/serializers_/journaling.py index a7d449152..848b2842a 100644 --- a/netbox/extras/api/serializers_/journaling.py +++ b/netbox/extras/api/serializers_/journaling.py @@ -8,7 +8,6 @@ from extras.choices import * from extras.models import JournalEntry from netbox.api.fields import ChoiceField, ContentTypeField from netbox.api.serializers import NetBoxModelSerializer -from netbox.constants import NESTED_SERIALIZER_PREFIX from utilities.api import get_serializer_for_model __all__ = ( @@ -59,9 +58,6 @@ class JournalEntrySerializer(NetBoxModelSerializer): @extend_schema_field(serializers.JSONField(allow_null=True)) def get_assigned_object(self, instance): - serializer = get_serializer_for_model( - instance.assigned_object_type.model_class(), - prefix=NESTED_SERIALIZER_PREFIX - ) + serializer = get_serializer_for_model(instance.assigned_object_type.model_class()) context = {'request': self.context['request']} - return serializer(instance.assigned_object, context=context).data + return serializer(instance.assigned_object, nested=True, context=context).data diff --git a/netbox/ipam/api/serializers_/fhrpgroups.py b/netbox/ipam/api/serializers_/fhrpgroups.py index 986ae64d5..9bf1d4548 100644 --- a/netbox/ipam/api/serializers_/fhrpgroups.py +++ b/netbox/ipam/api/serializers_/fhrpgroups.py @@ -5,7 +5,6 @@ from rest_framework import serializers from ipam.models import FHRPGroup, FHRPGroupAssignment from netbox.api.fields import ContentTypeField from netbox.api.serializers import NetBoxModelSerializer -from netbox.constants import NESTED_SERIALIZER_PREFIX from utilities.api import get_serializer_for_model from .ip import IPAddressSerializer @@ -48,6 +47,6 @@ class FHRPGroupAssignmentSerializer(NetBoxModelSerializer): def get_interface(self, obj): if obj.interface is None: return None - serializer = get_serializer_for_model(obj.interface, prefix=NESTED_SERIALIZER_PREFIX) + serializer = get_serializer_for_model(obj.interface) context = {'request': self.context['request']} - return serializer(obj.interface, context=context).data + return serializer(obj.interface, nested=True, context=context).data diff --git a/netbox/ipam/api/serializers_/ip.py b/netbox/ipam/api/serializers_/ip.py index 33dcca326..e5fa81314 100644 --- a/netbox/ipam/api/serializers_/ip.py +++ b/netbox/ipam/api/serializers_/ip.py @@ -8,16 +8,14 @@ from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS from ipam.models import Aggregate, IPAddress, IPRange, Prefix from netbox.api.fields import ChoiceField, ContentTypeField from netbox.api.serializers import NetBoxModelSerializer -from netbox.constants import NESTED_SERIALIZER_PREFIX from tenancy.api.serializers_.tenants import TenantSerializer from utilities.api import get_serializer_for_model -from ..field_serializers import IPAddressField, IPNetworkField -from ..nested_serializers import * - from .asns import RIRSerializer -from .vrfs import VRFSerializer from .roles import RoleSerializer from .vlans import VLANSerializer +from .vrfs import VRFSerializer +from ..field_serializers import IPAddressField, IPNetworkField +from ..nested_serializers import * __all__ = ( 'AggregateSerializer', @@ -174,9 +172,9 @@ class IPAddressSerializer(NetBoxModelSerializer): def get_assigned_object(self, obj): if obj.assigned_object is None: return None - serializer = get_serializer_for_model(obj.assigned_object, prefix=NESTED_SERIALIZER_PREFIX) + serializer = get_serializer_for_model(obj.assigned_object) context = {'request': self.context['request']} - return serializer(obj.assigned_object, context=context).data + return serializer(obj.assigned_object, nested=True, context=context).data class AvailableIPSerializer(serializers.Serializer): diff --git a/netbox/ipam/api/serializers_/vlans.py b/netbox/ipam/api/serializers_/vlans.py index c92e09a70..a400f949b 100644 --- a/netbox/ipam/api/serializers_/vlans.py +++ b/netbox/ipam/api/serializers_/vlans.py @@ -8,11 +8,9 @@ from ipam.constants import VLANGROUP_SCOPE_TYPES from ipam.models import VLAN, VLANGroup from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField from netbox.api.serializers import NetBoxModelSerializer -from netbox.constants import NESTED_SERIALIZER_PREFIX from tenancy.api.serializers_.tenants import TenantSerializer from utilities.api import get_serializer_for_model from vpn.api.serializers_.l2vpn import L2VPNTerminationSerializer - from .roles import RoleSerializer __all__ = ( @@ -53,10 +51,9 @@ class VLANGroupSerializer(NetBoxModelSerializer): def get_scope(self, obj): if obj.scope_id is None: return None - serializer = get_serializer_for_model(obj.scope, prefix=NESTED_SERIALIZER_PREFIX) + serializer = get_serializer_for_model(obj.scope) context = {'request': self.context['request']} - - return serializer(obj.scope, context=context).data + return serializer(obj.scope, nested=True, context=context).data class VLANSerializer(NetBoxModelSerializer): diff --git a/netbox/netbox/api/serializers/generic.py b/netbox/netbox/api/serializers/generic.py index 545ebb936..fb4fab8b0 100644 --- a/netbox/netbox/api/serializers/generic.py +++ b/netbox/netbox/api/serializers/generic.py @@ -3,7 +3,6 @@ from drf_spectacular.utils import extend_schema_field from rest_framework import serializers from netbox.api.fields import ContentTypeField -from netbox.constants import NESTED_SERIALIZER_PREFIX from utilities.api import get_serializer_for_model from utilities.utils import content_type_identifier @@ -40,6 +39,5 @@ class GenericObjectSerializer(serializers.Serializer): @extend_schema_field(serializers.JSONField(allow_null=True)) def get_object(self, obj): - serializer = get_serializer_for_model(obj, prefix=NESTED_SERIALIZER_PREFIX) - # context = {'request': self.context['request']} - return serializer(obj, context=self.context).data + serializer = get_serializer_for_model(obj) + return serializer(obj, nested=True, context=self.context).data diff --git a/netbox/netbox/constants.py b/netbox/netbox/constants.py index 547e2079b..6a6928021 100644 --- a/netbox/netbox/constants.py +++ b/netbox/netbox/constants.py @@ -1,4 +1,5 @@ # Prefix for nested serializers +# TODO: Remove in v4.1 NESTED_SERIALIZER_PREFIX = 'Nested' # RQ queue names diff --git a/netbox/tenancy/api/serializers_/contacts.py b/netbox/tenancy/api/serializers_/contacts.py index ddac48815..925e04cfd 100644 --- a/netbox/tenancy/api/serializers_/contacts.py +++ b/netbox/tenancy/api/serializers_/contacts.py @@ -5,7 +5,6 @@ from rest_framework import serializers from netbox.api.fields import ChoiceField, ContentTypeField from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer -from netbox.constants import NESTED_SERIALIZER_PREFIX from tenancy.choices import ContactPriorityChoices from tenancy.models import ContactAssignment, Contact, ContactGroup, ContactRole from utilities.api import get_serializer_for_model @@ -77,6 +76,6 @@ class ContactAssignmentSerializer(NetBoxModelSerializer): @extend_schema_field(OpenApiTypes.OBJECT) def get_object(self, instance): - serializer = get_serializer_for_model(instance.content_type.model_class(), prefix=NESTED_SERIALIZER_PREFIX) + serializer = get_serializer_for_model(instance.content_type.model_class()) context = {'request': self.context['request']} - return serializer(instance.object, context=context).data + return serializer(instance.object, nested=True, context=context).data diff --git a/netbox/vpn/api/serializers_/l2vpn.py b/netbox/vpn/api/serializers_/l2vpn.py index 1c2a1cb38..3b78d50c9 100644 --- a/netbox/vpn/api/serializers_/l2vpn.py +++ b/netbox/vpn/api/serializers_/l2vpn.py @@ -6,7 +6,6 @@ from ipam.api.serializers_.vrfs import RouteTargetSerializer from ipam.models import RouteTarget from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField from netbox.api.serializers import NetBoxModelSerializer -from netbox.constants import NESTED_SERIALIZER_PREFIX from tenancy.api.serializers_.tenants import TenantSerializer from utilities.api import get_serializer_for_model from vpn.choices import * @@ -66,6 +65,6 @@ class L2VPNTerminationSerializer(NetBoxModelSerializer): @extend_schema_field(serializers.JSONField(allow_null=True)) def get_assigned_object(self, instance): - serializer = get_serializer_for_model(instance.assigned_object, prefix=NESTED_SERIALIZER_PREFIX) + serializer = get_serializer_for_model(instance.assigned_object) context = {'request': self.context['request']} - return serializer(instance.assigned_object, context=context).data + return serializer(instance.assigned_object, nested=True, context=context).data diff --git a/netbox/vpn/api/serializers_/tunnels.py b/netbox/vpn/api/serializers_/tunnels.py index 6d4f334f0..3e131b1c1 100644 --- a/netbox/vpn/api/serializers_/tunnels.py +++ b/netbox/vpn/api/serializers_/tunnels.py @@ -5,12 +5,10 @@ from rest_framework import serializers from ipam.api.serializers_.ip import IPAddressSerializer from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField from netbox.api.serializers import NetBoxModelSerializer -from netbox.constants import NESTED_SERIALIZER_PREFIX from tenancy.api.serializers_.tenants import TenantSerializer from utilities.api import get_serializer_for_model from vpn.choices import * from vpn.models import Tunnel, TunnelGroup, TunnelTermination - from .crypto import IPSecProfileSerializer __all__ = ( @@ -109,6 +107,6 @@ class TunnelTerminationSerializer(NetBoxModelSerializer): @extend_schema_field(serializers.JSONField(allow_null=True)) def get_termination(self, obj): - serializer = get_serializer_for_model(obj.termination, prefix=NESTED_SERIALIZER_PREFIX) + serializer = get_serializer_for_model(obj.termination) context = {'request': self.context['request']} - return serializer(obj.termination, context=context).data + return serializer(obj.termination, nested=True, context=context).data From 7008ffe6d8e730375b7b03198d20ee09db4c230f Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 27 Feb 2024 17:01:29 -0500 Subject: [PATCH 05/47] Rename 'requested_fields' kwarg to 'fields' on BaseSerializer --- .../dcim/api/serializers_/device_components.py | 16 ++++++++-------- netbox/netbox/api/serializers/base.py | 10 +++++----- netbox/netbox/api/viewsets/__init__.py | 2 +- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/netbox/dcim/api/serializers_/device_components.py b/netbox/dcim/api/serializers_/device_components.py index 2e473567d..87d142978 100644 --- a/netbox/dcim/api/serializers_/device_components.py +++ b/netbox/dcim/api/serializers_/device_components.py @@ -45,7 +45,7 @@ class ConsoleServerPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, device = DeviceSerializer(nested=True) module = ModuleSerializer( nested=True, - requested_fields=('id', 'url', 'display', 'device', 'module_bay'), + fields=('id', 'url', 'display', 'device', 'module_bay'), required=False, allow_null=True ) @@ -76,7 +76,7 @@ class ConsolePortSerializer(NetBoxModelSerializer, CabledObjectSerializer, Conne device = DeviceSerializer(nested=True) module = ModuleSerializer( nested=True, - requested_fields=('id', 'url', 'display', 'device', 'module_bay'), + fields=('id', 'url', 'display', 'device', 'module_bay'), required=False, allow_null=True ) @@ -107,7 +107,7 @@ class PowerPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect device = DeviceSerializer(nested=True) module = ModuleSerializer( nested=True, - requested_fields=('id', 'url', 'display', 'device', 'module_bay'), + fields=('id', 'url', 'display', 'device', 'module_bay'), required=False, allow_null=True ) @@ -134,7 +134,7 @@ class PowerOutletSerializer(NetBoxModelSerializer, CabledObjectSerializer, Conne device = DeviceSerializer(nested=True) module = ModuleSerializer( nested=True, - requested_fields=('id', 'url', 'display', 'device', 'module_bay'), + fields=('id', 'url', 'display', 'device', 'module_bay'), required=False, allow_null=True ) @@ -179,7 +179,7 @@ class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect ) module = ModuleSerializer( nested=True, - requested_fields=('id', 'url', 'display', 'device', 'module_bay'), + fields=('id', 'url', 'display', 'device', 'module_bay'), required=False, allow_null=True ) @@ -254,7 +254,7 @@ class RearPortSerializer(NetBoxModelSerializer, CabledObjectSerializer): device = DeviceSerializer(nested=True) module = ModuleSerializer( nested=True, - requested_fields=('id', 'url', 'display', 'device', 'module_bay'), + fields=('id', 'url', 'display', 'device', 'module_bay'), required=False, allow_null=True ) @@ -286,7 +286,7 @@ class FrontPortSerializer(NetBoxModelSerializer, CabledObjectSerializer): device = DeviceSerializer(nested=True) module = ModuleSerializer( nested=True, - requested_fields=('id', 'url', 'display', 'device', 'module_bay'), + fields=('id', 'url', 'display', 'device', 'module_bay'), required=False, allow_null=True ) @@ -308,7 +308,7 @@ class ModuleBaySerializer(NetBoxModelSerializer): device = DeviceSerializer(nested=True) installed_module = ModuleSerializer( nested=True, - requested_fields=('id', 'url', 'display', 'serial', 'description'), + fields=('id', 'url', 'display', 'serial', 'description'), required=False, allow_null=True ) diff --git a/netbox/netbox/api/serializers/base.py b/netbox/netbox/api/serializers/base.py index 7fe68d4ef..bad53144e 100644 --- a/netbox/netbox/api/serializers/base.py +++ b/netbox/netbox/api/serializers/base.py @@ -13,18 +13,18 @@ __all__ = ( class BaseModelSerializer(serializers.ModelSerializer): display = serializers.SerializerMethodField(read_only=True) - def __init__(self, *args, nested=False, requested_fields=None, **kwargs): + def __init__(self, *args, nested=False, fields=None, **kwargs): super().__init__(*args, **kwargs) self.nested = nested - if nested and not requested_fields: - requested_fields = getattr(self.Meta, 'brief_fields', None) + if nested and not fields: + fields = getattr(self.Meta, 'brief_fields', None) # If specific fields have been requested, omit the others - if requested_fields: + if fields: for field in list(self.fields.keys()): - if field not in requested_fields: + if field not in fields: self.fields.pop(field) def to_internal_value(self, data): diff --git a/netbox/netbox/api/viewsets/__init__.py b/netbox/netbox/api/viewsets/__init__.py index 2f0431efa..d72507e8a 100644 --- a/netbox/netbox/api/viewsets/__init__.py +++ b/netbox/netbox/api/viewsets/__init__.py @@ -69,7 +69,7 @@ class BaseViewSet(GenericViewSet): # If specific fields have been requested, pass them to the serializer if self.requested_fields: - kwargs['requested_fields'] = self.requested_fields + kwargs['fields'] = self.requested_fields return super().get_serializer(*args, **kwargs) From 125a493dc6baed50a834dbbc3963212796a421d1 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 29 Feb 2024 11:37:23 -0500 Subject: [PATCH 06/47] Changelog for #14438, #15042, #15087, #15131, #15238 --- docs/release-notes/version-4.0.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/release-notes/version-4.0.md b/docs/release-notes/version-4.0.md index 9bf0a4db8..4bae93fa8 100644 --- a/docs/release-notes/version-4.0.md +++ b/docs/release-notes/version-4.0.md @@ -13,6 +13,10 @@ The NetBox user interface has been completely refreshed and updated. +#### Dynamic REST API Fields ([#15087](https://github.com/netbox-community/netbox/issues/15087)) + +The REST API now supports specifying which fields to include in the response data. + ### Enhancements * [#12851](https://github.com/netbox-community/netbox/issues/12851) - Replace bleach HTML sanitization library with nh3 @@ -22,6 +26,9 @@ The NetBox user interface has been completely refreshed and updated. * [#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 * [#14729](https://github.com/netbox-community/netbox/issues/14729) - All background task views have been moved from the legacy admin UI to the main NetBox UI +* [#14438](https://github.com/netbox-community/netbox/issues/14438) - Track individual custom scripts as database objects +* [#15131](https://github.com/netbox-community/netbox/issues/15131) - Automatically annotate related object counts on REST API querysets +* [#15238](https://github.com/netbox-community/netbox/issues/15238) - Include the `description` field in "brief" REST API serializations ### Other Changes @@ -34,5 +41,6 @@ The NetBox user interface has been completely refreshed and updated. * [#14657](https://github.com/netbox-community/netbox/issues/14657) - Remove backward compatibility for old permissions mapping under `ActionsMixin` * [#14658](https://github.com/netbox-community/netbox/issues/14658) - Remove backward compatibility for importing `process_webhook()` (now `extras.webhooks.send_webhook()`) * [#14740](https://github.com/netbox-community/netbox/issues/14740) - Remove the obsolete `BootstrapMixin` form mixin class +* [#15042](https://github.com/netbox-community/netbox/issues/15042) - Rearchitect the logic for registering models & model features * [#15099](https://github.com/netbox-community/netbox/issues/15099) - Remove obsolete `device_role` and `device_role_id` filters for devices * [#15100](https://github.com/netbox-community/netbox/issues/15100) - Remove obsolete `NullableCharField` class From 3bd28e2efe9d39d9992245c955d406c77b519e4b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 1 Mar 2024 10:18:58 -0500 Subject: [PATCH 07/47] Improve serializer initialization performance --- netbox/netbox/api/serializers/base.py | 36 +++++++++++++++++++++------ 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/netbox/netbox/api/serializers/base.py b/netbox/netbox/api/serializers/base.py index bad53144e..4445f62da 100644 --- a/netbox/netbox/api/serializers/base.py +++ b/netbox/netbox/api/serializers/base.py @@ -1,4 +1,7 @@ +from functools import cached_property + from rest_framework import serializers +from rest_framework.utils.serializer_helpers import BindingDict from drf_spectacular.utils import extend_schema_field from drf_spectacular.types import OpenApiTypes @@ -14,18 +17,22 @@ class BaseModelSerializer(serializers.ModelSerializer): display = serializers.SerializerMethodField(read_only=True) def __init__(self, *args, nested=False, fields=None, **kwargs): - super().__init__(*args, **kwargs) + """ + Extends the base __init__() method to support dynamic fields. + :param nested: Set to True if this serializer is being employed within a parent serializer + :param fields: An iterable of fields to include when rendering the serialized object, If nested is + True but no fields are specified, Meta.brief_fields will be used. + """ self.nested = nested + self._requested_fields = fields + # If this serializer is nested but no fields have been specified, + # default to using Meta.brief_fields (if set) if nested and not fields: - fields = getattr(self.Meta, 'brief_fields', None) + self._requested_fields = getattr(self.Meta, 'brief_fields', None) - # If specific fields have been requested, omit the others - if fields: - for field in list(self.fields.keys()): - if field not in fields: - self.fields.pop(field) + super().__init__(*args, **kwargs) def to_internal_value(self, data): @@ -37,6 +44,21 @@ class BaseModelSerializer(serializers.ModelSerializer): return super().to_internal_value(data) + @cached_property + def fields(self): + """ + Override the fields property to check for requested fields. If defined, + return only the applicable fields. + """ + if not self._requested_fields: + return super().fields + + fields = BindingDict(self) + for key, value in self.get_fields().items(): + if key in self._requested_fields: + fields[key] = value + return fields + @extend_schema_field(OpenApiTypes.STR) def get_display(self, obj): return str(obj) From 709eac6b9838fe1129f1371ab793d25bc656913c Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 29 Feb 2024 08:53:45 -0500 Subject: [PATCH 08/47] Closes #15292: Remove obsolete device_role attribute from Device model --- docs/customization/custom-scripts.md | 2 +- docs/models/dcim/device.md | 4 ++-- netbox/dcim/api/serializers.py | 32 ++++++++++++---------------- netbox/dcim/models/devices.py | 14 ------------ netbox/dcim/tests/test_cablepaths.py | 2 +- netbox/dcim/tests/test_models.py | 24 --------------------- 6 files changed, 18 insertions(+), 60 deletions(-) diff --git a/docs/customization/custom-scripts.md b/docs/customization/custom-scripts.md index 671f3ab17..bdc3f9104 100644 --- a/docs/customization/custom-scripts.md +++ b/docs/customization/custom-scripts.md @@ -476,7 +476,7 @@ class NewBranchScript(Script): name=f'{site.slug}-switch{i}', site=site, status=DeviceStatusChoices.STATUS_PLANNED, - device_role=switch_role + role=switch_role ) switch.full_clean() switch.save() diff --git a/docs/models/dcim/device.md b/docs/models/dcim/device.md index c9f05cd93..8b38d7c89 100644 --- a/docs/models/dcim/device.md +++ b/docs/models/dcim/device.md @@ -18,9 +18,9 @@ When a device has one or more interfaces with IP addresses assigned, a primary I The device's configured name. This field is optional; devices can be unnamed. However, if set, the name must be unique to the assigned site and tenant. -### Device Role +### Role -The functional [role](./devicerole.md) assigned to this device. +The functional [device role](./devicerole.md) assigned to this device. ### Device Type diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 1bf4969e2..8fbe9fd04 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -702,7 +702,6 @@ class DeviceSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail') device_type = NestedDeviceTypeSerializer() role = NestedDeviceRoleSerializer() - device_role = NestedDeviceRoleSerializer(read_only=True, help_text='Deprecated in v3.6 in favor of `role`.') tenant = NestedTenantSerializer(required=False, allow_null=True, default=None) platform = NestedPlatformSerializer(required=False, allow_null=True) site = NestedSiteSerializer() @@ -744,13 +743,13 @@ class DeviceSerializer(NetBoxModelSerializer): class Meta: model = Device fields = [ - 'id', 'url', 'display', 'name', 'device_type', 'role', 'device_role', 'tenant', 'platform', 'serial', - 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device', - 'status', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', - 'vc_position', 'vc_priority', 'description', 'comments', 'config_template', 'local_context_data', 'tags', - 'custom_fields', 'created', 'last_updated', 'console_port_count', 'console_server_port_count', - 'power_port_count', 'power_outlet_count', 'interface_count', 'front_port_count', 'rear_port_count', - 'device_bay_count', 'module_bay_count', 'inventory_item_count', + 'id', 'url', 'display', 'name', 'device_type', 'role', 'tenant', 'platform', 'serial', 'asset_tag', 'site', + 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device', 'status', 'airflow', + 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', 'vc_position', + 'vc_priority', 'description', 'comments', 'config_template', 'local_context_data', 'tags', 'custom_fields', + 'created', 'last_updated', 'console_port_count', 'console_server_port_count', 'power_port_count', + 'power_outlet_count', 'interface_count', 'front_port_count', 'rear_port_count', 'device_bay_count', + 'module_bay_count', 'inventory_item_count', ] brief_fields = ('id', 'url', 'display', 'name', 'description') @@ -765,22 +764,19 @@ class DeviceSerializer(NetBoxModelSerializer): data['device_bay'] = NestedDeviceBaySerializer(instance=device_bay, context=context).data return data - def get_device_role(self, obj): - return obj.role - class DeviceWithConfigContextSerializer(DeviceSerializer): config_context = serializers.SerializerMethodField(read_only=True) class Meta(DeviceSerializer.Meta): fields = [ - 'id', 'url', 'display', 'name', 'device_type', 'role', 'device_role', 'tenant', 'platform', 'serial', - 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device', - 'status', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', - 'vc_position', 'vc_priority', 'description', 'comments', 'config_template', 'config_context', - 'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated', 'console_port_count', - 'console_server_port_count', 'power_port_count', 'power_outlet_count', 'interface_count', - 'front_port_count', 'rear_port_count', 'device_bay_count', 'module_bay_count', 'inventory_item_count', + 'id', 'url', 'display', 'name', 'device_type', 'role', 'tenant', 'platform', 'serial', 'asset_tag', 'site', + 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device', 'status', 'airflow', + 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', 'vc_position', + 'vc_priority', 'description', 'comments', 'config_template', 'config_context', 'local_context_data', 'tags', + 'custom_fields', 'created', 'last_updated', 'console_port_count', 'console_server_port_count', + 'power_port_count', 'power_outlet_count', 'interface_count', 'front_port_count', 'rear_port_count', + 'device_bay_count', 'module_bay_count', 'inventory_item_count', ] @extend_schema_field(serializers.JSONField(allow_null=True)) diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index 5e773364a..c75757fa7 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -815,20 +815,6 @@ class Device( def get_absolute_url(self): return reverse('dcim:device', args=[self.pk]) - @property - def device_role(self): - """ - For backwards compatibility with pre-v3.6 code expecting a device_role to be present on Device. - """ - return self.role - - @device_role.setter - def device_role(self, value): - """ - For backwards compatibility with pre-v3.6 code expecting a device_role to be present on Device. - """ - self.role = value - def clean(self): super().clean() diff --git a/netbox/dcim/tests/test_cablepaths.py b/netbox/dcim/tests/test_cablepaths.py index a827939f7..49a71022e 100644 --- a/netbox/dcim/tests/test_cablepaths.py +++ b/netbox/dcim/tests/test_cablepaths.py @@ -2156,7 +2156,7 @@ class CablePathTestCase(TestCase): device = Device.objects.create( site=self.site, device_type=self.device.device_type, - device_role=self.device.device_role, + role=self.device.role, name='Test mid-span Device' ) interface1 = Interface.objects.create(device=self.device, name='Interface 1') diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index d56bf0741..8eb057020 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -533,30 +533,6 @@ class DeviceTestCase(TestCase): device2.full_clean() device2.save() - def test_old_device_role_field(self): - """ - Ensure that the old device role field sets the value in the new role field. - """ - - # Test getter method - device = Device( - site=Site.objects.first(), - device_type=DeviceType.objects.first(), - role=DeviceRole.objects.first(), - name='Test Device 1', - device_role=DeviceRole.objects.first() - ) - device.full_clean() - device.save() - - self.assertEqual(device.role, device.device_role) - - # Test setter method - device.device_role = DeviceRole.objects.last() - device.full_clean() - device.save() - self.assertEqual(device.role, device.device_role) - class CableTestCase(TestCase): From c6a3fc2407fe598cca2bb71a7a124d30615256c2 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 4 Mar 2024 08:29:53 -0500 Subject: [PATCH 09/47] #12795: Introduce a custom Group model (#15304) * Rename sequences & indexes after renaming users table * Migrate from auth.Group to a custom group model * Delete original groups from auth_group table * Update object & multi-object custom fields referencing the Group model * Fix ContentType resolution * Clean up obsolete logic for view/serializer resolution --- netbox/netbox/authentication.py | 4 +- netbox/netbox/navigation/menu.py | 6 +- netbox/netbox/tests/test_authentication.py | 3 +- netbox/templates/users/group.html | 2 +- netbox/templates/users/objectpermission.html | 2 +- netbox/templates/users/user.html | 2 +- netbox/users/api/nested_serializers.py | 3 +- netbox/users/api/serializers.py | 3 +- netbox/users/api/views.py | 10 +-- netbox/users/filtersets.py | 3 +- netbox/users/forms/bulk_import.py | 2 +- netbox/users/forms/filtersets.py | 5 +- netbox/users/forms/model_forms.py | 7 +- netbox/users/graphql/schema.py | 6 +- netbox/users/graphql/types.py | 2 +- .../users/migrations/0005_alter_user_table.py | 20 ++++- .../migrations/0006_custom_group_model.py | 80 +++++++++++++++++ netbox/users/models.py | 86 ++++++++++++------- netbox/users/tables.py | 8 +- netbox/users/tests/test_api.py | 3 +- netbox/users/tests/test_filtersets.py | 3 +- netbox/users/tests/test_views.py | 3 +- netbox/users/urls.py | 10 +-- netbox/users/views.py | 20 ++--- netbox/utilities/api.py | 16 +--- netbox/utilities/utils.py | 18 +--- 26 files changed, 208 insertions(+), 119 deletions(-) create mode 100644 netbox/users/migrations/0006_custom_group_model.py diff --git a/netbox/netbox/authentication.py b/netbox/netbox/authentication.py index 10555b373..c70c68bc0 100644 --- a/netbox/netbox/authentication.py +++ b/netbox/netbox/authentication.py @@ -4,13 +4,13 @@ from collections import defaultdict from django.conf import settings from django.contrib.auth import get_user_model from django.contrib.auth.backends import ModelBackend, RemoteUserBackend as _RemoteUserBackend -from django.contrib.auth.models import Group, AnonymousUser +from django.contrib.auth.models import AnonymousUser from django.core.exceptions import ImproperlyConfigured from django.db.models import Q from django.utils.translation import gettext_lazy as _ from users.constants import CONSTRAINT_TOKEN_USER -from users.models import ObjectPermission +from users.models import Group, ObjectPermission from utilities.permissions import ( permission_is_exempt, qs_filter_from_constraints, resolve_permission, resolve_permission_ct, ) diff --git a/netbox/netbox/navigation/menu.py b/netbox/netbox/navigation/menu.py index 2dba76e72..621bd4f5d 100644 --- a/netbox/netbox/navigation/menu.py +++ b/netbox/netbox/navigation/menu.py @@ -392,19 +392,19 @@ ADMIN_MENU = Menu( ), # Proxy model for auth.Group MenuItem( - link=f'users:netboxgroup_list', + link=f'users:group_list', link_text=_('Groups'), permissions=[f'auth.view_group'], staff_only=True, buttons=( MenuItemButton( - link=f'users:netboxgroup_add', + link=f'users:group_add', title='Add', icon_class='mdi mdi-plus-thick', permissions=[f'auth.add_group'] ), MenuItemButton( - link=f'users:netboxgroup_import', + link=f'users:group_import', title='Import', icon_class='mdi mdi-upload', permissions=[f'auth.add_group'] diff --git a/netbox/netbox/tests/test_authentication.py b/netbox/netbox/tests/test_authentication.py index 1804087d1..6a894edcd 100644 --- a/netbox/netbox/tests/test_authentication.py +++ b/netbox/netbox/tests/test_authentication.py @@ -2,7 +2,6 @@ import datetime from django.conf import settings from django.contrib.auth import get_user_model -from django.contrib.auth.models import Group from django.contrib.contenttypes.models import ContentType from django.test import Client from django.test.utils import override_settings @@ -12,7 +11,7 @@ from rest_framework.test import APIClient from dcim.models import Site from ipam.models import Prefix -from users.models import ObjectPermission, Token +from users.models import Group, ObjectPermission, Token from utilities.testing import TestCase from utilities.testing.api import APITestCase diff --git a/netbox/templates/users/group.html b/netbox/templates/users/group.html index 27b4707fb..d3f02af12 100644 --- a/netbox/templates/users/group.html +++ b/netbox/templates/users/group.html @@ -24,7 +24,7 @@
{% trans "Users" %}
- {% for user in object.user_set.all %} + {% for user in object.users.all %} {{ user }} {% empty %}
{% trans "None" %}
diff --git a/netbox/templates/users/objectpermission.html b/netbox/templates/users/objectpermission.html index 9a222ba80..3e8b71327 100644 --- a/netbox/templates/users/objectpermission.html +++ b/netbox/templates/users/objectpermission.html @@ -82,7 +82,7 @@
{% trans "Assigned Groups" %}
{% for group in object.groups.all %} - {{ group }} + {{ group }} {% empty %}
{% trans "None" %}
{% endfor %} diff --git a/netbox/templates/users/user.html b/netbox/templates/users/user.html index 3b08f98d9..0dd12fb52 100644 --- a/netbox/templates/users/user.html +++ b/netbox/templates/users/user.html @@ -53,7 +53,7 @@
{% trans "Assigned Groups" %}
{% for group in object.groups.all %} - {{ group }} + {{ group }} {% empty %}
{% trans "None" %}
{% endfor %} diff --git a/netbox/users/api/nested_serializers.py b/netbox/users/api/nested_serializers.py index 5e15fa41a..552c24906 100644 --- a/netbox/users/api/nested_serializers.py +++ b/netbox/users/api/nested_serializers.py @@ -1,5 +1,4 @@ from django.contrib.auth import get_user_model -from django.contrib.auth.models import Group from django.contrib.contenttypes.models import ContentType from drf_spectacular.utils import extend_schema_field from drf_spectacular.types import OpenApiTypes @@ -7,7 +6,7 @@ from rest_framework import serializers from netbox.api.fields import ContentTypeField from netbox.api.serializers import WritableNestedSerializer -from users.models import ObjectPermission, Token +from users.models import Group, ObjectPermission, Token __all__ = [ 'NestedGroupSerializer', diff --git a/netbox/users/api/serializers.py b/netbox/users/api/serializers.py index 0eef61dc8..b9bd55e75 100644 --- a/netbox/users/api/serializers.py +++ b/netbox/users/api/serializers.py @@ -1,7 +1,6 @@ from django.conf import settings from django.contrib.auth import authenticate from django.contrib.auth import get_user_model -from django.contrib.auth.models import Group from django.contrib.contenttypes.models import ContentType from drf_spectacular.utils import extend_schema_field from drf_spectacular.types import OpenApiTypes @@ -10,7 +9,7 @@ from rest_framework.exceptions import AuthenticationFailed, PermissionDenied from netbox.api.fields import ContentTypeField, IPNetworkSerializer, SerializedPKRelatedField from netbox.api.serializers import ValidatedModelSerializer -from users.models import ObjectPermission, Token +from users.models import Group, ObjectPermission, Token from .nested_serializers import * diff --git a/netbox/users/api/views.py b/netbox/users/api/views.py index 895600822..412bccf59 100644 --- a/netbox/users/api/views.py +++ b/netbox/users/api/views.py @@ -1,11 +1,9 @@ import logging -from django.contrib.auth import authenticate + from django.contrib.auth import get_user_model -from django.contrib.auth.models import Group from django.db.models import Count -from drf_spectacular.utils import extend_schema from drf_spectacular.types import OpenApiTypes -from rest_framework.exceptions import AuthenticationFailed +from drf_spectacular.utils import extend_schema from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.routers import APIRootView @@ -15,7 +13,7 @@ from rest_framework.viewsets import ViewSet from netbox.api.viewsets import NetBoxModelViewSet from users import filtersets -from users.models import ObjectPermission, Token, UserConfig +from users.models import Group, ObjectPermission, Token, UserConfig from utilities.querysets import RestrictedQuerySet from utilities.utils import deepmerge from . import serializers @@ -40,7 +38,7 @@ class UserViewSet(NetBoxModelViewSet): class GroupViewSet(NetBoxModelViewSet): - queryset = RestrictedQuerySet(model=Group).annotate(user_count=Count('user')).order_by('name') + queryset = Group.objects.annotate(user_count=Count('user')) serializer_class = serializers.GroupSerializer filterset_class = filtersets.GroupFilterSet diff --git a/netbox/users/filtersets.py b/netbox/users/filtersets.py index 0f590e012..5dbca7738 100644 --- a/netbox/users/filtersets.py +++ b/netbox/users/filtersets.py @@ -1,11 +1,10 @@ import django_filters from django.contrib.auth import get_user_model -from django.contrib.auth.models import Group from django.db.models import Q from django.utils.translation import gettext as _ from netbox.filtersets import BaseFilterSet -from users.models import ObjectPermission, Token +from users.models import Group, ObjectPermission, Token __all__ = ( 'GroupFilterSet', diff --git a/netbox/users/forms/bulk_import.py b/netbox/users/forms/bulk_import.py index 055998c69..cbaa1ad76 100644 --- a/netbox/users/forms/bulk_import.py +++ b/netbox/users/forms/bulk_import.py @@ -14,7 +14,7 @@ __all__ = ( class GroupImportForm(CSVModelForm): class Meta: - model = NetBoxGroup + model = Group fields = ( 'name', ) diff --git a/netbox/users/forms/filtersets.py b/netbox/users/forms/filtersets.py index c127e2144..23bbe45e1 100644 --- a/netbox/users/forms/filtersets.py +++ b/netbox/users/forms/filtersets.py @@ -1,11 +1,10 @@ from django import forms from django.contrib.auth import get_user_model -from django.contrib.auth.models import Group from django.utils.translation import gettext_lazy as _ from netbox.forms import NetBoxModelFilterSetForm from netbox.forms.mixins import SavedFiltersMixin -from users.models import NetBoxGroup, User, ObjectPermission, Token +from users.models import Group, ObjectPermission, Token, User from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm from utilities.forms.fields import DynamicModelMultipleChoiceField from utilities.forms.widgets import DateTimePicker @@ -19,7 +18,7 @@ __all__ = ( class GroupFilterForm(NetBoxModelFilterSetForm): - model = NetBoxGroup + model = Group fieldsets = ( (None, ('q', 'filter_id',)), ) diff --git a/netbox/users/forms/model_forms.py b/netbox/users/forms/model_forms.py index 8875dc7f0..2a024bf47 100644 --- a/netbox/users/forms/model_forms.py +++ b/netbox/users/forms/model_forms.py @@ -1,7 +1,6 @@ from django import forms from django.conf import settings from django.contrib.auth import get_user_model -from django.contrib.auth.models import Group from django.contrib.contenttypes.models import ContentType from django.contrib.postgres.forms import SimpleArrayField from django.core.exceptions import FieldError @@ -253,7 +252,7 @@ class GroupForm(forms.ModelForm): ) class Meta: - model = NetBoxGroup + model = Group fields = [ 'name', 'users', 'object_permissions', ] @@ -263,14 +262,14 @@ class GroupForm(forms.ModelForm): # Populate assigned users and permissions if self.instance.pk: - self.fields['users'].initial = self.instance.user_set.values_list('id', flat=True) + self.fields['users'].initial = self.instance.users.values_list('id', flat=True) self.fields['object_permissions'].initial = self.instance.object_permissions.values_list('id', flat=True) def save(self, *args, **kwargs): instance = super().save(*args, **kwargs) # Update assigned users and permissions - instance.user_set.set(self.cleaned_data['users']) + instance.users.set(self.cleaned_data['users']) instance.object_permissions.set(self.cleaned_data['object_permissions']) return instance diff --git a/netbox/users/graphql/schema.py b/netbox/users/graphql/schema.py index f033a535a..84ae0c975 100644 --- a/netbox/users/graphql/schema.py +++ b/netbox/users/graphql/schema.py @@ -1,10 +1,10 @@ import graphene - from django.contrib.auth import get_user_model -from django.contrib.auth.models import Group + from netbox.graphql.fields import ObjectField, ObjectListField -from .types import * +from users.models import Group from utilities.graphql_optimizer import gql_query_optimizer +from .types import * class UsersQuery(graphene.ObjectType): diff --git a/netbox/users/graphql/types.py b/netbox/users/graphql/types.py index 4254f1791..58d211028 100644 --- a/netbox/users/graphql/types.py +++ b/netbox/users/graphql/types.py @@ -1,8 +1,8 @@ from django.contrib.auth import get_user_model -from django.contrib.auth.models import Group from graphene_django import DjangoObjectType from users import filtersets +from users.models import Group from utilities.querysets import RestrictedQuerySet __all__ = ( diff --git a/netbox/users/migrations/0005_alter_user_table.py b/netbox/users/migrations/0005_alter_user_table.py index 6c4a815dd..e07db6875 100644 --- a/netbox/users/migrations/0005_alter_user_table.py +++ b/netbox/users/migrations/0005_alter_user_table.py @@ -1,5 +1,3 @@ -# Generated by Django 5.0.1 on 2024-01-31 23:18 - from django.db import migrations @@ -27,12 +25,26 @@ class Migration(migrations.Migration): ] operations = [ - # 0001_squashed had model with db_table=auth_user - now we switch it - # to None to use the default Django resolution (users.user) + # The User table was originally created as 'auth_user'. Now we nullify the model's + # db_table option, so that it defaults to the app & model name (users_user). This + # causes the database table to be renamed. migrations.AlterModelTable( name='user', table=None, ), + + # Rename auth_user_* sequences + migrations.RunSQL("ALTER TABLE auth_user_groups_id_seq RENAME TO users_user_groups_id_seq"), + migrations.RunSQL("ALTER TABLE auth_user_id_seq RENAME TO users_user_id_seq"), + migrations.RunSQL("ALTER TABLE auth_user_user_permissions_id_seq RENAME TO users_user_user_permissions_id_seq"), + + # Rename auth_user_* indexes + migrations.RunSQL("ALTER INDEX auth_user_pkey RENAME TO users_user_pkey"), + # Hash is deterministic; generated via schema_editor._create_index_name() + migrations.RunSQL("ALTER INDEX auth_user_username_6821ab7c_like RENAME TO users_user_username_06e46fe6_like"), + migrations.RunSQL("ALTER INDEX auth_user_username_key RENAME TO users_user_username_key"), + + # Update ContentTypes migrations.RunPython( code=update_content_types, reverse_code=migrations.RunPython.noop diff --git a/netbox/users/migrations/0006_custom_group_model.py b/netbox/users/migrations/0006_custom_group_model.py new file mode 100644 index 000000000..282da3ce0 --- /dev/null +++ b/netbox/users/migrations/0006_custom_group_model.py @@ -0,0 +1,80 @@ +import users.models +from django.db import migrations, models + + +def update_custom_fields(apps, schema_editor): + """ + Update any CustomFields referencing the old Group model to use the new model. + """ + ContentType = apps.get_model('contenttypes', 'ContentType') + CustomField = apps.get_model('extras', 'CustomField') + Group = apps.get_model('users', 'Group') + + if old_ct := ContentType.objects.filter(app_label='users', model='netboxgroup').first(): + new_ct = ContentType.objects.get_for_model(Group) + CustomField.objects.filter(object_type=old_ct).update(object_type=new_ct) + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0005_alter_user_table'), + ] + + operations = [ + # Create the new Group model & table + migrations.CreateModel( + name='Group', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=150, unique=True)), + ('description', models.CharField(blank=True, max_length=200)), + ('permissions', models.ManyToManyField(blank=True, related_name='groups', related_query_name='group', to='auth.permission')), + ], + options={ + 'verbose_name': 'group', + 'verbose_name_plural': 'groups', + }, + managers=[ + ('objects', users.models.NetBoxGroupManager()), + ], + ), + + # Copy existing groups from the old table into the new one + migrations.RunSQL( + "INSERT INTO users_group (SELECT id, name, '' AS description FROM auth_group)" + ), + + # Update the sequence for group ID values + migrations.RunSQL( + "SELECT setval('users_group_id_seq', (SELECT MAX(id) FROM users_group))" + ), + + # Update the "groups" M2M fields on User & ObjectPermission + migrations.AlterField( + model_name='user', + name='groups', + field=models.ManyToManyField(blank=True, related_name='users', related_query_name='user', to='users.group'), + ), + migrations.AlterField( + model_name='objectpermission', + name='groups', + field=models.ManyToManyField(blank=True, related_name='object_permissions', to='users.group'), + ), + + # Delete groups from the old table + migrations.RunSQL( + "DELETE from auth_group" + ), + + # Update custom fields + migrations.RunPython( + code=update_custom_fields, + reverse_code=migrations.RunPython.noop + ), + + # Delete the proxy model + migrations.DeleteModel( + name='NetBoxGroup', + ), + ] diff --git a/netbox/users/models.py b/netbox/users/models.py index 5e817be0b..19d6013c7 100644 --- a/netbox/users/models.py +++ b/netbox/users/models.py @@ -4,7 +4,12 @@ import os from django.conf import settings from django.contrib.auth import get_user_model from django.contrib.auth.models import ( - AbstractUser, Group, GroupManager, User as DjangoUser, UserManager as DjangoUserManager + AbstractUser, + Group as DjangoGroup, + GroupManager, + Permission, + User as DjangoUser, + UserManager as DjangoUserManager ) from django.contrib.postgres.fields import ArrayField from django.core.exceptions import ValidationError @@ -25,7 +30,7 @@ from utilities.utils import flatten_dict from .constants import * __all__ = ( - 'NetBoxGroup', + 'Group', 'ObjectPermission', 'Token', 'User', @@ -33,22 +38,61 @@ __all__ = ( ) -# -# Proxies for Django's User and Group models -# +class NetBoxGroupManager(GroupManager.from_queryset(RestrictedQuerySet)): + pass + + +class Group(models.Model): + name = models.CharField( + verbose_name=_('name'), + max_length=150, + unique=True + ) + description = models.CharField( + verbose_name=_('description'), + max_length=200, + blank=True + ) + + # Replicate legacy Django permissions support from stock Group model + # to ensure authentication backend compatibility + permissions = models.ManyToManyField( + Permission, + verbose_name=_("permissions"), + blank=True, + related_name='groups', + related_query_name='group' + ) + + objects = NetBoxGroupManager() + + class Meta: + verbose_name = _('group') + verbose_name_plural = _('groups') + + def __str__(self): + return self.name + + def get_absolute_url(self): + return reverse('users:group', args=[self.pk]) + + def natural_key(self): + return (self.name,) + class UserManager(DjangoUserManager.from_queryset(RestrictedQuerySet)): pass -class NetBoxGroupManager(GroupManager.from_queryset(RestrictedQuerySet)): - pass - - class User(AbstractUser): - """ - Proxy contrib.auth.models.User for the UI - """ + groups = models.ManyToManyField( + to='users.Group', + verbose_name=_('groups'), + blank=True, + related_name='users', + related_query_name='user' + ) + objects = UserManager() class Meta: @@ -68,22 +112,6 @@ class User(AbstractUser): raise ValidationError(_("A user with this username already exists.")) -class NetBoxGroup(Group): - """ - Proxy contrib.auth.models.User for the UI - """ - objects = NetBoxGroupManager() - - class Meta: - proxy = True - ordering = ('name',) - verbose_name = _('group') - verbose_name_plural = _('groups') - - def get_absolute_url(self): - return reverse('users:netboxgroup', args=[self.pk]) - - # # User preferences # @@ -360,7 +388,7 @@ class ObjectPermission(models.Model): related_name='object_permissions' ) groups = models.ManyToManyField( - to=Group, + to='users.Group', blank=True, related_name='object_permissions' ) diff --git a/netbox/users/tables.py b/netbox/users/tables.py index 781660817..813d729c9 100644 --- a/netbox/users/tables.py +++ b/netbox/users/tables.py @@ -3,7 +3,7 @@ from django.utils.translation import gettext as _ from account.tables import UserTokenTable from netbox.tables import NetBoxTable, columns -from users.models import NetBoxGroup, User, ObjectPermission, Token +from users.models import Group, ObjectPermission, Token, User __all__ = ( 'GroupTable', @@ -33,7 +33,7 @@ class UserTable(NetBoxTable): ) groups = columns.ManyToManyColumn( verbose_name=_('Groups'), - linkify_item=('users:netboxgroup', {'pk': tables.A('pk')}) + linkify_item=('users:group', {'pk': tables.A('pk')}) ) is_active = columns.BooleanColumn( verbose_name=_('Is Active'), @@ -67,7 +67,7 @@ class GroupTable(NetBoxTable): ) class Meta(NetBoxTable.Meta): - model = NetBoxGroup + model = Group fields = ( 'pk', 'id', 'name', 'users_count', ) @@ -107,7 +107,7 @@ class ObjectPermissionTable(NetBoxTable): ) groups = columns.ManyToManyColumn( verbose_name=_('Groups'), - linkify_item=('users:netboxgroup', {'pk': tables.A('pk')}) + linkify_item=('users:group', {'pk': tables.A('pk')}) ) actions = columns.ActionsColumn( actions=('edit', 'delete'), diff --git a/netbox/users/tests/test_api.py b/netbox/users/tests/test_api.py index 40a3edf31..51fc21c97 100644 --- a/netbox/users/tests/test_api.py +++ b/netbox/users/tests/test_api.py @@ -1,9 +1,8 @@ from django.contrib.auth import get_user_model -from django.contrib.auth.models import Group from django.contrib.contenttypes.models import ContentType from django.urls import reverse -from users.models import ObjectPermission, Token +from users.models import Group, ObjectPermission, Token from utilities.testing import APIViewTestCases, APITestCase, create_test_user from utilities.utils import deepmerge diff --git a/netbox/users/tests/test_filtersets.py b/netbox/users/tests/test_filtersets.py index 38a0df813..5d373628f 100644 --- a/netbox/users/tests/test_filtersets.py +++ b/netbox/users/tests/test_filtersets.py @@ -1,13 +1,12 @@ import datetime from django.contrib.auth import get_user_model -from django.contrib.auth.models import Group from django.contrib.contenttypes.models import ContentType from django.test import TestCase from django.utils.timezone import make_aware from users import filtersets -from users.models import ObjectPermission, Token +from users.models import Group, ObjectPermission, Token from utilities.testing import BaseFilterSetTests User = get_user_model() diff --git a/netbox/users/tests/test_views.py b/netbox/users/tests/test_views.py index 259b7a857..27d2aeab1 100644 --- a/netbox/users/tests/test_views.py +++ b/netbox/users/tests/test_views.py @@ -1,4 +1,3 @@ -from django.contrib.auth.models import Group from django.contrib.contenttypes.models import ContentType from users.models import * @@ -70,7 +69,7 @@ class GroupTestCase( ViewTestCases.BulkImportObjectsViewTestCase, ViewTestCases.BulkDeleteObjectsViewTestCase, ): - model = NetBoxGroup + model = Group maxDiff = None @classmethod diff --git a/netbox/users/urls.py b/netbox/users/urls.py index 486a0c771..adfeba378 100644 --- a/netbox/users/urls.py +++ b/netbox/users/urls.py @@ -23,11 +23,11 @@ urlpatterns = [ path('users//', include(get_model_urls('users', 'user'))), # Groups - path('groups/', views.GroupListView.as_view(), name='netboxgroup_list'), - path('groups/add/', views.GroupEditView.as_view(), name='netboxgroup_add'), - path('groups/import/', views.GroupBulkImportView.as_view(), name='netboxgroup_import'), - path('groups/delete/', views.GroupBulkDeleteView.as_view(), name='netboxgroup_bulk_delete'), - path('groups//', include(get_model_urls('users', 'netboxgroup'))), + path('groups/', views.GroupListView.as_view(), name='group_list'), + path('groups/add/', views.GroupEditView.as_view(), name='group_add'), + path('groups/import/', views.GroupBulkImportView.as_view(), name='group_import'), + path('groups/delete/', views.GroupBulkDeleteView.as_view(), name='group_bulk_delete'), + path('groups//', include(get_model_urls('users', 'group'))), # Permissions path('permissions/', views.ObjectPermissionListView.as_view(), name='objectpermission_list'), diff --git a/netbox/users/views.py b/netbox/users/views.py index 324125604..662e5e573 100644 --- a/netbox/users/views.py +++ b/netbox/users/views.py @@ -5,7 +5,7 @@ from extras.tables import ObjectChangeTable from netbox.views import generic from utilities.views import register_model_view from . import filtersets, forms, tables -from .models import NetBoxGroup, User, ObjectPermission, Token +from .models import Group, User, ObjectPermission, Token # @@ -110,36 +110,36 @@ class UserBulkDeleteView(generic.BulkDeleteView): # class GroupListView(generic.ObjectListView): - queryset = NetBoxGroup.objects.annotate(users_count=Count('user')) + queryset = Group.objects.annotate(users_count=Count('user')) filterset = filtersets.GroupFilterSet filterset_form = forms.GroupFilterForm table = tables.GroupTable -@register_model_view(NetBoxGroup) +@register_model_view(Group) class GroupView(generic.ObjectView): - queryset = NetBoxGroup.objects.all() + queryset = Group.objects.all() template_name = 'users/group.html' -@register_model_view(NetBoxGroup, 'edit') +@register_model_view(Group, 'edit') class GroupEditView(generic.ObjectEditView): - queryset = NetBoxGroup.objects.all() + queryset = Group.objects.all() form = forms.GroupForm -@register_model_view(NetBoxGroup, 'delete') +@register_model_view(Group, 'delete') class GroupDeleteView(generic.ObjectDeleteView): - queryset = NetBoxGroup.objects.all() + queryset = Group.objects.all() class GroupBulkImportView(generic.BulkImportView): - queryset = NetBoxGroup.objects.all() + queryset = Group.objects.all() model_form = forms.GroupImportForm class GroupBulkDeleteView(generic.BulkDeleteView): - queryset = NetBoxGroup.objects.annotate(users_count=Count('user')) + queryset = Group.objects.annotate(users_count=Count('user')) filterset = filtersets.GroupFilterSet table = tables.GroupTable diff --git a/netbox/utilities/api.py b/netbox/utilities/api.py index a13e62bfd..25a350c81 100644 --- a/netbox/utilities/api.py +++ b/netbox/utilities/api.py @@ -31,23 +31,13 @@ def get_serializer_for_model(model, prefix=''): """ Dynamically resolve and return the appropriate serializer for a model. """ - app_name, model_name = model._meta.label.split('.') - # Serializers for Django's auth models are in the users app - if app_name == 'auth': - app_name = 'users' - # Account for changes using Proxy model - if app_name == 'users': - if model_name == 'NetBoxUser': - model_name = 'User' - elif model_name == 'NetBoxGroup': - model_name = 'Group' - - serializer_name = f'{app_name}.api.serializers.{prefix}{model_name}Serializer' + app_label, model_name = model._meta.label.split('.') + serializer_name = f'{app_label}.api.serializers.{prefix}{model_name}Serializer' try: return dynamic_import(serializer_name) except AttributeError: raise SerializerNotFound( - f"Could not determine serializer for {app_name}.{model_name} with prefix '{prefix}'" + f"Could not determine serializer for {app_label}.{model_name} with prefix '{prefix}'" ) diff --git a/netbox/utilities/utils.py b/netbox/utilities/utils.py index bd03ae4b8..5a25b4465 100644 --- a/netbox/utilities/utils.py +++ b/netbox/utilities/utils.py @@ -1,11 +1,12 @@ import datetime import decimal import json -import nh3 import re from decimal import Decimal from itertools import count, groupby +from urllib.parse import urlencode +import nh3 from django.contrib.contenttypes.models import ContentType from django.core import serializers from django.db.models import Count, ManyToOneRel, OuterRef, Subquery @@ -23,7 +24,6 @@ from dcim.choices import CableLengthUnitChoices, WeightUnitChoices from extras.utils import is_taggable from netbox.config import get_config from netbox.plugins import PluginConfig -from urllib.parse import urlencode from utilities.constants import HTTP_REQUEST_META_SAFE_COPY from .constants import HTML_ALLOWED_ATTRIBUTES, HTML_ALLOWED_TAGS @@ -48,26 +48,16 @@ def get_viewname(model, action=None, rest_api=False): model_name = model._meta.model_name if rest_api: + viewname = f'{app_label}-api:{model_name}' if is_plugin: - viewname = f'plugins-api:{app_label}-api:{model_name}' - else: - # Alter the app_label for group and user model_name to point to users app - if app_label == 'auth' and model_name in ['group', 'user']: - app_label = 'users' - if app_label == 'users' and model._meta.proxy and model_name in ['netboxuser', 'netboxgroup']: - model_name = model._meta.proxy_for_model._meta.model_name - - viewname = f'{app_label}-api:{model_name}' - # Append the action, if any + viewname = f'plugins-api:{viewname}' if action: viewname = f'{viewname}-{action}' else: viewname = f'{app_label}:{model_name}' - # Prepend the plugins namespace if this is a plugin model if is_plugin: viewname = f'plugins:{viewname}' - # Append the action, if any if action: viewname = f'{viewname}_{action}' From 0df68bf29180a3fab599bf3f828821560d622dd2 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 1 Mar 2024 11:47:46 -0500 Subject: [PATCH 10/47] Rename ContentType proxy model to ObjectType --- netbox/core/forms/filtersets.py | 2 +- netbox/core/management/commands/nbshell.py | 4 +-- .../core/migrations/0008_contenttype_proxy.py | 6 ++--- netbox/core/models/contenttypes.py | 12 ++++----- netbox/core/models/jobs.py | 6 ++--- netbox/dcim/models/cables.py | 12 ++++----- netbox/extras/api/serializers.py | 26 +++++++++---------- netbox/extras/dashboard/widgets.py | 12 ++++----- netbox/extras/forms/bulk_import.py | 18 ++++++------- netbox/extras/forms/filtersets.py | 22 ++++++++-------- netbox/extras/forms/model_forms.py | 22 ++++++++-------- netbox/extras/models/change_logging.py | 4 +-- netbox/extras/models/customfields.py | 4 +-- netbox/extras/models/models.py | 8 +++--- netbox/ipam/models/ip.py | 4 +-- netbox/netbox/models/features.py | 6 ++--- netbox/tenancy/forms/filtersets.py | 4 +-- netbox/tenancy/models/contacts.py | 4 +-- netbox/users/models.py | 2 +- netbox/vpn/models/l2vpn.py | 6 ++--- 20 files changed, 91 insertions(+), 93 deletions(-) diff --git a/netbox/core/forms/filtersets.py b/netbox/core/forms/filtersets.py index 0c164ac29..bd74c0f14 100644 --- a/netbox/core/forms/filtersets.py +++ b/netbox/core/forms/filtersets.py @@ -68,7 +68,7 @@ class JobFilterForm(SavedFiltersMixin, FilterForm): ) object_type = ContentTypeChoiceField( label=_('Object Type'), - queryset=ContentType.objects.with_feature('jobs'), + queryset=ObjectType.objects.with_feature('jobs'), required=False, ) status = forms.MultipleChoiceField( diff --git a/netbox/core/management/commands/nbshell.py b/netbox/core/management/commands/nbshell.py index eeefe502b..b96870252 100644 --- a/netbox/core/management/commands/nbshell.py +++ b/netbox/core/management/commands/nbshell.py @@ -8,7 +8,7 @@ from django.conf import settings from django.contrib.auth import get_user_model from django.core.management.base import BaseCommand -from core.models import ContentType +from core.models import ObjectType APPS = ('circuits', 'core', 'dcim', 'extras', 'ipam', 'tenancy', 'users', 'virtualization', 'vpn', 'wireless') @@ -60,7 +60,7 @@ class Command(BaseCommand): pass # Additional objects to include - namespace['ContentType'] = ContentType + namespace['ObjectType'] = ObjectType namespace['User'] = get_user_model() # Load convenience commands diff --git a/netbox/core/migrations/0008_contenttype_proxy.py b/netbox/core/migrations/0008_contenttype_proxy.py index ac11d906a..dee82a969 100644 --- a/netbox/core/migrations/0008_contenttype_proxy.py +++ b/netbox/core/migrations/0008_contenttype_proxy.py @@ -1,5 +1,3 @@ -# Generated by Django 4.2.6 on 2023-10-31 19:38 - import core.models.contenttypes from django.db import migrations @@ -13,7 +11,7 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='ContentType', + name='ObjectType', fields=[ ], options={ @@ -23,7 +21,7 @@ class Migration(migrations.Migration): }, bases=('contenttypes.contenttype',), managers=[ - ('objects', core.models.contenttypes.ContentTypeManager()), + ('objects', core.models.contenttypes.ObjectTypeManager()), ], ), ] diff --git a/netbox/core/models/contenttypes.py b/netbox/core/models/contenttypes.py index c98184c3d..b0301848f 100644 --- a/netbox/core/models/contenttypes.py +++ b/netbox/core/models/contenttypes.py @@ -1,15 +1,15 @@ -from django.contrib.contenttypes.models import ContentType as ContentType_, ContentTypeManager as ContentTypeManager_ +from django.contrib.contenttypes.models import ContentType, ContentTypeManager from django.db.models import Q from netbox.registry import registry __all__ = ( - 'ContentType', - 'ContentTypeManager', + 'ObjectType', + 'ObjectTypeManager', ) -class ContentTypeManager(ContentTypeManager_): +class ObjectTypeManager(ContentTypeManager): def public(self): """ @@ -40,11 +40,11 @@ class ContentTypeManager(ContentTypeManager_): return self.get_queryset().filter(q) -class ContentType(ContentType_): +class ObjectType(ContentType): """ Wrap Django's native ContentType model to use our custom manager. """ - objects = ContentTypeManager() + objects = ObjectTypeManager() class Meta: proxy = True diff --git a/netbox/core/models/jobs.py b/netbox/core/models/jobs.py index 2e3425129..b9f0d0b91 100644 --- a/netbox/core/models/jobs.py +++ b/netbox/core/models/jobs.py @@ -11,7 +11,7 @@ from django.utils import timezone from django.utils.translation import gettext as _ from core.choices import JobStatusChoices -from core.models import ContentType +from core.models import ObjectType from core.signals import job_end, job_start from extras.constants import EVENT_JOB_END, EVENT_JOB_START from netbox.config import get_config @@ -130,7 +130,7 @@ class Job(models.Model): super().clean() # Validate the assigned object type - if self.object_type not in ContentType.objects.with_feature('jobs'): + if self.object_type not in ObjectType.objects.with_feature('jobs'): raise ValidationError( _("Jobs cannot be assigned to this object type ({type}).").format(type=self.object_type) ) @@ -210,7 +210,7 @@ class Job(models.Model): schedule_at: Schedule the job to be executed at the passed date and time interval: Recurrence interval (in minutes) """ - object_type = ContentType.objects.get_for_model(instance, for_concrete_model=False) + object_type = ObjectType.objects.get_for_model(instance, for_concrete_model=False) rq_queue_name = get_queue_for_model(object_type.model) queue = django_rq.get_queue(rq_queue_name) status = JobStatusChoices.STATUS_SCHEDULED if schedule_at else JobStatusChoices.STATUS_PENDING diff --git a/netbox/dcim/models/cables.py b/netbox/dcim/models/cables.py index cba345941..f8a61a794 100644 --- a/netbox/dcim/models/cables.py +++ b/netbox/dcim/models/cables.py @@ -9,7 +9,7 @@ from django.dispatch import Signal from django.urls import reverse from django.utils.translation import gettext_lazy as _ -from core.models import ContentType +from core.models import ObjectType from dcim.choices import * from dcim.constants import * from dcim.fields import PathField @@ -481,13 +481,13 @@ class CablePath(models.Model): def origin_type(self): if self.path: ct_id, _ = decompile_path_node(self.path[0][0]) - return ContentType.objects.get_for_id(ct_id) + return ObjectType.objects.get_for_id(ct_id) @property def destination_type(self): if self.is_complete: ct_id, _ = decompile_path_node(self.path[-1][0]) - return ContentType.objects.get_for_id(ct_id) + return ObjectType.objects.get_for_id(ct_id) @property def path_objects(self): @@ -594,7 +594,7 @@ class CablePath(models.Model): # Step 6: Determine the far-end terminations if isinstance(links[0], Cable): - termination_type = ContentType.objects.get_for_model(terminations[0]) + termination_type = ObjectType.objects.get_for_model(terminations[0]) local_cable_terminations = CableTermination.objects.filter( termination_type=termination_type, termination_id__in=[t.pk for t in terminations] @@ -747,7 +747,7 @@ class CablePath(models.Model): # Prefetch path objects using one query per model type. Prefetch related devices where appropriate. prefetched = {} for ct_id, object_ids in to_prefetch.items(): - model_class = ContentType.objects.get_for_id(ct_id).model_class() + model_class = ObjectType.objects.get_for_id(ct_id).model_class() queryset = model_class.objects.filter(pk__in=object_ids) if hasattr(model_class, 'device'): queryset = queryset.prefetch_related('device') @@ -774,7 +774,7 @@ class CablePath(models.Model): """ Return all Cable IDs within the path. """ - cable_ct = ContentType.objects.get_for_model(Cable).pk + cable_ct = ObjectType.objects.get_for_model(Cable).pk cable_ids = [] for node in self._nodes: diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index 7dad95263..a87947d68 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -7,7 +7,7 @@ from rest_framework import serializers from core.api.nested_serializers import NestedDataSourceSerializer, NestedDataFileSerializer, NestedJobSerializer from core.api.serializers import JobSerializer -from core.models import ContentType +from core.models import ObjectType from dcim.api.nested_serializers import ( NestedDeviceRoleSerializer, NestedDeviceTypeSerializer, NestedLocationSerializer, NestedPlatformSerializer, NestedRegionSerializer, NestedSiteSerializer, NestedSiteGroupSerializer, @@ -60,12 +60,12 @@ __all__ = ( class EventRuleSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='extras-api:eventrule-detail') content_types = ContentTypeField( - queryset=ContentType.objects.with_feature('event_rules'), + queryset=ObjectType.objects.with_feature('event_rules'), many=True ) action_type = ChoiceField(choices=EventRuleActionChoices) action_object_type = ContentTypeField( - queryset=ContentType.objects.with_feature('event_rules'), + queryset=ObjectType.objects.with_feature('event_rules'), ) action_object = serializers.SerializerMethodField(read_only=True) @@ -118,12 +118,12 @@ class WebhookSerializer(NetBoxModelSerializer): class CustomFieldSerializer(ValidatedModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfield-detail') content_types = ContentTypeField( - queryset=ContentType.objects.with_feature('custom_fields'), + queryset=ObjectType.objects.with_feature('custom_fields'), many=True ) type = ChoiceField(choices=CustomFieldTypeChoices) object_type = ContentTypeField( - queryset=ContentType.objects.all(), + queryset=ObjectType.objects.all(), required=False, allow_null=True ) @@ -197,7 +197,7 @@ class CustomFieldChoiceSetSerializer(ValidatedModelSerializer): class CustomLinkSerializer(ValidatedModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='extras-api:customlink-detail') content_types = ContentTypeField( - queryset=ContentType.objects.with_feature('custom_links'), + queryset=ObjectType.objects.with_feature('custom_links'), many=True ) @@ -217,7 +217,7 @@ class CustomLinkSerializer(ValidatedModelSerializer): class ExportTemplateSerializer(ValidatedModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='extras-api:exporttemplate-detail') content_types = ContentTypeField( - queryset=ContentType.objects.with_feature('export_templates'), + queryset=ObjectType.objects.with_feature('export_templates'), many=True ) data_source = NestedDataSourceSerializer( @@ -244,7 +244,7 @@ class ExportTemplateSerializer(ValidatedModelSerializer): class SavedFilterSerializer(ValidatedModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='extras-api:savedfilter-detail') content_types = ContentTypeField( - queryset=ContentType.objects.all(), + queryset=ObjectType.objects.all(), many=True ) @@ -264,7 +264,7 @@ class SavedFilterSerializer(ValidatedModelSerializer): class BookmarkSerializer(ValidatedModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='extras-api:bookmark-detail') object_type = ContentTypeField( - queryset=ContentType.objects.with_feature('bookmarks'), + queryset=ObjectType.objects.with_feature('bookmarks'), ) object = serializers.SerializerMethodField(read_only=True) user = NestedUserSerializer() @@ -289,7 +289,7 @@ class BookmarkSerializer(ValidatedModelSerializer): class TagSerializer(ValidatedModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='extras-api:tag-detail') object_types = ContentTypeField( - queryset=ContentType.objects.with_feature('tags'), + queryset=ObjectType.objects.with_feature('tags'), many=True, required=False ) @@ -313,7 +313,7 @@ class TagSerializer(ValidatedModelSerializer): class ImageAttachmentSerializer(ValidatedModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='extras-api:imageattachment-detail') content_type = ContentTypeField( - queryset=ContentType.objects.all() + queryset=ObjectType.objects.all() ) parent = serializers.SerializerMethodField(read_only=True) @@ -353,7 +353,7 @@ class ImageAttachmentSerializer(ValidatedModelSerializer): class JournalEntrySerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='extras-api:journalentry-detail') assigned_object_type = ContentTypeField( - queryset=ContentType.objects.all() + queryset=ObjectType.objects.all() ) assigned_object = serializers.SerializerMethodField(read_only=True) created_by = serializers.PrimaryKeyRelatedField( @@ -645,7 +645,7 @@ class ContentTypeSerializer(BaseModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='extras-api:contenttype-detail') class Meta: - model = ContentType + model = ObjectType fields = ['id', 'url', 'display', 'app_label', 'model'] diff --git a/netbox/extras/dashboard/widgets.py b/netbox/extras/dashboard/widgets.py index 1bdc4bc1d..69bef0d8f 100644 --- a/netbox/extras/dashboard/widgets.py +++ b/netbox/extras/dashboard/widgets.py @@ -12,7 +12,7 @@ from django.template.loader import render_to_string from django.urls import NoReverseMatch, resolve, reverse from django.utils.translation import gettext as _ -from core.models import ContentType +from core.models import ObjectType from extras.choices import BookmarkOrderingChoices from utilities.choices import ButtonColorChoices from utilities.permissions import get_permission_for_model @@ -34,14 +34,14 @@ __all__ = ( def get_object_type_choices(): return [ (content_type_identifier(ct), content_type_name(ct)) - for ct in ContentType.objects.public().order_by('app_label', 'model') + for ct in ObjectType.objects.public().order_by('app_label', 'model') ] def get_bookmarks_object_type_choices(): return [ (content_type_identifier(ct), content_type_name(ct)) - for ct in ContentType.objects.with_feature('bookmarks').order_by('app_label', 'model') + for ct in ObjectType.objects.with_feature('bookmarks').order_by('app_label', 'model') ] @@ -52,7 +52,7 @@ def get_models_from_content_types(content_types): models = [] for content_type_id in content_types: app_label, model_name = content_type_id.split('.') - content_type = ContentType.objects.get_by_natural_key(app_label, model_name) + content_type = ObjectType.objects.get_by_natural_key(app_label, model_name) models.append(content_type.model_class()) return models @@ -238,7 +238,7 @@ class ObjectListWidget(DashboardWidget): def render(self, request): app_label, model_name = self.config['model'].split('.') - model = ContentType.objects.get_by_natural_key(app_label, model_name).model_class() + model = ObjectType.objects.get_by_natural_key(app_label, model_name).model_class() viewname = get_viewname(model, action='list') # Evaluate user's permission. Note that this controls only whether the HTMX element is @@ -371,7 +371,7 @@ class BookmarksWidget(DashboardWidget): bookmarks = Bookmark.objects.filter(user=request.user).order_by(self.config['order_by']) if object_types := self.config.get('object_types'): models = get_models_from_content_types(object_types) - conent_types = ContentType.objects.get_for_models(*models).values() + conent_types = ObjectType.objects.get_for_models(*models).values() bookmarks = bookmarks.filter(object_type__in=conent_types) if max_items := self.config.get('max_items'): bookmarks = bookmarks[:max_items] diff --git a/netbox/extras/forms/bulk_import.py b/netbox/extras/forms/bulk_import.py index 440600af5..133233ccc 100644 --- a/netbox/extras/forms/bulk_import.py +++ b/netbox/extras/forms/bulk_import.py @@ -6,7 +6,7 @@ from django.core.exceptions import ObjectDoesNotExist from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ -from core.models import ContentType +from core.models import ObjectType from extras.choices import * from extras.models import * from netbox.forms import NetBoxModelImportForm @@ -32,7 +32,7 @@ __all__ = ( class CustomFieldImportForm(CSVModelForm): content_types = CSVMultipleContentTypeField( label=_('Content types'), - queryset=ContentType.objects.with_feature('custom_fields'), + queryset=ObjectType.objects.with_feature('custom_fields'), help_text=_("One or more assigned object types") ) type = CSVChoiceField( @@ -42,7 +42,7 @@ class CustomFieldImportForm(CSVModelForm): ) object_type = CSVContentTypeField( label=_('Object type'), - queryset=ContentType.objects.public(), + queryset=ObjectType.objects.public(), required=False, help_text=_("Object type (for object or multi-object fields)") ) @@ -113,7 +113,7 @@ class CustomFieldChoiceSetImportForm(CSVModelForm): class CustomLinkImportForm(CSVModelForm): content_types = CSVMultipleContentTypeField( label=_('Content types'), - queryset=ContentType.objects.with_feature('custom_links'), + queryset=ObjectType.objects.with_feature('custom_links'), help_text=_("One or more assigned object types") ) @@ -128,7 +128,7 @@ class CustomLinkImportForm(CSVModelForm): class ExportTemplateImportForm(CSVModelForm): content_types = CSVMultipleContentTypeField( label=_('Content types'), - queryset=ContentType.objects.with_feature('export_templates'), + queryset=ObjectType.objects.with_feature('export_templates'), help_text=_("One or more assigned object types") ) @@ -151,7 +151,7 @@ class ConfigTemplateImportForm(CSVModelForm): class SavedFilterImportForm(CSVModelForm): content_types = CSVMultipleContentTypeField( label=_('Content types'), - queryset=ContentType.objects.all(), + queryset=ObjectType.objects.all(), help_text=_("One or more assigned object types") ) @@ -175,7 +175,7 @@ class WebhookImportForm(NetBoxModelImportForm): class EventRuleImportForm(NetBoxModelImportForm): content_types = CSVMultipleContentTypeField( label=_('Content types'), - queryset=ContentType.objects.with_feature('event_rules'), + queryset=ObjectType.objects.with_feature('event_rules'), help_text=_("One or more assigned object types") ) action_object = forms.CharField( @@ -213,7 +213,7 @@ class EventRuleImportForm(NetBoxModelImportForm): except ObjectDoesNotExist: raise forms.ValidationError(_("Script {name} not found").format(name=action_object)) self.instance.action_object = script - self.instance.action_object_type = ContentType.objects.get_for_model(script, for_concrete_model=False) + self.instance.action_object_type = ObjectType.objects.get_for_model(script, for_concrete_model=False) class TagImportForm(CSVModelForm): @@ -229,7 +229,7 @@ class TagImportForm(CSVModelForm): class JournalEntryImportForm(NetBoxModelImportForm): assigned_object_type = CSVContentTypeField( - queryset=ContentType.objects.all(), + queryset=ObjectType.objects.all(), label=_('Assigned object type'), ) kind = CSVChoiceField( diff --git a/netbox/extras/forms/filtersets.py b/netbox/extras/forms/filtersets.py index 3a6421901..075144d12 100644 --- a/netbox/extras/forms/filtersets.py +++ b/netbox/extras/forms/filtersets.py @@ -2,7 +2,7 @@ from django import forms from django.contrib.auth import get_user_model from django.utils.translation import gettext_lazy as _ -from core.models import ContentType, DataFile, DataSource +from core.models import ObjectType, DataFile, DataSource from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup from extras.choices import * from extras.models import * @@ -43,7 +43,7 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm): )), ) content_type_id = ContentTypeMultipleChoiceField( - queryset=ContentType.objects.with_feature('custom_fields'), + queryset=ObjectType.objects.with_feature('custom_fields'), required=False, label=_('Object type') ) @@ -112,7 +112,7 @@ class CustomLinkFilterForm(SavedFiltersMixin, FilterForm): ) content_types = ContentTypeMultipleChoiceField( label=_('Content types'), - queryset=ContentType.objects.with_feature('custom_links'), + queryset=ObjectType.objects.with_feature('custom_links'), required=False ) enabled = forms.NullBooleanField( @@ -155,7 +155,7 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm): } ) content_type_id = ContentTypeMultipleChoiceField( - queryset=ContentType.objects.with_feature('export_templates'), + queryset=ObjectType.objects.with_feature('export_templates'), required=False, label=_('Content types') ) @@ -183,7 +183,7 @@ class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm): ) content_type_id = ContentTypeChoiceField( label=_('Content type'), - queryset=ContentType.objects.with_feature('image_attachments'), + queryset=ObjectType.objects.with_feature('image_attachments'), required=False ) name = forms.CharField( @@ -199,7 +199,7 @@ class SavedFilterFilterForm(SavedFiltersMixin, FilterForm): ) content_types = ContentTypeMultipleChoiceField( label=_('Content types'), - queryset=ContentType.objects.public(), + queryset=ObjectType.objects.public(), required=False ) enabled = forms.NullBooleanField( @@ -254,7 +254,7 @@ class EventRuleFilterForm(NetBoxModelFilterSetForm): (_('Events'), ('type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end')), ) content_type_id = ContentTypeMultipleChoiceField( - queryset=ContentType.objects.with_feature('event_rules'), + queryset=ObjectType.objects.with_feature('event_rules'), required=False, label=_('Object type') ) @@ -310,12 +310,12 @@ class EventRuleFilterForm(NetBoxModelFilterSetForm): class TagFilterForm(SavedFiltersMixin, FilterForm): model = Tag content_type_id = ContentTypeMultipleChoiceField( - queryset=ContentType.objects.with_feature('tags'), + queryset=ObjectType.objects.with_feature('tags'), required=False, label=_('Tagged object type') ) for_object_type_id = ContentTypeChoiceField( - queryset=ContentType.objects.with_feature('tags'), + queryset=ObjectType.objects.with_feature('tags'), required=False, label=_('Allowed object type') ) @@ -464,7 +464,7 @@ class JournalEntryFilterForm(NetBoxModelFilterSetForm): label=_('User') ) assigned_object_type_id = DynamicModelMultipleChoiceField( - queryset=ContentType.objects.all(), + queryset=ObjectType.objects.all(), required=False, label=_('Object Type'), widget=APISelectMultiple( @@ -507,7 +507,7 @@ class ObjectChangeFilterForm(SavedFiltersMixin, FilterForm): label=_('User') ) changed_object_type_id = DynamicModelMultipleChoiceField( - queryset=ContentType.objects.all(), + queryset=ObjectType.objects.all(), required=False, label=_('Object Type'), widget=APISelectMultiple( diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py index 5c3671b3c..443ba0a30 100644 --- a/netbox/extras/forms/model_forms.py +++ b/netbox/extras/forms/model_forms.py @@ -7,7 +7,7 @@ from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ from core.forms.mixins import SyncedDataMixin -from core.models import ContentType +from core.models import ObjectType from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup from extras.choices import * from extras.models import * @@ -41,11 +41,11 @@ __all__ = ( class CustomFieldForm(forms.ModelForm): content_types = ContentTypeMultipleChoiceField( label=_('Content types'), - queryset=ContentType.objects.with_feature('custom_fields') + queryset=ObjectType.objects.with_feature('custom_fields') ) object_type = ContentTypeChoiceField( label=_('Object type'), - queryset=ContentType.objects.public(), + queryset=ObjectType.objects.public(), required=False, help_text=_("Type of the related object (for object/multi-object fields only)") ) @@ -125,7 +125,7 @@ class CustomFieldChoiceSetForm(forms.ModelForm): class CustomLinkForm(forms.ModelForm): content_types = ContentTypeMultipleChoiceField( label=_('Content types'), - queryset=ContentType.objects.with_feature('custom_links') + queryset=ObjectType.objects.with_feature('custom_links') ) fieldsets = ( @@ -154,7 +154,7 @@ class CustomLinkForm(forms.ModelForm): class ExportTemplateForm(SyncedDataMixin, forms.ModelForm): content_types = ContentTypeMultipleChoiceField( label=_('Content types'), - queryset=ContentType.objects.with_feature('export_templates') + queryset=ObjectType.objects.with_feature('export_templates') ) template_code = forms.CharField( label=_('Template code'), @@ -195,7 +195,7 @@ class SavedFilterForm(forms.ModelForm): slug = SlugField() content_types = ContentTypeMultipleChoiceField( label=_('Content types'), - queryset=ContentType.objects.all() + queryset=ObjectType.objects.all() ) parameters = JSONField() @@ -221,7 +221,7 @@ class SavedFilterForm(forms.ModelForm): class BookmarkForm(forms.ModelForm): object_type = ContentTypeChoiceField( label=_('Object type'), - queryset=ContentType.objects.with_feature('bookmarks') + queryset=ObjectType.objects.with_feature('bookmarks') ) class Meta: @@ -251,7 +251,7 @@ class WebhookForm(NetBoxModelForm): class EventRuleForm(NetBoxModelForm): content_types = ContentTypeMultipleChoiceField( label=_('Content types'), - queryset=ContentType.objects.with_feature('event_rules'), + queryset=ObjectType.objects.with_feature('event_rules'), ) action_choice = forms.ChoiceField( label=_('Action choice'), @@ -339,11 +339,11 @@ class EventRuleForm(NetBoxModelForm): action_choice = self.cleaned_data.get('action_choice') # Webhook if self.cleaned_data.get('action_type') == EventRuleActionChoices.WEBHOOK: - self.cleaned_data['action_object_type'] = ContentType.objects.get_for_model(action_choice) + self.cleaned_data['action_object_type'] = ObjectType.objects.get_for_model(action_choice) self.cleaned_data['action_object_id'] = action_choice.id # Script elif self.cleaned_data.get('action_type') == EventRuleActionChoices.SCRIPT: - self.cleaned_data['action_object_type'] = ContentType.objects.get_for_model( + self.cleaned_data['action_object_type'] = ObjectType.objects.get_for_model( Script, for_concrete_model=False ) @@ -356,7 +356,7 @@ class TagForm(forms.ModelForm): slug = SlugField() object_types = ContentTypeMultipleChoiceField( label=_('Object types'), - queryset=ContentType.objects.with_feature('tags'), + queryset=ObjectType.objects.with_feature('tags'), required=False ) diff --git a/netbox/extras/models/change_logging.py b/netbox/extras/models/change_logging.py index 0155849aa..ebcebc09a 100644 --- a/netbox/extras/models/change_logging.py +++ b/netbox/extras/models/change_logging.py @@ -5,7 +5,7 @@ from django.db import models from django.urls import reverse from django.utils.translation import gettext_lazy as _ -from core.models import ContentType +from core.models import ObjectType from extras.choices import * from ..querysets import ObjectChangeQuerySet @@ -113,7 +113,7 @@ class ObjectChange(models.Model): super().clean() # Validate the assigned object type - if self.changed_object_type not in ContentType.objects.with_feature('change_logging'): + if self.changed_object_type not in ObjectType.objects.with_feature('change_logging'): raise ValidationError( _("Change logging is not supported for this object type ({type}).").format( type=self.changed_object_type diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py index e78d1af23..34b58e712 100644 --- a/netbox/extras/models/customfields.py +++ b/netbox/extras/models/customfields.py @@ -12,7 +12,7 @@ from django.urls import reverse from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ -from core.models import ContentType +from core.models import ObjectType from extras.choices import * from extras.data import CHOICE_SETS from netbox.models import ChangeLoggedModel @@ -52,7 +52,7 @@ class CustomFieldManager(models.Manager.from_queryset(RestrictedQuerySet)): """ Return all CustomFields assigned to the given model. """ - content_type = ContentType.objects.get_for_model(model._meta.concrete_model) + content_type = ObjectType.objects.get_for_model(model._meta.concrete_model) return self.get_queryset().filter(content_types=content_type) def get_defaults_for_model(self, model): diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index 60bccd8f2..6b5a7c150 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -12,7 +12,7 @@ from django.utils.formats import date_format from django.utils.translation import gettext_lazy as _ from rest_framework.utils.encoders import JSONEncoder -from core.models import ContentType +from core.models import ObjectType from extras.choices import * from extras.conditions import ConditionSet from extras.constants import * @@ -646,7 +646,7 @@ class ImageAttachment(ChangeLoggedModel): super().clean() # Validate the assigned object type - if self.content_type not in ContentType.objects.with_feature('image_attachments'): + if self.content_type not in ObjectType.objects.with_feature('image_attachments'): raise ValidationError( _("Image attachments cannot be assigned to this object type ({type}).").format(type=self.content_type) ) @@ -739,7 +739,7 @@ class JournalEntry(CustomFieldsMixin, CustomLinksMixin, TagsMixin, ExportTemplat super().clean() # Validate the assigned object type - if self.assigned_object_type not in ContentType.objects.with_feature('journaling'): + if self.assigned_object_type not in ObjectType.objects.with_feature('journaling'): raise ValidationError( _("Journaling is not supported for this object type ({type}).").format(type=self.assigned_object_type) ) @@ -795,7 +795,7 @@ class Bookmark(models.Model): super().clean() # Validate the assigned object type - if self.object_type not in ContentType.objects.with_feature('bookmarks'): + if self.object_type not in ObjectType.objects.with_feature('bookmarks'): raise ValidationError( _("Bookmarks cannot be assigned to this object type ({type}).").format(type=self.object_type) ) diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py index 76fae2990..cce5b6b68 100644 --- a/netbox/ipam/models/ip.py +++ b/netbox/ipam/models/ip.py @@ -8,7 +8,7 @@ from django.urls import reverse from django.utils.functional import cached_property from django.utils.translation import gettext_lazy as _ -from core.models import ContentType +from core.models import ObjectType from ipam.choices import * from ipam.constants import * from ipam.fields import IPNetworkField, IPAddressField @@ -861,7 +861,7 @@ class IPAddress(PrimaryModel): if self._original_assigned_object_id and self._original_assigned_object_type_id: parent = getattr(self.assigned_object, 'parent_object', None) - ct = ContentType.objects.get_for_id(self._original_assigned_object_type_id) + ct = ObjectType.objects.get_for_id(self._original_assigned_object_type_id) original_assigned_object = ct.get_object_for_this_type(pk=self._original_assigned_object_id) original_parent = getattr(original_assigned_object, 'parent_object', None) diff --git a/netbox/netbox/models/features.py b/netbox/netbox/models/features.py index dbd008354..fb6cf8498 100644 --- a/netbox/netbox/models/features.py +++ b/netbox/netbox/models/features.py @@ -10,7 +10,7 @@ from django.utils.translation import gettext_lazy as _ from taggit.managers import TaggableManager from core.choices import JobStatusChoices -from core.models import ContentType +from core.models import ObjectType from extras.choices import * from extras.utils import is_taggable from netbox.config import get_config @@ -490,7 +490,7 @@ class SyncedDataMixin(models.Model): ret = super().save(*args, **kwargs) # Create/delete AutoSyncRecord as needed - content_type = ContentType.objects.get_for_model(self) + content_type = ObjectType.objects.get_for_model(self) if self.auto_sync_enabled: AutoSyncRecord.objects.update_or_create( object_type=content_type, @@ -510,7 +510,7 @@ class SyncedDataMixin(models.Model): from core.models import AutoSyncRecord # Delete AutoSyncRecord - content_type = ContentType.objects.get_for_model(self) + content_type = ObjectType.objects.get_for_model(self) AutoSyncRecord.objects.filter( datafile=self.data_file, object_type=content_type, diff --git a/netbox/tenancy/forms/filtersets.py b/netbox/tenancy/forms/filtersets.py index 77e945542..e5f038923 100644 --- a/netbox/tenancy/forms/filtersets.py +++ b/netbox/tenancy/forms/filtersets.py @@ -1,7 +1,7 @@ from django import forms from django.utils.translation import gettext_lazy as _ -from core.models import ContentType +from core.models import ObjectType from netbox.forms import NetBoxModelFilterSetForm from tenancy.choices import * from tenancy.models import * @@ -86,7 +86,7 @@ class ContactAssignmentFilterForm(NetBoxModelFilterSetForm): (_('Assignment'), ('content_type_id', 'group_id', 'contact_id', 'role_id', 'priority')), ) content_type_id = ContentTypeMultipleChoiceField( - queryset=ContentType.objects.with_feature('contacts'), + queryset=ObjectType.objects.with_feature('contacts'), required=False, label=_('Object type') ) diff --git a/netbox/tenancy/models/contacts.py b/netbox/tenancy/models/contacts.py index 664fff098..1ea62db0c 100644 --- a/netbox/tenancy/models/contacts.py +++ b/netbox/tenancy/models/contacts.py @@ -4,7 +4,7 @@ from django.db import models from django.urls import reverse from django.utils.translation import gettext_lazy as _ -from core.models import ContentType +from core.models import ObjectType from netbox.models import ChangeLoggedModel, NestedGroupModel, OrganizationalModel, PrimaryModel from netbox.models.features import CustomFieldsMixin, ExportTemplatesMixin, TagsMixin from tenancy.choices import * @@ -165,7 +165,7 @@ class ContactAssignment(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, Chan super().clean() # Validate the assigned object type - if self.content_type not in ContentType.objects.with_feature('contacts'): + if self.content_type not in ObjectType.objects.with_feature('contacts'): raise ValidationError( _("Contacts cannot be assigned to this object type ({type}).").format(type=self.content_type) ) diff --git a/netbox/users/models.py b/netbox/users/models.py index 19d6013c7..94eb0ad58 100644 --- a/netbox/users/models.py +++ b/netbox/users/models.py @@ -22,7 +22,7 @@ from django.utils import timezone from django.utils.translation import gettext_lazy as _ from netaddr import IPNetwork -from core.models import ContentType +from core.models import ObjectType from ipam.fields import IPNetworkField from netbox.config import get_config from utilities.querysets import RestrictedQuerySet diff --git a/netbox/vpn/models/l2vpn.py b/netbox/vpn/models/l2vpn.py index 31d267113..39956edc8 100644 --- a/netbox/vpn/models/l2vpn.py +++ b/netbox/vpn/models/l2vpn.py @@ -5,7 +5,7 @@ from django.urls import reverse from django.utils.functional import cached_property from django.utils.translation import gettext_lazy as _ -from core.models import ContentType +from core.models import ObjectType from netbox.models import NetBoxModel, PrimaryModel from netbox.models.features import ContactsMixin from vpn.choices import L2VPNTypeChoices @@ -128,7 +128,7 @@ class L2VPNTermination(NetBoxModel): # Only check is assigned_object is set. Required otherwise we have an Integrity Error thrown. if self.assigned_object: obj_id = self.assigned_object.pk - obj_type = ContentType.objects.get_for_model(self.assigned_object) + obj_type = ObjectType.objects.get_for_model(self.assigned_object) if L2VPNTermination.objects.filter(assigned_object_id=obj_id, assigned_object_type=obj_type).\ exclude(pk=self.pk).count() > 0: raise ValidationError( @@ -150,7 +150,7 @@ class L2VPNTermination(NetBoxModel): @property def assigned_object_parent(self): - obj_type = ContentType.objects.get_for_model(self.assigned_object) + obj_type = ObjectType.objects.get_for_model(self.assigned_object) if obj_type.model == 'vminterface': return self.assigned_object.virtual_machine elif obj_type.model == 'interface': From aeeec284a5d55cc70acff5628e4d0aa46589c892 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 1 Mar 2024 14:36:35 -0500 Subject: [PATCH 11/47] Rename CustomField.content_types to object_types & use ObjectType proxy --- netbox/dcim/tests/test_models.py | 6 +- netbox/extras/api/customfields.py | 4 +- netbox/extras/api/serializers.py | 4 +- netbox/extras/filtersets.py | 10 ++- netbox/extras/forms/bulk_import.py | 6 +- netbox/extras/forms/filtersets.py | 4 +- netbox/extras/forms/model_forms.py | 7 +- netbox/extras/graphql/types.py | 2 +- .../migrations/0111_rename_content_types.py | 28 ++++++ netbox/extras/models/customfields.py | 10 +-- netbox/extras/signals.py | 6 +- netbox/extras/tests/test_api.py | 14 +-- netbox/extras/tests/test_changelog.py | 13 +-- netbox/extras/tests/test_customfields.py | 90 +++++++++---------- netbox/extras/tests/test_filtersets.py | 17 ++-- netbox/extras/tests/test_forms.py | 29 +++--- netbox/extras/tests/test_views.py | 9 +- netbox/extras/views.py | 4 +- netbox/netbox/api/viewsets/mixins.py | 5 +- netbox/netbox/filtersets.py | 2 +- netbox/netbox/forms/base.py | 2 +- netbox/netbox/forms/mixins.py | 7 +- netbox/netbox/middleware.py | 4 +- netbox/netbox/search/backends.py | 13 +-- netbox/netbox/tables/tables.py | 8 +- netbox/utilities/testing/base.py | 7 +- netbox/utilities/tests/test_api.py | 8 +- 27 files changed, 177 insertions(+), 142 deletions(-) create mode 100644 netbox/extras/migrations/0111_rename_content_types.py diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index 8eb057020..1a5cc8435 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -1,8 +1,8 @@ -from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError from django.test import TestCase from circuits.models import * +from core.models import ObjectType from dcim.choices import * from dcim.models import * from extras.models import CustomField @@ -293,8 +293,8 @@ class DeviceTestCase(TestCase): # Create a CustomField with a default value & assign it to all component models cf1 = CustomField.objects.create(name='cf1', default='foo') - cf1.content_types.set( - ContentType.objects.filter(app_label='dcim', model__in=[ + cf1.object_types.set( + ObjectType.objects.filter(app_label='dcim', model__in=[ 'consoleport', 'consoleserverport', 'powerport', diff --git a/netbox/extras/api/customfields.py b/netbox/extras/api/customfields.py index 6cd3a245e..77c3a170e 100644 --- a/netbox/extras/api/customfields.py +++ b/netbox/extras/api/customfields.py @@ -26,7 +26,7 @@ class CustomFieldDefaultValues: # Retrieve the CustomFields for the parent model content_type = ContentType.objects.get_for_model(self.model) - fields = CustomField.objects.filter(content_types=content_type) + fields = CustomField.objects.filter(object_types=content_type) # Populate the default value for each CustomField value = {} @@ -48,7 +48,7 @@ class CustomFieldsDataField(Field): """ if not hasattr(self, '_custom_fields'): content_type = ContentType.objects.get_for_model(self.parent.Meta.model) - self._custom_fields = CustomField.objects.filter(content_types=content_type) + self._custom_fields = CustomField.objects.filter(object_types=content_type) return self._custom_fields def to_representation(self, obj): diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index a87947d68..370450712 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -117,7 +117,7 @@ class WebhookSerializer(NetBoxModelSerializer): class CustomFieldSerializer(ValidatedModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfield-detail') - content_types = ContentTypeField( + object_types = ContentTypeField( queryset=ObjectType.objects.with_feature('custom_fields'), many=True ) @@ -139,7 +139,7 @@ class CustomFieldSerializer(ValidatedModelSerializer): class Meta: model = CustomField fields = [ - 'id', 'url', 'display', 'content_types', 'type', 'object_type', 'data_type', 'name', 'label', 'group_name', + 'id', 'url', 'display', 'object_types', 'type', 'object_type', 'data_type', 'name', 'label', 'group_name', 'description', 'required', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'is_cloneable', 'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choice_set', 'created', 'last_updated', diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py index 734f7db50..290670f5c 100644 --- a/netbox/extras/filtersets.py +++ b/netbox/extras/filtersets.py @@ -124,10 +124,12 @@ class CustomFieldFilterSet(BaseFilterSet): type = django_filters.MultipleChoiceFilter( choices=CustomFieldTypeChoices ) - content_type_id = MultiValueNumberFilter( - field_name='content_types__id' + object_types_id = MultiValueNumberFilter( + field_name='object_types__id' + ) + object_types = ContentTypeFilter( + field_name='object_types' ) - content_types = ContentTypeFilter() choice_set_id = django_filters.ModelMultipleChoiceFilter( queryset=CustomFieldChoiceSet.objects.all() ) @@ -140,7 +142,7 @@ class CustomFieldFilterSet(BaseFilterSet): class Meta: model = CustomField fields = [ - 'id', 'content_types', 'name', 'group_name', 'required', 'search_weight', 'filter_logic', 'ui_visible', + 'id', 'object_types', 'name', 'group_name', 'required', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'weight', 'is_cloneable', 'description', ] diff --git a/netbox/extras/forms/bulk_import.py b/netbox/extras/forms/bulk_import.py index 133233ccc..1d1b27617 100644 --- a/netbox/extras/forms/bulk_import.py +++ b/netbox/extras/forms/bulk_import.py @@ -30,8 +30,8 @@ __all__ = ( class CustomFieldImportForm(CSVModelForm): - content_types = CSVMultipleContentTypeField( - label=_('Content types'), + object_types = CSVMultipleContentTypeField( + label=_('Object types'), queryset=ObjectType.objects.with_feature('custom_fields'), help_text=_("One or more assigned object types") ) @@ -69,7 +69,7 @@ class CustomFieldImportForm(CSVModelForm): class Meta: model = CustomField fields = ( - 'name', 'label', 'group_name', 'type', 'content_types', 'object_type', 'required', 'description', + 'name', 'label', 'group_name', 'type', 'object_types', 'object_type', 'required', 'description', 'search_weight', 'filter_logic', 'default', 'choice_set', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'ui_visible', 'ui_editable', 'is_cloneable', ) diff --git a/netbox/extras/forms/filtersets.py b/netbox/extras/forms/filtersets.py index 075144d12..ccfeb8c1d 100644 --- a/netbox/extras/forms/filtersets.py +++ b/netbox/extras/forms/filtersets.py @@ -38,11 +38,11 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm): fieldsets = ( (None, ('q', 'filter_id')), (_('Attributes'), ( - 'type', 'content_type_id', 'group_name', 'weight', 'required', 'choice_set_id', 'ui_visible', 'ui_editable', + 'type', 'object_types_id', 'group_name', 'weight', 'required', 'choice_set_id', 'ui_visible', 'ui_editable', 'is_cloneable', )), ) - content_type_id = ContentTypeMultipleChoiceField( + object_types_id = ContentTypeMultipleChoiceField( queryset=ObjectType.objects.with_feature('custom_fields'), required=False, label=_('Object type') diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py index 443ba0a30..776265878 100644 --- a/netbox/extras/forms/model_forms.py +++ b/netbox/extras/forms/model_forms.py @@ -2,7 +2,6 @@ import json import re from django import forms -from django.contrib.contenttypes.models import ContentType from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ @@ -39,8 +38,8 @@ __all__ = ( class CustomFieldForm(forms.ModelForm): - content_types = ContentTypeMultipleChoiceField( - label=_('Content types'), + object_types = ContentTypeMultipleChoiceField( + label=_('Object types'), queryset=ObjectType.objects.with_feature('custom_fields') ) object_type = ContentTypeChoiceField( @@ -56,7 +55,7 @@ class CustomFieldForm(forms.ModelForm): fieldsets = ( (_('Custom Field'), ( - 'content_types', 'name', 'label', 'group_name', 'type', 'object_type', 'required', 'description', + 'object_types', 'name', 'label', 'group_name', 'type', 'object_type', 'required', 'description', )), (_('Behavior'), ('search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'weight', 'is_cloneable')), (_('Values'), ('default', 'choice_set')), diff --git a/netbox/extras/graphql/types.py b/netbox/extras/graphql/types.py index 4981ddd72..643568eaa 100644 --- a/netbox/extras/graphql/types.py +++ b/netbox/extras/graphql/types.py @@ -39,7 +39,7 @@ class CustomFieldType(ObjectType): class Meta: model = models.CustomField - exclude = ('content_types', ) + exclude = ('object_types', 'object_type') filterset_class = filtersets.CustomFieldFilterSet diff --git a/netbox/extras/migrations/0111_rename_content_types.py b/netbox/extras/migrations/0111_rename_content_types.py new file mode 100644 index 000000000..2b7b2358a --- /dev/null +++ b/netbox/extras/migrations/0111_rename_content_types.py @@ -0,0 +1,28 @@ +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0010_gfk_indexes'), + ('extras', '0110_remove_eventrule_action_parameters'), + ] + + operations = [ + migrations.RenameField( + model_name='customfield', + old_name='content_types', + new_name='object_types', + ), + migrations.AlterField( + model_name='customfield', + name='object_types', + field=models.ManyToManyField(related_name='custom_fields', to='core.objecttype'), + ), + migrations.AlterField( + model_name='customfield', + name='object_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='core.objecttype'), + ), + ] diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py index 34b58e712..311ccce76 100644 --- a/netbox/extras/models/customfields.py +++ b/netbox/extras/models/customfields.py @@ -53,7 +53,7 @@ class CustomFieldManager(models.Manager.from_queryset(RestrictedQuerySet)): Return all CustomFields assigned to the given model. """ content_type = ObjectType.objects.get_for_model(model._meta.concrete_model) - return self.get_queryset().filter(content_types=content_type) + return self.get_queryset().filter(object_types=content_type) def get_defaults_for_model(self, model): """ @@ -66,8 +66,8 @@ class CustomFieldManager(models.Manager.from_queryset(RestrictedQuerySet)): class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): - content_types = models.ManyToManyField( - to='contenttypes.ContentType', + object_types = models.ManyToManyField( + to='core.ObjectType', related_name='custom_fields', help_text=_('The object(s) to which this field applies.') ) @@ -79,7 +79,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): help_text=_('The type of data this custom field holds') ) object_type = models.ForeignKey( - to='contenttypes.ContentType', + to='core.ObjectType', on_delete=models.PROTECT, blank=True, null=True, @@ -284,7 +284,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): """ Called when a CustomField has been renamed. Updates all assigned object data. """ - for ct in self.content_types.all(): + for ct in self.object_types.all(): model = ct.model_class() params = {f'custom_field_data__{old_name}__isnull': False} instances = model.objects.filter(**params) diff --git a/netbox/extras/signals.py b/netbox/extras/signals.py index f8dc204e7..85c00169c 100644 --- a/netbox/extras/signals.py +++ b/netbox/extras/signals.py @@ -205,13 +205,13 @@ def handle_cf_deleted(instance, **kwargs): """ Handle the cleanup of old custom field data when a CustomField is deleted. """ - instance.remove_stale_data(instance.content_types.all()) + instance.remove_stale_data(instance.object_types.all()) post_save.connect(handle_cf_renamed, sender=CustomField) pre_delete.connect(handle_cf_deleted, sender=CustomField) -m2m_changed.connect(handle_cf_added_obj_types, sender=CustomField.content_types.through) -m2m_changed.connect(handle_cf_removed_obj_types, sender=CustomField.content_types.through) +m2m_changed.connect(handle_cf_added_obj_types, sender=CustomField.object_types.through) +m2m_changed.connect(handle_cf_removed_obj_types, sender=CustomField.object_types.through) # diff --git a/netbox/extras/tests/test_api.py b/netbox/extras/tests/test_api.py index 5db906b25..01bbb312f 100644 --- a/netbox/extras/tests/test_api.py +++ b/netbox/extras/tests/test_api.py @@ -7,10 +7,10 @@ from django.utils.timezone import make_aware from rest_framework import status from core.choices import ManagedFileRootPathChoices +from core.models import ObjectType from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Rack, Location, RackRole, Site from extras.choices import * from extras.models import * -from extras.reports import Report from extras.scripts import BooleanVar, IntegerVar, Script as PythonClass, StringVar from utilities.testing import APITestCase, APIViewTestCases @@ -152,17 +152,17 @@ class CustomFieldTest(APIViewTestCases.APIViewTestCase): brief_fields = ['description', 'display', 'id', 'name', 'url'] create_data = [ { - 'content_types': ['dcim.site'], + 'object_types': ['dcim.site'], 'name': 'cf4', 'type': 'date', }, { - 'content_types': ['dcim.site'], + 'object_types': ['dcim.site'], 'name': 'cf5', 'type': 'url', }, { - 'content_types': ['dcim.site'], + 'object_types': ['dcim.site'], 'name': 'cf6', 'type': 'text', }, @@ -171,14 +171,14 @@ class CustomFieldTest(APIViewTestCases.APIViewTestCase): 'description': 'New description', } update_data = { - 'content_types': ['dcim.device'], + 'object_types': ['dcim.device'], 'name': 'New_Name', 'description': 'New description', } @classmethod def setUpTestData(cls): - site_ct = ContentType.objects.get_for_model(Site) + site_ct = ObjectType.objects.get_for_model(Site) custom_fields = ( CustomField( @@ -196,7 +196,7 @@ class CustomFieldTest(APIViewTestCases.APIViewTestCase): ) CustomField.objects.bulk_create(custom_fields) for cf in custom_fields: - cf.content_types.add(site_ct) + cf.object_types.add(site_ct) class CustomFieldChoiceSetTest(APIViewTestCases.APIViewTestCase): diff --git a/netbox/extras/tests/test_changelog.py b/netbox/extras/tests/test_changelog.py index e144c5dee..d9d6f1f45 100644 --- a/netbox/extras/tests/test_changelog.py +++ b/netbox/extras/tests/test_changelog.py @@ -3,6 +3,7 @@ from django.test import override_settings from django.urls import reverse from rest_framework import status +from core.models import ObjectType from dcim.choices import SiteStatusChoices from dcim.models import Site from extras.choices import * @@ -23,14 +24,14 @@ class ChangeLogViewTest(ModelViewTestCase): ) # Create a custom field on the Site model - ct = ContentType.objects.get_for_model(Site) + site_type = ObjectType.objects.get_for_model(Site) cf = CustomField( type=CustomFieldTypeChoices.TYPE_TEXT, name='cf1', required=False ) cf.save() - cf.content_types.set([ct]) + cf.object_types.set([site_type]) # Create a select custom field on the Site model cf_select = CustomField( @@ -40,7 +41,7 @@ class ChangeLogViewTest(ModelViewTestCase): choice_set=choice_set ) cf_select.save() - cf_select.content_types.set([ct]) + cf_select.object_types.set([site_type]) def test_create_object(self): tags = create_tags('Tag 1', 'Tag 2') @@ -275,14 +276,14 @@ class ChangeLogAPITest(APITestCase): def setUpTestData(cls): # Create a custom field on the Site model - ct = ContentType.objects.get_for_model(Site) + site_type = ObjectType.objects.get_for_model(Site) cf = CustomField( type=CustomFieldTypeChoices.TYPE_TEXT, name='cf1', required=False ) cf.save() - cf.content_types.set([ct]) + cf.object_types.set([site_type]) # Create a select custom field on the Site model choice_set = CustomFieldChoiceSet.objects.create( @@ -296,7 +297,7 @@ class ChangeLogAPITest(APITestCase): choice_set=choice_set ) cf_select.save() - cf_select.content_types.set([ct]) + cf_select.object_types.set([site_type]) # Create some tags tags = ( diff --git a/netbox/extras/tests/test_customfields.py b/netbox/extras/tests/test_customfields.py index 574452a81..7ca18250c 100644 --- a/netbox/extras/tests/test_customfields.py +++ b/netbox/extras/tests/test_customfields.py @@ -1,11 +1,11 @@ import datetime from decimal import Decimal -from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError from django.urls import reverse from rest_framework import status +from core.models import ObjectType from dcim.filtersets import SiteFilterSet from dcim.forms import SiteImportForm from dcim.models import Manufacturer, Rack, Site @@ -28,7 +28,7 @@ class CustomFieldTest(TestCase): Site(name='Site C', slug='site-c'), ]) - cls.object_type = ContentType.objects.get_for_model(Site) + cls.object_type = ObjectType.objects.get_for_model(Site) def test_invalid_name(self): """ @@ -50,7 +50,7 @@ class CustomFieldTest(TestCase): type=CustomFieldTypeChoices.TYPE_TEXT, required=False ) - cf.content_types.set([self.object_type]) + cf.object_types.set([self.object_type]) instance = Site.objects.first() self.assertIsNone(instance.custom_field_data[cf.name]) @@ -75,7 +75,7 @@ class CustomFieldTest(TestCase): type=CustomFieldTypeChoices.TYPE_LONGTEXT, required=False ) - cf.content_types.set([self.object_type]) + cf.object_types.set([self.object_type]) instance = Site.objects.first() self.assertIsNone(instance.custom_field_data[cf.name]) @@ -99,7 +99,7 @@ class CustomFieldTest(TestCase): type=CustomFieldTypeChoices.TYPE_INTEGER, required=False ) - cf.content_types.set([self.object_type]) + cf.object_types.set([self.object_type]) instance = Site.objects.first() self.assertIsNone(instance.custom_field_data[cf.name]) @@ -125,7 +125,7 @@ class CustomFieldTest(TestCase): type=CustomFieldTypeChoices.TYPE_DECIMAL, required=False ) - cf.content_types.set([self.object_type]) + cf.object_types.set([self.object_type]) instance = Site.objects.first() self.assertIsNone(instance.custom_field_data[cf.name]) @@ -151,7 +151,7 @@ class CustomFieldTest(TestCase): type=CustomFieldTypeChoices.TYPE_INTEGER, required=False ) - cf.content_types.set([self.object_type]) + cf.object_types.set([self.object_type]) instance = Site.objects.first() self.assertIsNone(instance.custom_field_data[cf.name]) @@ -178,7 +178,7 @@ class CustomFieldTest(TestCase): type=CustomFieldTypeChoices.TYPE_DATE, required=False ) - cf.content_types.set([self.object_type]) + cf.object_types.set([self.object_type]) instance = Site.objects.first() self.assertIsNone(instance.custom_field_data[cf.name]) @@ -203,7 +203,7 @@ class CustomFieldTest(TestCase): type=CustomFieldTypeChoices.TYPE_DATETIME, required=False ) - cf.content_types.set([self.object_type]) + cf.object_types.set([self.object_type]) instance = Site.objects.first() self.assertIsNone(instance.custom_field_data[cf.name]) @@ -228,7 +228,7 @@ class CustomFieldTest(TestCase): type=CustomFieldTypeChoices.TYPE_URL, required=False ) - cf.content_types.set([self.object_type]) + cf.object_types.set([self.object_type]) instance = Site.objects.first() self.assertIsNone(instance.custom_field_data[cf.name]) @@ -253,7 +253,7 @@ class CustomFieldTest(TestCase): type=CustomFieldTypeChoices.TYPE_JSON, required=False ) - cf.content_types.set([self.object_type]) + cf.object_types.set([self.object_type]) instance = Site.objects.first() self.assertIsNone(instance.custom_field_data[cf.name]) @@ -290,7 +290,7 @@ class CustomFieldTest(TestCase): required=False, choice_set=choice_set ) - cf.content_types.set([self.object_type]) + cf.object_types.set([self.object_type]) instance = Site.objects.first() self.assertIsNone(instance.custom_field_data[cf.name]) @@ -327,7 +327,7 @@ class CustomFieldTest(TestCase): required=False, choice_set=choice_set ) - cf.content_types.set([self.object_type]) + cf.object_types.set([self.object_type]) instance = Site.objects.first() self.assertIsNone(instance.custom_field_data[cf.name]) @@ -350,10 +350,10 @@ class CustomFieldTest(TestCase): cf = CustomField.objects.create( name='object_field', type=CustomFieldTypeChoices.TYPE_OBJECT, - object_type=ContentType.objects.get_for_model(VLAN), + object_type=ObjectType.objects.get_for_model(VLAN), required=False ) - cf.content_types.set([self.object_type]) + cf.object_types.set([self.object_type]) instance = Site.objects.first() self.assertIsNone(instance.custom_field_data[cf.name]) @@ -382,10 +382,10 @@ class CustomFieldTest(TestCase): cf = CustomField.objects.create( name='object_field', type=CustomFieldTypeChoices.TYPE_MULTIOBJECT, - object_type=ContentType.objects.get_for_model(VLAN), + object_type=ObjectType.objects.get_for_model(VLAN), required=False ) - cf.content_types.set([self.object_type]) + cf.object_types.set([self.object_type]) instance = Site.objects.first() self.assertIsNone(instance.custom_field_data[cf.name]) @@ -402,13 +402,13 @@ class CustomFieldTest(TestCase): self.assertIsNone(instance.custom_field_data.get(cf.name)) def test_rename_customfield(self): - obj_type = ContentType.objects.get_for_model(Site) + obj_type = ObjectType.objects.get_for_model(Site) FIELD_DATA = 'abc' # Create a custom field cf = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='field1') cf.save() - cf.content_types.set([obj_type]) + cf.object_types.set([obj_type]) # Assign custom field data to an object site = Site.objects.create( @@ -437,7 +437,7 @@ class CustomFieldTest(TestCase): ) ) site = Site.objects.create(name='Site 1', slug='site-1') - object_type = ContentType.objects.get_for_model(Site) + object_type = ObjectType.objects.get_for_model(Site) # Text CustomField(name='test', type='text', required=True, default="Default text").full_clean() @@ -524,10 +524,10 @@ class CustomFieldManagerTest(TestCase): @classmethod def setUpTestData(cls): - content_type = ContentType.objects.get_for_model(Site) + object_type = ObjectType.objects.get_for_model(Site) custom_field = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='text_field', default='foo') custom_field.save() - custom_field.content_types.set([content_type]) + custom_field.object_types.set([object_type]) def test_get_for_model(self): self.assertEqual(CustomField.objects.get_for_model(Site).count(), 1) @@ -538,7 +538,7 @@ class CustomFieldAPITest(APITestCase): @classmethod def setUpTestData(cls): - content_type = ContentType.objects.get_for_model(Site) + object_type = ObjectType.objects.get_for_model(Site) # Create some VLANs vlans = ( @@ -581,19 +581,19 @@ class CustomFieldAPITest(APITestCase): CustomField( type=CustomFieldTypeChoices.TYPE_OBJECT, name='object_field', - object_type=ContentType.objects.get_for_model(VLAN), + object_type=ObjectType.objects.get_for_model(VLAN), default=vlans[0].pk, ), CustomField( type=CustomFieldTypeChoices.TYPE_MULTIOBJECT, name='multiobject_field', - object_type=ContentType.objects.get_for_model(VLAN), + object_type=ObjectType.objects.get_for_model(VLAN), default=[vlans[0].pk, vlans[1].pk], ), ) for cf in custom_fields: cf.save() - cf.content_types.set([content_type]) + cf.object_types.set([object_type]) # Create some sites *after* creating the custom fields. This ensures that # default values are not set for the assigned objects. @@ -1163,7 +1163,7 @@ class CustomFieldImportTest(TestCase): ) for cf in custom_fields: cf.save() - cf.content_types.set([ContentType.objects.get_for_model(Site)]) + cf.object_types.set([ObjectType.objects.get_for_model(Site)]) def test_import(self): """ @@ -1256,11 +1256,11 @@ class CustomFieldModelTest(TestCase): def setUpTestData(cls): cf1 = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='foo') cf1.save() - cf1.content_types.set([ContentType.objects.get_for_model(Site)]) + cf1.object_types.set([ObjectType.objects.get_for_model(Site)]) cf2 = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='bar') cf2.save() - cf2.content_types.set([ContentType.objects.get_for_model(Rack)]) + cf2.object_types.set([ObjectType.objects.get_for_model(Rack)]) def test_cf_data(self): """ @@ -1299,7 +1299,7 @@ class CustomFieldModelTest(TestCase): """ cf3 = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='baz', required=True) cf3.save() - cf3.content_types.set([ContentType.objects.get_for_model(Site)]) + cf3.object_types.set([ObjectType.objects.get_for_model(Site)]) site = Site(name='Test Site', slug='test-site') @@ -1318,7 +1318,7 @@ class CustomFieldModelFilterTest(TestCase): @classmethod def setUpTestData(cls): - obj_type = ContentType.objects.get_for_model(Site) + object_type = ObjectType.objects.get_for_model(Site) manufacturers = Manufacturer.objects.bulk_create(( Manufacturer(name='Manufacturer 1', slug='manufacturer-1'), @@ -1335,17 +1335,17 @@ class CustomFieldModelFilterTest(TestCase): # Integer filtering cf = CustomField(name='cf1', type=CustomFieldTypeChoices.TYPE_INTEGER) cf.save() - cf.content_types.set([obj_type]) + cf.object_types.set([object_type]) # Decimal filtering cf = CustomField(name='cf2', type=CustomFieldTypeChoices.TYPE_DECIMAL) cf.save() - cf.content_types.set([obj_type]) + cf.object_types.set([object_type]) # Boolean filtering cf = CustomField(name='cf3', type=CustomFieldTypeChoices.TYPE_BOOLEAN) cf.save() - cf.content_types.set([obj_type]) + cf.object_types.set([object_type]) # Exact text filtering cf = CustomField( @@ -1354,7 +1354,7 @@ class CustomFieldModelFilterTest(TestCase): filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT ) cf.save() - cf.content_types.set([obj_type]) + cf.object_types.set([object_type]) # Loose text filtering cf = CustomField( @@ -1363,12 +1363,12 @@ class CustomFieldModelFilterTest(TestCase): filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE ) cf.save() - cf.content_types.set([obj_type]) + cf.object_types.set([object_type]) # Date filtering cf = CustomField(name='cf6', type=CustomFieldTypeChoices.TYPE_DATE) cf.save() - cf.content_types.set([obj_type]) + cf.object_types.set([object_type]) # Exact URL filtering cf = CustomField( @@ -1377,7 +1377,7 @@ class CustomFieldModelFilterTest(TestCase): filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT ) cf.save() - cf.content_types.set([obj_type]) + cf.object_types.set([object_type]) # Loose URL filtering cf = CustomField( @@ -1386,7 +1386,7 @@ class CustomFieldModelFilterTest(TestCase): filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE ) cf.save() - cf.content_types.set([obj_type]) + cf.object_types.set([object_type]) # Selection filtering cf = CustomField( @@ -1395,7 +1395,7 @@ class CustomFieldModelFilterTest(TestCase): choice_set=choice_set ) cf.save() - cf.content_types.set([obj_type]) + cf.object_types.set([object_type]) # Multiselect filtering cf = CustomField( @@ -1404,25 +1404,25 @@ class CustomFieldModelFilterTest(TestCase): choice_set=choice_set ) cf.save() - cf.content_types.set([obj_type]) + cf.object_types.set([object_type]) # Object filtering cf = CustomField( name='cf11', type=CustomFieldTypeChoices.TYPE_OBJECT, - object_type=ContentType.objects.get_for_model(Manufacturer) + object_type=ObjectType.objects.get_for_model(Manufacturer) ) cf.save() - cf.content_types.set([obj_type]) + cf.object_types.set([object_type]) # Multi-object filtering cf = CustomField( name='cf12', type=CustomFieldTypeChoices.TYPE_MULTIOBJECT, - object_type=ContentType.objects.get_for_model(Manufacturer) + object_type=ObjectType.objects.get_for_model(Manufacturer) ) cf.save() - cf.content_types.set([obj_type]) + cf.object_types.set([object_type]) Site.objects.bulk_create([ Site(name='Site 1', slug='site-1', custom_field_data={ diff --git a/netbox/extras/tests/test_filtersets.py b/netbox/extras/tests/test_filtersets.py index ef8aedcbd..25263fe68 100644 --- a/netbox/extras/tests/test_filtersets.py +++ b/netbox/extras/tests/test_filtersets.py @@ -7,6 +7,7 @@ from django.test import TestCase from circuits.models import Provider from core.choices import ManagedFileRootPathChoices +from core.models import ObjectType from dcim.filtersets import SiteFilterSet from dcim.models import DeviceRole, DeviceType, Manufacturer, Platform, Rack, Region, Site, SiteGroup from dcim.models import Location @@ -87,11 +88,11 @@ class CustomFieldTestCase(TestCase, BaseFilterSetTests): ), ) CustomField.objects.bulk_create(custom_fields) - custom_fields[0].content_types.add(ContentType.objects.get_by_natural_key('dcim', 'site')) - custom_fields[1].content_types.add(ContentType.objects.get_by_natural_key('dcim', 'rack')) - custom_fields[2].content_types.add(ContentType.objects.get_by_natural_key('dcim', 'device')) - custom_fields[3].content_types.add(ContentType.objects.get_by_natural_key('dcim', 'device')) - custom_fields[4].content_types.add(ContentType.objects.get_by_natural_key('dcim', 'device')) + custom_fields[0].object_types.add(ObjectType.objects.get_by_natural_key('dcim', 'site')) + custom_fields[1].object_types.add(ObjectType.objects.get_by_natural_key('dcim', 'rack')) + custom_fields[2].object_types.add(ObjectType.objects.get_by_natural_key('dcim', 'device')) + custom_fields[3].object_types.add(ObjectType.objects.get_by_natural_key('dcim', 'device')) + custom_fields[4].object_types.add(ObjectType.objects.get_by_natural_key('dcim', 'device')) def test_q(self): params = {'q': 'foobar1'} @@ -101,10 +102,10 @@ class CustomFieldTestCase(TestCase, BaseFilterSetTests): params = {'name': ['Custom Field 1', 'Custom Field 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - def test_content_types(self): - params = {'content_types': 'dcim.site'} + def test_object_types(self): + params = {'object_types': 'dcim.site'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) - params = {'content_type_id': [ContentType.objects.get_by_natural_key('dcim', 'site').pk]} + params = {'object_types_id': [ObjectType.objects.get_by_natural_key('dcim', 'site').pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) def test_required(self): diff --git a/netbox/extras/tests/test_forms.py b/netbox/extras/tests/test_forms.py index 9c22bf83c..5adb57fb2 100644 --- a/netbox/extras/tests/test_forms.py +++ b/netbox/extras/tests/test_forms.py @@ -1,6 +1,7 @@ from django.contrib.contenttypes.models import ContentType from django.test import TestCase +from core.models import ObjectType from dcim.forms import SiteForm from dcim.models import Site from extras.choices import CustomFieldTypeChoices @@ -12,66 +13,66 @@ class CustomFieldModelFormTest(TestCase): @classmethod def setUpTestData(cls): - obj_type = ContentType.objects.get_for_model(Site) + object_type = ObjectType.objects.get_for_model(Site) choice_set = CustomFieldChoiceSet.objects.create( name='Choice Set 1', extra_choices=(('a', 'A'), ('b', 'B'), ('c', 'C')) ) cf_text = CustomField.objects.create(name='text', type=CustomFieldTypeChoices.TYPE_TEXT) - cf_text.content_types.set([obj_type]) + cf_text.object_types.set([object_type]) cf_longtext = CustomField.objects.create(name='longtext', type=CustomFieldTypeChoices.TYPE_LONGTEXT) - cf_longtext.content_types.set([obj_type]) + cf_longtext.object_types.set([object_type]) cf_integer = CustomField.objects.create(name='integer', type=CustomFieldTypeChoices.TYPE_INTEGER) - cf_integer.content_types.set([obj_type]) + cf_integer.object_types.set([object_type]) cf_integer = CustomField.objects.create(name='decimal', type=CustomFieldTypeChoices.TYPE_DECIMAL) - cf_integer.content_types.set([obj_type]) + cf_integer.object_types.set([object_type]) cf_boolean = CustomField.objects.create(name='boolean', type=CustomFieldTypeChoices.TYPE_BOOLEAN) - cf_boolean.content_types.set([obj_type]) + cf_boolean.object_types.set([object_type]) cf_date = CustomField.objects.create(name='date', type=CustomFieldTypeChoices.TYPE_DATE) - cf_date.content_types.set([obj_type]) + cf_date.object_types.set([object_type]) cf_datetime = CustomField.objects.create(name='datetime', type=CustomFieldTypeChoices.TYPE_DATETIME) - cf_datetime.content_types.set([obj_type]) + cf_datetime.object_types.set([object_type]) cf_url = CustomField.objects.create(name='url', type=CustomFieldTypeChoices.TYPE_URL) - cf_url.content_types.set([obj_type]) + cf_url.object_types.set([object_type]) cf_json = CustomField.objects.create(name='json', type=CustomFieldTypeChoices.TYPE_JSON) - cf_json.content_types.set([obj_type]) + cf_json.object_types.set([object_type]) cf_select = CustomField.objects.create( name='select', type=CustomFieldTypeChoices.TYPE_SELECT, choice_set=choice_set ) - cf_select.content_types.set([obj_type]) + cf_select.object_types.set([object_type]) cf_multiselect = CustomField.objects.create( name='multiselect', type=CustomFieldTypeChoices.TYPE_MULTISELECT, choice_set=choice_set ) - cf_multiselect.content_types.set([obj_type]) + cf_multiselect.object_types.set([object_type]) cf_object = CustomField.objects.create( name='object', type=CustomFieldTypeChoices.TYPE_OBJECT, object_type=ContentType.objects.get_for_model(Site) ) - cf_object.content_types.set([obj_type]) + cf_object.object_types.set([object_type]) cf_multiobject = CustomField.objects.create( name='multiobject', type=CustomFieldTypeChoices.TYPE_MULTIOBJECT, object_type=ContentType.objects.get_for_model(Site) ) - cf_multiobject.content_types.set([obj_type]) + cf_multiobject.object_types.set([object_type]) def test_empty_values(self): """ diff --git a/netbox/extras/tests/test_views.py b/netbox/extras/tests/test_views.py index d720560e4..699388c53 100644 --- a/netbox/extras/tests/test_views.py +++ b/netbox/extras/tests/test_views.py @@ -5,6 +5,7 @@ from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType from django.urls import reverse +from core.models import ObjectType from dcim.models import DeviceType, Manufacturer, Site from extras.choices import * from extras.models import * @@ -19,7 +20,7 @@ class CustomFieldTestCase(ViewTestCases.PrimaryObjectViewTestCase): @classmethod def setUpTestData(cls): - site_ct = ContentType.objects.get_for_model(Site) + site_type = ObjectType.objects.get_for_model(Site) CustomFieldChoiceSet.objects.create( name='Choice Set 1', extra_choices=( @@ -36,13 +37,13 @@ class CustomFieldTestCase(ViewTestCases.PrimaryObjectViewTestCase): ) for customfield in custom_fields: customfield.save() - customfield.content_types.add(site_ct) + customfield.object_types.add(site_type) cls.form_data = { 'name': 'field_x', 'label': 'Field X', 'type': 'text', - 'content_types': [site_ct.pk], + 'object_types': [site_type.pk], 'search_weight': 2000, 'filter_logic': CustomFieldFilterLogicChoices.FILTER_EXACT, 'default': None, @@ -53,7 +54,7 @@ class CustomFieldTestCase(ViewTestCases.PrimaryObjectViewTestCase): } cls.csv_data = ( - 'name,label,type,content_types,object_type,weight,search_weight,filter_logic,choice_set,validation_minimum,validation_maximum,validation_regex,ui_visible,ui_editable', + 'name,label,type,object_types,object_type,weight,search_weight,filter_logic,choice_set,validation_minimum,validation_maximum,validation_regex,ui_visible,ui_editable', 'field4,Field 4,text,dcim.site,,100,1000,exact,,,,[a-z]{3},always,yes', 'field5,Field 5,integer,dcim.site,,100,2000,exact,,1,100,,always,yes', 'field6,Field 6,select,dcim.site,,100,3000,exact,Choice Set 1,,,,always,yes', diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 56840f7a9..49c6dcab3 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -46,9 +46,9 @@ class CustomFieldView(generic.ObjectView): def get_extra_context(self, request, instance): related_models = () - for content_type in instance.content_types.all(): + for object_type in instance.object_types.all(): related_models += ( - content_type.model_class().objects.restrict(request.user, 'view').exclude( + object_type.model_class().objects.restrict(request.user, 'view').exclude( Q(**{f'custom_field_data__{instance.name}': ''}) | Q(**{f'custom_field_data__{instance.name}': None}) ), diff --git a/netbox/netbox/api/viewsets/mixins.py b/netbox/netbox/api/viewsets/mixins.py index 9d8286968..7df498ac6 100644 --- a/netbox/netbox/api/viewsets/mixins.py +++ b/netbox/netbox/api/viewsets/mixins.py @@ -5,6 +5,7 @@ from django.http import Http404 from rest_framework import status from rest_framework.response import Response +from core.models import ObjectType from extras.models import ExportTemplate from netbox.api.serializers import BulkOperationSerializer @@ -26,9 +27,9 @@ class CustomFieldsMixin: context = super().get_serializer_context() if hasattr(self.queryset.model, 'custom_fields'): - content_type = ContentType.objects.get_for_model(self.queryset.model) + object_type = ObjectType.objects.get_for_model(self.queryset.model) context.update({ - 'custom_fields': content_type.custom_fields.all(), + 'custom_fields': object_type.custom_fields.all(), }) return context diff --git a/netbox/netbox/filtersets.py b/netbox/netbox/filtersets.py index ebb98d15f..7f07cfbfb 100644 --- a/netbox/netbox/filtersets.py +++ b/netbox/netbox/filtersets.py @@ -281,7 +281,7 @@ class NetBoxModelFilterSet(ChangeLoggedModelFilterSet): # Dynamically add a Filter for each CustomField applicable to the parent model custom_fields = CustomField.objects.filter( - content_types=ContentType.objects.get_for_model(self._meta.model) + object_types=ContentType.objects.get_for_model(self._meta.model) ).exclude( filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED ) diff --git a/netbox/netbox/forms/base.py b/netbox/netbox/forms/base.py index 7e1eaa80c..12de3b12b 100644 --- a/netbox/netbox/forms/base.py +++ b/netbox/netbox/forms/base.py @@ -88,7 +88,7 @@ class NetBoxModelImportForm(CSVModelForm, NetBoxModelForm): def _get_custom_fields(self, content_type): return CustomField.objects.filter( - content_types=content_type, + object_types=content_type, ui_editable=CustomFieldUIEditableChoices.YES ) diff --git a/netbox/netbox/forms/mixins.py b/netbox/netbox/forms/mixins.py index 815f1f6fa..2f903db5d 100644 --- a/netbox/netbox/forms/mixins.py +++ b/netbox/netbox/forms/mixins.py @@ -2,6 +2,7 @@ from django import forms from django.contrib.contenttypes.models import ContentType from django.utils.translation import gettext as _ +from core.models import ObjectType from extras.choices import * from extras.models import * from utilities.forms.fields import DynamicModelMultipleChoiceField @@ -32,16 +33,16 @@ class CustomFieldsMixin: def _get_content_type(self): """ - Return the ContentType of the form's model. + Return the ObjectType of the form's model. """ if not getattr(self, 'model', None): raise NotImplementedError(_("{class_name} must specify a model class.").format( class_name=self.__class__.__name__ )) - return ContentType.objects.get_for_model(self.model) + return ObjectType.objects.get_for_model(self.model) def _get_custom_fields(self, content_type): - return CustomField.objects.filter(content_types=content_type).exclude( + return CustomField.objects.filter(object_types=content_type).exclude( ui_editable=CustomFieldUIEditableChoices.HIDDEN ) diff --git a/netbox/netbox/middleware.py b/netbox/netbox/middleware.py index cb7d2c8ba..c140462ec 100644 --- a/netbox/netbox/middleware.py +++ b/netbox/netbox/middleware.py @@ -70,8 +70,8 @@ class CoreMiddleware: return # Cleanly handle exceptions that occur from REST API requests - if is_api_request(request): - return rest_api_server_error(request) + # if is_api_request(request): + # return rest_api_server_error(request) # Ignore Http404s (defer to Django's built-in 404 handling) if isinstance(exception, Http404): diff --git a/netbox/netbox/search/backends.py b/netbox/netbox/search/backends.py index 1fb23a37c..0d7667087 100644 --- a/netbox/netbox/search/backends.py +++ b/netbox/netbox/search/backends.py @@ -11,6 +11,7 @@ from django.utils.module_loading import import_string import netaddr from netaddr.core import AddrFormatError +from core.models import ObjectType from extras.models import CachedValue, CustomField from netbox.registry import registry from utilities.querysets import RestrictedPrefetch @@ -134,7 +135,7 @@ class CachedValueSearchBackend(SearchBackend): # objects). This must be done before generating the final results list, which returns # a RawQuerySet. content_type_ids = set(queryset.values_list('object_type', flat=True)) - content_types = ContentType.objects.filter(pk__in=content_type_ids) + object_types = ObjectType.objects.filter(pk__in=content_type_ids) # Construct a Prefetch to pre-fetch only those related objects for which the # user has permission to view. @@ -153,7 +154,7 @@ class CachedValueSearchBackend(SearchBackend): # Iterate through each ContentType represented in the search results and prefetch any # related objects necessary to render the prescribed display attributes (display_attrs). - for ct in content_types: + for ct in object_types: model = ct.model_class() indexer = registry['search'].get(content_type_identifier(ct)) if not (display_attrs := getattr(indexer, 'display_attrs', None)): @@ -182,7 +183,7 @@ class CachedValueSearchBackend(SearchBackend): return ret def cache(self, instances, indexer=None, remove_existing=True): - content_type = None + object_type = None custom_fields = None # Convert a single instance to an iterable @@ -204,8 +205,8 @@ class CachedValueSearchBackend(SearchBackend): break # Prefetch any associated custom fields - content_type = ContentType.objects.get_for_model(indexer.model) - custom_fields = CustomField.objects.filter(content_types=content_type).exclude(search_weight=0) + object_type = ObjectType.objects.get_for_model(indexer.model) + custom_fields = CustomField.objects.filter(object_types=object_type).exclude(search_weight=0) # Wipe out any previously cached values for the object if remove_existing: @@ -215,7 +216,7 @@ class CachedValueSearchBackend(SearchBackend): for field in indexer.to_cache(instance, custom_fields=custom_fields): buffer.append( CachedValue( - object_type=content_type, + object_type=object_type, object_id=instance.pk, field=field.name, type=field.type, diff --git a/netbox/netbox/tables/tables.py b/netbox/netbox/tables/tables.py index 495e56991..597d73a16 100644 --- a/netbox/netbox/tables/tables.py +++ b/netbox/netbox/tables/tables.py @@ -3,7 +3,6 @@ from copy import deepcopy import django_tables2 as tables from django.contrib.auth.models import AnonymousUser from django.contrib.contenttypes.fields import GenericForeignKey -from django.contrib.contenttypes.models import ContentType from django.core.exceptions import FieldDoesNotExist from django.db.models.fields.related import RelatedField from django.urls import reverse @@ -12,6 +11,7 @@ from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ from django_tables2.data import TableQuerysetData +from core.models import ObjectType from extras.choices import * from extras.models import CustomField, CustomLink from netbox.registry import registry @@ -201,14 +201,14 @@ class NetBoxTable(BaseTable): ]) # Add custom field & custom link columns - content_type = ContentType.objects.get_for_model(self._meta.model) + object_type = ObjectType.objects.get_for_model(self._meta.model) custom_fields = CustomField.objects.filter( - content_types=content_type + object_types=object_type ).exclude(ui_visible=CustomFieldUIVisibleChoices.HIDDEN) extra_columns.extend([ (f'cf_{cf.name}', columns.CustomFieldColumn(cf)) for cf in custom_fields ]) - custom_links = CustomLink.objects.filter(content_types=content_type, enabled=True) + custom_links = CustomLink.objects.filter(content_types=object_type, enabled=True) extra_columns.extend([ (f'cl_{cl.name}', columns.CustomLinkColumn(cl)) for cl in custom_links ]) diff --git a/netbox/utilities/testing/base.py b/netbox/utilities/testing/base.py index aa2093a9a..f16c2fbbd 100644 --- a/netbox/utilities/testing/base.py +++ b/netbox/utilities/testing/base.py @@ -10,6 +10,7 @@ from django.test import Client, TestCase as _TestCase from netaddr import IPNetwork from taggit.managers import TaggableManager +from core.models import ObjectType from users.models import ObjectPermission from utilities.permissions import resolve_permission_ct from utilities.utils import content_type_identifier @@ -112,7 +113,7 @@ class ModelTestCase(TestCase): # Handle ManyToManyFields if value and type(field) in (ManyToManyField, TaggableManager): - if field.related_model is ContentType and api: + if field.related_model in (ContentType, ObjectType) and api: model_dict[key] = sorted([content_type_identifier(ct) for ct in value]) else: model_dict[key] = sorted([obj.pk for obj in value]) @@ -120,8 +121,8 @@ class ModelTestCase(TestCase): elif api: # Replace ContentType numeric IDs with . - if type(getattr(instance, key)) is ContentType: - ct = ContentType.objects.get(pk=value) + if type(getattr(instance, key)) in (ContentType, ObjectType): + ct = ObjectType.objects.get(pk=value) model_dict[key] = content_type_identifier(ct) # Convert IPNetwork instances to strings diff --git a/netbox/utilities/tests/test_api.py b/netbox/utilities/tests/test_api.py index 1cc3487b1..81be70a34 100644 --- a/netbox/utilities/tests/test_api.py +++ b/netbox/utilities/tests/test_api.py @@ -1,10 +1,8 @@ -import urllib.parse - -from django.contrib.contenttypes.models import ContentType from django.test import Client, TestCase, override_settings from django.urls import reverse from rest_framework import status +from core.models import ObjectType from dcim.models import Region, Site from extras.choices import CustomFieldTypeChoices from extras.models import CustomField @@ -240,10 +238,10 @@ class APIDocsTestCase(TestCase): self.client = Client() # Populate a CustomField to activate CustomFieldSerializer - content_type = ContentType.objects.get_for_model(Site) + object_type = ObjectType.objects.get_for_model(Site) self.cf_text = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='test') self.cf_text.save() - self.cf_text.content_types.set([content_type]) + self.cf_text.object_types.set([object_type]) self.cf_text.save() def test_api_docs(self): From 54b9d1b3f235b8212c73717173e1e7aa7e638b6e Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 1 Mar 2024 14:37:32 -0500 Subject: [PATCH 12/47] Disconnect search backend during test to avoid discrepancy with ContentTypes on transaction rollback --- netbox/netbox/tests/test_staging.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/netbox/netbox/tests/test_staging.py b/netbox/netbox/tests/test_staging.py index ed3a69f10..0a73b2987 100644 --- a/netbox/netbox/tests/test_staging.py +++ b/netbox/netbox/tests/test_staging.py @@ -1,9 +1,11 @@ +from django.db.models.signals import post_save from django.test import TransactionTestCase from circuits.models import Provider, Circuit, CircuitType from extras.choices import ChangeActionChoices from extras.models import Branch, StagedChange, Tag from ipam.models import ASN, RIR +from netbox.search.backends import search_backend from netbox.staging import checkout from utilities.testing import create_tags @@ -11,6 +13,10 @@ from utilities.testing import create_tags class StagingTestCase(TransactionTestCase): def setUp(self): + # Disconnect search backend to avoid issues with cached ObjectTypes being deleted + # from the database upon transaction rollback + post_save.disconnect(search_backend.caching_handler) + create_tags('Alpha', 'Bravo', 'Charlie') rir = RIR.objects.create(name='RIR 1', slug='rir-1') From ba514aceac778122049998c113e912d83b359d9b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 1 Mar 2024 15:04:02 -0500 Subject: [PATCH 13/47] Rename CustomLink.content_types to object_types & use ObjectType proxy --- netbox/extras/api/serializers.py | 4 ++-- netbox/extras/filtersets.py | 8 ++++---- netbox/extras/forms/bulk_import.py | 6 +++--- netbox/extras/forms/filtersets.py | 6 +++--- netbox/extras/forms/model_forms.py | 6 +++--- netbox/extras/graphql/types.py | 2 +- .../migrations/0111_rename_content_types.py | 13 +++++++++++++ netbox/extras/models/models.py | 4 ++-- netbox/extras/tables/tables.py | 16 ++++++++-------- netbox/extras/templatetags/custom_links.py | 6 +++--- netbox/extras/tests/test_api.py | 10 +++++----- netbox/extras/tests/test_filtersets.py | 10 +++++----- netbox/extras/tests/test_views.py | 10 +++++----- netbox/netbox/tables/tables.py | 2 +- netbox/templates/extras/customfield.html | 2 +- netbox/templates/extras/customlink.html | 2 +- 16 files changed, 60 insertions(+), 47 deletions(-) diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index 370450712..fe66e2035 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -196,7 +196,7 @@ class CustomFieldChoiceSetSerializer(ValidatedModelSerializer): class CustomLinkSerializer(ValidatedModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='extras-api:customlink-detail') - content_types = ContentTypeField( + object_types = ContentTypeField( queryset=ObjectType.objects.with_feature('custom_links'), many=True ) @@ -204,7 +204,7 @@ class CustomLinkSerializer(ValidatedModelSerializer): class Meta: model = CustomLink fields = [ - 'id', 'url', 'display', 'content_types', 'name', 'enabled', 'link_text', 'link_url', 'weight', 'group_name', + 'id', 'url', 'display', 'object_types', 'name', 'enabled', 'link_text', 'link_url', 'weight', 'group_name', 'button_class', 'new_window', 'created', 'last_updated', ] brief_fields = ('id', 'url', 'display', 'name') diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py index 290670f5c..dd378075a 100644 --- a/netbox/extras/filtersets.py +++ b/netbox/extras/filtersets.py @@ -190,15 +190,15 @@ class CustomLinkFilterSet(BaseFilterSet): method='search', label=_('Search'), ) - content_type_id = MultiValueNumberFilter( - field_name='content_types__id' + object_types_id = MultiValueNumberFilter( + field_name='object_types__id' ) - content_types = ContentTypeFilter() + object_types = ContentTypeFilter() class Meta: model = CustomLink fields = [ - 'id', 'content_types', 'name', 'enabled', 'link_text', 'link_url', 'weight', 'group_name', 'new_window', + 'id', 'object_types', 'name', 'enabled', 'link_text', 'link_url', 'weight', 'group_name', 'new_window', ] def search(self, queryset, name, value): diff --git a/netbox/extras/forms/bulk_import.py b/netbox/extras/forms/bulk_import.py index 1d1b27617..abe35e30c 100644 --- a/netbox/extras/forms/bulk_import.py +++ b/netbox/extras/forms/bulk_import.py @@ -111,8 +111,8 @@ class CustomFieldChoiceSetImportForm(CSVModelForm): class CustomLinkImportForm(CSVModelForm): - content_types = CSVMultipleContentTypeField( - label=_('Content types'), + object_types = CSVMultipleContentTypeField( + label=_('Object types'), queryset=ObjectType.objects.with_feature('custom_links'), help_text=_("One or more assigned object types") ) @@ -120,7 +120,7 @@ class CustomLinkImportForm(CSVModelForm): class Meta: model = CustomLink fields = ( - 'name', 'content_types', 'enabled', 'weight', 'group_name', 'button_class', 'new_window', 'link_text', + 'name', 'object_types', 'enabled', 'weight', 'group_name', 'button_class', 'new_window', 'link_text', 'link_url', ) diff --git a/netbox/extras/forms/filtersets.py b/netbox/extras/forms/filtersets.py index ccfeb8c1d..208f28725 100644 --- a/netbox/extras/forms/filtersets.py +++ b/netbox/extras/forms/filtersets.py @@ -108,10 +108,10 @@ class CustomFieldChoiceSetFilterForm(SavedFiltersMixin, FilterForm): class CustomLinkFilterForm(SavedFiltersMixin, FilterForm): fieldsets = ( (None, ('q', 'filter_id')), - (_('Attributes'), ('content_types', 'enabled', 'new_window', 'weight')), + (_('Attributes'), ('object_types', 'enabled', 'new_window', 'weight')), ) - content_types = ContentTypeMultipleChoiceField( - label=_('Content types'), + object_types = ContentTypeMultipleChoiceField( + label=_('Object types'), queryset=ObjectType.objects.with_feature('custom_links'), required=False ) diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py index 776265878..70f533013 100644 --- a/netbox/extras/forms/model_forms.py +++ b/netbox/extras/forms/model_forms.py @@ -122,13 +122,13 @@ class CustomFieldChoiceSetForm(forms.ModelForm): class CustomLinkForm(forms.ModelForm): - content_types = ContentTypeMultipleChoiceField( - label=_('Content types'), + object_types = ContentTypeMultipleChoiceField( + label=_('Object types'), queryset=ObjectType.objects.with_feature('custom_links') ) fieldsets = ( - (_('Custom Link'), ('name', 'content_types', 'weight', 'group_name', 'button_class', 'enabled', 'new_window')), + (_('Custom Link'), ('name', 'object_types', 'weight', 'group_name', 'button_class', 'enabled', 'new_window')), (_('Templates'), ('link_text', 'link_url')), ) diff --git a/netbox/extras/graphql/types.py b/netbox/extras/graphql/types.py index 643568eaa..e4cfd4be7 100644 --- a/netbox/extras/graphql/types.py +++ b/netbox/extras/graphql/types.py @@ -55,7 +55,7 @@ class CustomLinkType(ObjectType): class Meta: model = models.CustomLink - exclude = ('content_types', ) + exclude = ('object_types', ) filterset_class = filtersets.CustomLinkFilterSet diff --git a/netbox/extras/migrations/0111_rename_content_types.py b/netbox/extras/migrations/0111_rename_content_types.py index 2b7b2358a..dc3ec9fe6 100644 --- a/netbox/extras/migrations/0111_rename_content_types.py +++ b/netbox/extras/migrations/0111_rename_content_types.py @@ -10,6 +10,7 @@ class Migration(migrations.Migration): ] operations = [ + # Custom fields migrations.RenameField( model_name='customfield', old_name='content_types', @@ -25,4 +26,16 @@ class Migration(migrations.Migration): name='object_type', field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='core.objecttype'), ), + + # Custom links + migrations.RenameField( + model_name='customlink', + old_name='content_types', + new_name='object_types', + ), + migrations.AlterField( + model_name='customlink', + name='object_types', + field=models.ManyToManyField(related_name='custom_links', to='core.objecttype'), + ), ] diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index 6b5a7c150..7439546fb 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -313,8 +313,8 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): A custom link to an external representation of a NetBox object. The link text and URL fields accept Jinja2 template code to be rendered with an object as context. """ - content_types = models.ManyToManyField( - to='contenttypes.ContentType', + object_types = models.ManyToManyField( + to='core.ObjectType', related_name='custom_links', help_text=_('The object type(s) to which this link applies.') ) diff --git a/netbox/extras/tables/tables.py b/netbox/extras/tables/tables.py index 8482c5e24..7adbae178 100644 --- a/netbox/extras/tables/tables.py +++ b/netbox/extras/tables/tables.py @@ -40,8 +40,8 @@ class CustomFieldTable(NetBoxTable): verbose_name=_('Name'), linkify=True ) - content_types = columns.ContentTypesColumn( - verbose_name=_('Content Types') + object_types = columns.ContentTypesColumn( + verbose_name=_('Object Types') ) required = columns.BooleanColumn( verbose_name=_('Required') @@ -71,11 +71,11 @@ class CustomFieldTable(NetBoxTable): class Meta(NetBoxTable.Meta): model = CustomField fields = ( - 'pk', 'id', 'name', 'content_types', 'label', 'type', 'group_name', 'required', 'default', 'description', + 'pk', 'id', 'name', 'object_types', 'label', 'type', 'group_name', 'required', 'default', 'description', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'is_cloneable', 'weight', 'choice_set', 'choices', 'created', 'last_updated', ) - default_columns = ('pk', 'name', 'content_types', 'label', 'group_name', 'type', 'required', 'description') + default_columns = ('pk', 'name', 'object_types', 'label', 'group_name', 'type', 'required', 'description') class CustomFieldChoiceSetTable(NetBoxTable): @@ -115,8 +115,8 @@ class CustomLinkTable(NetBoxTable): verbose_name=_('Name'), linkify=True ) - content_types = columns.ContentTypesColumn( - verbose_name=_('Content Types'), + object_types = columns.ContentTypesColumn( + verbose_name=_('Object Types'), ) enabled = columns.BooleanColumn( verbose_name=_('Enabled'), @@ -128,10 +128,10 @@ class CustomLinkTable(NetBoxTable): class Meta(NetBoxTable.Meta): model = CustomLink fields = ( - 'pk', 'id', 'name', 'content_types', 'enabled', 'link_text', 'link_url', 'weight', 'group_name', + 'pk', 'id', 'name', 'object_types', 'enabled', 'link_text', 'link_url', 'weight', 'group_name', 'button_class', 'new_window', 'created', 'last_updated', ) - default_columns = ('pk', 'name', 'content_types', 'enabled', 'group_name', 'button_class', 'new_window') + default_columns = ('pk', 'name', 'object_types', 'enabled', 'group_name', 'button_class', 'new_window') class ExportTemplateTable(NetBoxTable): diff --git a/netbox/extras/templatetags/custom_links.py b/netbox/extras/templatetags/custom_links.py index 5de95b607..31cd22815 100644 --- a/netbox/extras/templatetags/custom_links.py +++ b/netbox/extras/templatetags/custom_links.py @@ -1,7 +1,7 @@ from django import template -from django.contrib.contenttypes.models import ContentType from django.utils.safestring import mark_safe +from core.models import ObjectType from extras.models import CustomLink @@ -32,8 +32,8 @@ def custom_links(context, obj): """ Render all applicable links for the given object. """ - content_type = ContentType.objects.get_for_model(obj) - custom_links = CustomLink.objects.filter(content_types=content_type, enabled=True) + object_type = ObjectType.objects.get_for_model(obj) + custom_links = CustomLink.objects.filter(object_types=object_type, enabled=True) if not custom_links: return '' diff --git a/netbox/extras/tests/test_api.py b/netbox/extras/tests/test_api.py index 01bbb312f..ae592b528 100644 --- a/netbox/extras/tests/test_api.py +++ b/netbox/extras/tests/test_api.py @@ -273,21 +273,21 @@ class CustomLinkTest(APIViewTestCases.APIViewTestCase): brief_fields = ['display', 'id', 'name', 'url'] create_data = [ { - 'content_types': ['dcim.site'], + 'object_types': ['dcim.site'], 'name': 'Custom Link 4', 'enabled': True, 'link_text': 'Link 4', 'link_url': 'http://example.com/?4', }, { - 'content_types': ['dcim.site'], + 'object_types': ['dcim.site'], 'name': 'Custom Link 5', 'enabled': True, 'link_text': 'Link 5', 'link_url': 'http://example.com/?5', }, { - 'content_types': ['dcim.site'], + 'object_types': ['dcim.site'], 'name': 'Custom Link 6', 'enabled': False, 'link_text': 'Link 6', @@ -301,7 +301,7 @@ class CustomLinkTest(APIViewTestCases.APIViewTestCase): @classmethod def setUpTestData(cls): - site_ct = ContentType.objects.get_for_model(Site) + site_type = ObjectType.objects.get_for_model(Site) custom_links = ( CustomLink( @@ -325,7 +325,7 @@ class CustomLinkTest(APIViewTestCases.APIViewTestCase): ) CustomLink.objects.bulk_create(custom_links) for i, custom_link in enumerate(custom_links): - custom_link.content_types.set([site_ct]) + custom_link.object_types.set([site_type]) class SavedFilterTest(APIViewTestCases.APIViewTestCase): diff --git a/netbox/extras/tests/test_filtersets.py b/netbox/extras/tests/test_filtersets.py index 25263fe68..a23e11288 100644 --- a/netbox/extras/tests/test_filtersets.py +++ b/netbox/extras/tests/test_filtersets.py @@ -397,7 +397,7 @@ class CustomLinkTestCase(TestCase, BaseFilterSetTests): @classmethod def setUpTestData(cls): - content_types = ContentType.objects.filter(model__in=['site', 'rack', 'device']) + object_types = ObjectType.objects.filter(model__in=['site', 'rack', 'device']) custom_links = ( CustomLink( @@ -427,7 +427,7 @@ class CustomLinkTestCase(TestCase, BaseFilterSetTests): ) CustomLink.objects.bulk_create(custom_links) for i, custom_link in enumerate(custom_links): - custom_link.content_types.set([content_types[i]]) + custom_link.object_types.set([object_types[i]]) def test_q(self): params = {'q': 'Custom Link 1'} @@ -437,10 +437,10 @@ class CustomLinkTestCase(TestCase, BaseFilterSetTests): params = {'name': ['Custom Link 1', 'Custom Link 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - def test_content_types(self): - params = {'content_types': 'dcim.site'} + def test_object_types(self): + params = {'object_types': 'dcim.site'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) - params = {'content_type_id': [ContentType.objects.get_for_model(Site).pk]} + params = {'object_types_id': [ContentType.objects.get_for_model(Site).pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) def test_weight(self): diff --git a/netbox/extras/tests/test_views.py b/netbox/extras/tests/test_views.py index 699388c53..890cd59de 100644 --- a/netbox/extras/tests/test_views.py +++ b/netbox/extras/tests/test_views.py @@ -138,7 +138,7 @@ class CustomLinkTestCase(ViewTestCases.PrimaryObjectViewTestCase): @classmethod def setUpTestData(cls): - site_ct = ContentType.objects.get_for_model(Site) + site_type = ObjectType.objects.get_for_model(Site) custom_links = ( CustomLink(name='Custom Link 1', enabled=True, link_text='Link 1', link_url='http://example.com/?1'), CustomLink(name='Custom Link 2', enabled=True, link_text='Link 2', link_url='http://example.com/?2'), @@ -146,11 +146,11 @@ class CustomLinkTestCase(ViewTestCases.PrimaryObjectViewTestCase): ) CustomLink.objects.bulk_create(custom_links) for i, custom_link in enumerate(custom_links): - custom_link.content_types.set([site_ct]) + custom_link.object_types.set([site_type]) cls.form_data = { 'name': 'Custom Link X', - 'content_types': [site_ct.pk], + 'object_types': [site_type.pk], 'enabled': False, 'weight': 100, 'button_class': CustomLinkButtonClassChoices.DEFAULT, @@ -159,7 +159,7 @@ class CustomLinkTestCase(ViewTestCases.PrimaryObjectViewTestCase): } cls.csv_data = ( - "name,content_types,enabled,weight,button_class,link_text,link_url", + "name,object_types,enabled,weight,button_class,link_text,link_url", "Custom Link 4,dcim.site,True,100,blue,Link 4,http://exmaple.com/?4", "Custom Link 5,dcim.site,True,100,blue,Link 5,http://exmaple.com/?5", "Custom Link 6,dcim.site,False,100,blue,Link 6,http://exmaple.com/?6", @@ -652,7 +652,7 @@ class CustomLinkTest(TestCase): new_window=False ) customlink.save() - customlink.content_types.set([ContentType.objects.get_for_model(Site)]) + customlink.object_types.set([ObjectType.objects.get_for_model(Site)]) site = Site(name='Test Site', slug='test-site') site.save() diff --git a/netbox/netbox/tables/tables.py b/netbox/netbox/tables/tables.py index 597d73a16..afef74752 100644 --- a/netbox/netbox/tables/tables.py +++ b/netbox/netbox/tables/tables.py @@ -208,7 +208,7 @@ class NetBoxTable(BaseTable): extra_columns.extend([ (f'cf_{cf.name}', columns.CustomFieldColumn(cf)) for cf in custom_fields ]) - custom_links = CustomLink.objects.filter(content_types=object_type, enabled=True) + custom_links = CustomLink.objects.filter(object_types=object_type, enabled=True) extra_columns.extend([ (f'cl_{cl.name}', columns.CustomLinkColumn(cl)) for cl in custom_links ]) diff --git a/netbox/templates/extras/customfield.html b/netbox/templates/extras/customfield.html index ca6988152..ddc6b30f4 100644 --- a/netbox/templates/extras/customfield.html +++ b/netbox/templates/extras/customfield.html @@ -89,7 +89,7 @@
{% trans "Object Types" %}
- {% for ct in object.content_types.all %} + {% for ct in object.object_types.all %} diff --git a/netbox/templates/extras/customlink.html b/netbox/templates/extras/customlink.html index 492408396..0b9b068da 100644 --- a/netbox/templates/extras/customlink.html +++ b/netbox/templates/extras/customlink.html @@ -38,7 +38,7 @@
{% trans "Assigned Models" %}
{{ ct }}
- {% for ct in object.content_types.all %} + {% for ct in object.object_types.all %} From e51d71d7e6dca05fc5d10eca4c59366263e13179 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 1 Mar 2024 15:31:03 -0500 Subject: [PATCH 14/47] Rename EventRule.content_types to object_types & use ObjectType proxy --- netbox/extras/api/serializers.py | 4 ++-- netbox/extras/events.py | 2 +- netbox/extras/filtersets.py | 6 ++--- netbox/extras/forms/bulk_import.py | 6 ++--- netbox/extras/forms/filtersets.py | 4 ++-- netbox/extras/forms/model_forms.py | 8 +++---- netbox/extras/graphql/types.py | 16 ++++++------- .../migrations/0111_rename_content_types.py | 12 ++++++++++ netbox/extras/models/models.py | 6 ++--- netbox/extras/tables/tables.py | 6 ++--- netbox/extras/tests/test_api.py | 6 ++--- netbox/extras/tests/test_event_rules.py | 23 ++++++++++--------- netbox/extras/tests/test_filtersets.py | 18 +++++++-------- netbox/extras/tests/test_views.py | 8 +++---- netbox/templates/extras/eventrule.html | 4 ++-- 15 files changed, 71 insertions(+), 58 deletions(-) diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index fe66e2035..32b708fcc 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -59,7 +59,7 @@ __all__ = ( class EventRuleSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='extras-api:eventrule-detail') - content_types = ContentTypeField( + object_types = ContentTypeField( queryset=ObjectType.objects.with_feature('event_rules'), many=True ) @@ -72,7 +72,7 @@ class EventRuleSerializer(NetBoxModelSerializer): class Meta: model = EventRule fields = [ - 'id', 'url', 'display', 'content_types', 'name', 'type_create', 'type_update', 'type_delete', + 'id', 'url', 'display', 'object_types', 'name', 'type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end', 'enabled', 'conditions', 'action_type', 'action_object_type', 'action_object_id', 'action_object', 'description', 'custom_fields', 'tags', 'created', 'last_updated', ] diff --git a/netbox/extras/events.py b/netbox/extras/events.py index e7706ea9f..0ee4cffa8 100644 --- a/netbox/extras/events.py +++ b/netbox/extras/events.py @@ -155,7 +155,7 @@ def process_event_queue(events): if content_type not in events_cache[action_flag]: events_cache[action_flag][content_type] = EventRule.objects.filter( **{action_flag: True}, - content_types=content_type, + object_types=content_type, enabled=True ) event_rules = events_cache[action_flag][content_type] diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py index dd378075a..d03d134be 100644 --- a/netbox/extras/filtersets.py +++ b/netbox/extras/filtersets.py @@ -89,10 +89,10 @@ class EventRuleFilterSet(NetBoxModelFilterSet): method='search', label=_('Search'), ) - content_type_id = MultiValueNumberFilter( - field_name='content_types__id' + object_types_id = MultiValueNumberFilter( + field_name='object_types__id' ) - content_types = ContentTypeFilter() + object_types = ContentTypeFilter() action_type = django_filters.MultipleChoiceFilter( choices=EventRuleActionChoices ) diff --git a/netbox/extras/forms/bulk_import.py b/netbox/extras/forms/bulk_import.py index abe35e30c..cbb7b4e44 100644 --- a/netbox/extras/forms/bulk_import.py +++ b/netbox/extras/forms/bulk_import.py @@ -173,8 +173,8 @@ class WebhookImportForm(NetBoxModelImportForm): class EventRuleImportForm(NetBoxModelImportForm): - content_types = CSVMultipleContentTypeField( - label=_('Content types'), + object_types = CSVMultipleContentTypeField( + label=_('Object types'), queryset=ObjectType.objects.with_feature('event_rules'), help_text=_("One or more assigned object types") ) @@ -187,7 +187,7 @@ class EventRuleImportForm(NetBoxModelImportForm): class Meta: model = EventRule fields = ( - 'name', 'description', 'enabled', 'conditions', 'content_types', 'type_create', 'type_update', + 'name', 'description', 'enabled', 'conditions', 'object_types', 'type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end', 'action_type', 'action_object', 'comments', 'tags' ) diff --git a/netbox/extras/forms/filtersets.py b/netbox/extras/forms/filtersets.py index 208f28725..f19695f57 100644 --- a/netbox/extras/forms/filtersets.py +++ b/netbox/extras/forms/filtersets.py @@ -250,10 +250,10 @@ class EventRuleFilterForm(NetBoxModelFilterSetForm): fieldsets = ( (None, ('q', 'filter_id', 'tag')), - (_('Attributes'), ('content_type_id', 'action_type', 'enabled')), + (_('Attributes'), ('object_types_id', 'action_type', 'enabled')), (_('Events'), ('type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end')), ) - content_type_id = ContentTypeMultipleChoiceField( + object_types_id = ContentTypeMultipleChoiceField( queryset=ObjectType.objects.with_feature('event_rules'), required=False, label=_('Object type') diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py index 70f533013..9423e35e2 100644 --- a/netbox/extras/forms/model_forms.py +++ b/netbox/extras/forms/model_forms.py @@ -248,8 +248,8 @@ class WebhookForm(NetBoxModelForm): class EventRuleForm(NetBoxModelForm): - content_types = ContentTypeMultipleChoiceField( - label=_('Content types'), + object_types = ContentTypeMultipleChoiceField( + label=_('Object types'), queryset=ObjectType.objects.with_feature('event_rules'), ) action_choice = forms.ChoiceField( @@ -266,7 +266,7 @@ class EventRuleForm(NetBoxModelForm): ) fieldsets = ( - (_('Event Rule'), ('name', 'description', 'content_types', 'enabled', 'tags')), + (_('Event Rule'), ('name', 'description', 'object_types', 'enabled', 'tags')), (_('Events'), ('type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end')), (_('Conditions'), ('conditions',)), (_('Action'), ( @@ -277,7 +277,7 @@ class EventRuleForm(NetBoxModelForm): class Meta: model = EventRule fields = ( - 'content_types', 'name', 'description', 'type_create', 'type_update', 'type_delete', 'type_job_start', + 'object_types', 'name', 'description', 'type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end', 'enabled', 'conditions', 'action_type', 'action_object_type', 'action_object_id', 'action_data', 'comments', 'tags' ) diff --git a/netbox/extras/graphql/types.py b/netbox/extras/graphql/types.py index e4cfd4be7..15ef76b6b 100644 --- a/netbox/extras/graphql/types.py +++ b/netbox/extras/graphql/types.py @@ -59,6 +59,14 @@ class CustomLinkType(ObjectType): filterset_class = filtersets.CustomLinkFilterSet +class EventRuleType(OrganizationalObjectType): + + class Meta: + model = models.EventRule + exclude = ('object_types',) + filterset_class = filtersets.EventRuleFilterSet + + class ExportTemplateType(ObjectType): class Meta: @@ -112,11 +120,3 @@ class WebhookType(OrganizationalObjectType): class Meta: model = models.Webhook filterset_class = filtersets.WebhookFilterSet - - -class EventRuleType(OrganizationalObjectType): - - class Meta: - model = models.EventRule - exclude = ('content_types', ) - filterset_class = filtersets.EventRuleFilterSet diff --git a/netbox/extras/migrations/0111_rename_content_types.py b/netbox/extras/migrations/0111_rename_content_types.py index dc3ec9fe6..f44b01e34 100644 --- a/netbox/extras/migrations/0111_rename_content_types.py +++ b/netbox/extras/migrations/0111_rename_content_types.py @@ -38,4 +38,16 @@ class Migration(migrations.Migration): name='object_types', field=models.ManyToManyField(related_name='custom_links', to='core.objecttype'), ), + + # Event rules + migrations.RenameField( + model_name='eventrule', + old_name='content_types', + new_name='object_types', + ), + migrations.AlterField( + model_name='eventrule', + name='object_types', + field=models.ManyToManyField(related_name='event_rules', to='core.objecttype'), + ), ] diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index 7439546fb..bfc11b7c5 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -43,9 +43,9 @@ class EventRule(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLogged specific type of object is created, modified, or deleted. The action to be taken might entail transmitting a webhook or executing a custom script. """ - content_types = models.ManyToManyField( - to='contenttypes.ContentType', - related_name='eventrules', + object_types = models.ManyToManyField( + to='core.ObjectType', + related_name='event_rules', verbose_name=_('object types'), help_text=_("The object(s) to which this rule applies.") ) diff --git a/netbox/extras/tables/tables.py b/netbox/extras/tables/tables.py index 7adbae178..7775916b9 100644 --- a/netbox/extras/tables/tables.py +++ b/netbox/extras/tables/tables.py @@ -281,8 +281,8 @@ class EventRuleTable(NetBoxTable): linkify=True, verbose_name=_('Object'), ) - content_types = columns.ContentTypesColumn( - verbose_name=_('Content Types'), + object_types = columns.ContentTypesColumn( + verbose_name=_('Object Types'), ) enabled = columns.BooleanColumn( verbose_name=_('Enabled'), @@ -309,7 +309,7 @@ class EventRuleTable(NetBoxTable): class Meta(NetBoxTable.Meta): model = EventRule fields = ( - 'pk', 'id', 'name', 'enabled', 'description', 'action_type', 'action_object', 'content_types', + 'pk', 'id', 'name', 'enabled', 'description', 'action_type', 'action_object', 'object_types', 'type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end', 'tags', 'created', 'last_updated', ) diff --git a/netbox/extras/tests/test_api.py b/netbox/extras/tests/test_api.py index ae592b528..7bed486a0 100644 --- a/netbox/extras/tests/test_api.py +++ b/netbox/extras/tests/test_api.py @@ -122,7 +122,7 @@ class EventRuleTest(APIViewTestCases.APIViewTestCase): cls.create_data = [ { 'name': 'EventRule 4', - 'content_types': ['dcim.device', 'dcim.devicetype'], + 'object_types': ['dcim.device', 'dcim.devicetype'], 'type_create': True, 'action_type': EventRuleActionChoices.WEBHOOK, 'action_object_type': 'extras.webhook', @@ -130,7 +130,7 @@ class EventRuleTest(APIViewTestCases.APIViewTestCase): }, { 'name': 'EventRule 5', - 'content_types': ['dcim.device', 'dcim.devicetype'], + 'object_types': ['dcim.device', 'dcim.devicetype'], 'type_create': True, 'action_type': EventRuleActionChoices.WEBHOOK, 'action_object_type': 'extras.webhook', @@ -138,7 +138,7 @@ class EventRuleTest(APIViewTestCases.APIViewTestCase): }, { 'name': 'EventRule 6', - 'content_types': ['dcim.device', 'dcim.devicetype'], + 'object_types': ['dcim.device', 'dcim.devicetype'], 'type_create': True, 'action_type': EventRuleActionChoices.WEBHOOK, 'action_object_type': 'extras.webhook', diff --git a/netbox/extras/tests/test_event_rules.py b/netbox/extras/tests/test_event_rules.py index 549c33478..8cea2078a 100644 --- a/netbox/extras/tests/test_event_rules.py +++ b/netbox/extras/tests/test_event_rules.py @@ -3,17 +3,18 @@ import uuid from unittest.mock import patch import django_rq -from dcim.choices import SiteStatusChoices -from dcim.models import Site -from django.contrib.contenttypes.models import ContentType from django.http import HttpResponse from django.urls import reverse +from requests import Session +from rest_framework import status + +from core.models import ObjectType +from dcim.choices import SiteStatusChoices +from dcim.models import Site from extras.choices import EventRuleActionChoices, ObjectChangeActionChoices from extras.events import enqueue_object, flush_events, serialize_for_event from extras.models import EventRule, Tag, Webhook from extras.webhooks import generate_signature, send_webhook -from requests import Session -from rest_framework import status from utilities.testing import APITestCase @@ -29,7 +30,7 @@ class EventRuleTest(APITestCase): @classmethod def setUpTestData(cls): - site_ct = ContentType.objects.get_for_model(Site) + site_type = ObjectType.objects.get_for_model(Site) DUMMY_URL = 'http://localhost:9000/' DUMMY_SECRET = 'LOOKATMEIMASECRETSTRING' @@ -39,32 +40,32 @@ class EventRuleTest(APITestCase): Webhook(name='Webhook 3', payload_url=DUMMY_URL, secret=DUMMY_SECRET), )) - ct = ContentType.objects.get(app_label='extras', model='webhook') + webhook_type = ObjectType.objects.get(app_label='extras', model='webhook') event_rules = EventRule.objects.bulk_create(( EventRule( name='Webhook Event 1', type_create=True, action_type=EventRuleActionChoices.WEBHOOK, - action_object_type=ct, + action_object_type=webhook_type, action_object_id=webhooks[0].id ), EventRule( name='Webhook Event 2', type_update=True, action_type=EventRuleActionChoices.WEBHOOK, - action_object_type=ct, + action_object_type=webhook_type, action_object_id=webhooks[0].id ), EventRule( name='Webhook Event 3', type_delete=True, action_type=EventRuleActionChoices.WEBHOOK, - action_object_type=ct, + action_object_type=webhook_type, action_object_id=webhooks[0].id ), )) for event_rule in event_rules: - event_rule.content_types.set([site_ct]) + event_rule.object_types.set([site_type]) Tag.objects.bulk_create(( Tag(name='Foo', slug='foo'), diff --git a/netbox/extras/tests/test_filtersets.py b/netbox/extras/tests/test_filtersets.py index a23e11288..43ee605fb 100644 --- a/netbox/extras/tests/test_filtersets.py +++ b/netbox/extras/tests/test_filtersets.py @@ -241,7 +241,7 @@ class EventRuleTestCase(TestCase, BaseFilterSetTests): @classmethod def setUpTestData(cls): - content_types = ContentType.objects.filter( + object_types = ObjectType.objects.filter( model__in=['region', 'site', 'rack', 'location', 'device'] ) @@ -334,11 +334,11 @@ class EventRuleTestCase(TestCase, BaseFilterSetTests): ), ) EventRule.objects.bulk_create(event_rules) - event_rules[0].content_types.add(content_types[0]) - event_rules[1].content_types.add(content_types[1]) - event_rules[2].content_types.add(content_types[2]) - event_rules[3].content_types.add(content_types[3]) - event_rules[4].content_types.add(content_types[4]) + event_rules[0].object_types.add(object_types[0]) + event_rules[1].object_types.add(object_types[1]) + event_rules[2].object_types.add(object_types[2]) + event_rules[3].object_types.add(object_types[3]) + event_rules[4].object_types.add(object_types[4]) def test_q(self): params = {'q': 'foobar1'} @@ -352,10 +352,10 @@ class EventRuleTestCase(TestCase, BaseFilterSetTests): params = {'description': ['foobar1', 'foobar2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - def test_content_types(self): - params = {'content_types': 'dcim.region'} + def test_object_types(self): + params = {'object_types': 'dcim.region'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) - params = {'content_type_id': [ContentType.objects.get_for_model(Region).pk]} + params = {'object_types_id': [ContentType.objects.get_for_model(Region).pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) def test_action_type(self): diff --git a/netbox/extras/tests/test_views.py b/netbox/extras/tests/test_views.py index 890cd59de..ba7cd0818 100644 --- a/netbox/extras/tests/test_views.py +++ b/netbox/extras/tests/test_views.py @@ -397,7 +397,7 @@ class EventRulesTestCase(ViewTestCases.PrimaryObjectViewTestCase): for webhook in webhooks: webhook.save() - site_ct = ContentType.objects.get_for_model(Site) + site_type = ObjectType.objects.get_for_model(Site) event_rules = ( EventRule(name='EventRule 1', type_create=True, action_object=webhooks[0]), EventRule(name='EventRule 2', type_create=True, action_object=webhooks[1]), @@ -405,12 +405,12 @@ class EventRulesTestCase(ViewTestCases.PrimaryObjectViewTestCase): ) for event in event_rules: event.save() - event.content_types.add(site_ct) + event.object_types.add(site_type) webhook_ct = ContentType.objects.get_for_model(Webhook) cls.form_data = { 'name': 'Event X', - 'content_types': [site_ct.pk], + 'object_types': [site_type.pk], 'type_create': False, 'type_update': True, 'type_delete': True, @@ -423,7 +423,7 @@ class EventRulesTestCase(ViewTestCases.PrimaryObjectViewTestCase): } cls.csv_data = ( - "name,content_types,type_create,action_type,action_object", + "name,object_types,type_create,action_type,action_object", "Webhook 4,dcim.site,True,webhook,Webhook 1", ) diff --git a/netbox/templates/extras/eventrule.html b/netbox/templates/extras/eventrule.html index d3c483819..844fbf9c6 100644 --- a/netbox/templates/extras/eventrule.html +++ b/netbox/templates/extras/eventrule.html @@ -26,9 +26,9 @@
{% trans "Object Types" %}
{{ ct }}
- {% for ct in object.content_types.all %} + {% for object_type in object.object_types.all %} - + {% endfor %}
{{ ct }}{{ object_type }}
From bef17e5a9511fce2646d669be7f4ce6f583e7319 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 1 Mar 2024 15:54:21 -0500 Subject: [PATCH 15/47] Rename ExportTemplate.content_types to object_types & use ObjectType proxy --- netbox/extras/api/serializers.py | 4 ++-- netbox/extras/filtersets.py | 8 ++++---- netbox/extras/forms/bulk_import.py | 6 +++--- netbox/extras/forms/filtersets.py | 4 ++-- netbox/extras/forms/model_forms.py | 6 +++--- netbox/extras/graphql/types.py | 2 +- .../extras/migrations/0111_rename_content_types.py | 12 ++++++++++++ netbox/extras/models/models.py | 4 ++-- netbox/extras/tables/tables.py | 8 ++++---- netbox/extras/tests/test_api.py | 8 ++++---- netbox/extras/tests/test_filtersets.py | 10 +++++----- netbox/extras/tests/test_views.py | 8 ++++---- netbox/templates/extras/exporttemplate.html | 4 ++-- netbox/utilities/templates/buttons/export.html | 2 +- netbox/utilities/templatetags/buttons.py | 9 +++++---- 15 files changed, 54 insertions(+), 41 deletions(-) diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index 32b708fcc..3871fcc70 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -216,7 +216,7 @@ class CustomLinkSerializer(ValidatedModelSerializer): class ExportTemplateSerializer(ValidatedModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='extras-api:exporttemplate-detail') - content_types = ContentTypeField( + object_types = ContentTypeField( queryset=ObjectType.objects.with_feature('export_templates'), many=True ) @@ -230,7 +230,7 @@ class ExportTemplateSerializer(ValidatedModelSerializer): class Meta: model = ExportTemplate fields = [ - 'id', 'url', 'display', 'content_types', 'name', 'description', 'template_code', 'mime_type', + 'id', 'url', 'display', 'object_types', 'name', 'description', 'template_code', 'mime_type', 'file_extension', 'as_attachment', 'data_source', 'data_path', 'data_file', 'data_synced', 'created', 'last_updated', ] diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py index d03d134be..83978806a 100644 --- a/netbox/extras/filtersets.py +++ b/netbox/extras/filtersets.py @@ -217,10 +217,10 @@ class ExportTemplateFilterSet(BaseFilterSet): method='search', label=_('Search'), ) - content_type_id = MultiValueNumberFilter( - field_name='content_types__id' + object_types_id = MultiValueNumberFilter( + field_name='object_types__id' ) - content_types = ContentTypeFilter() + object_types = ContentTypeFilter() data_source_id = django_filters.ModelMultipleChoiceFilter( queryset=DataSource.objects.all(), label=_('Data source (ID)'), @@ -232,7 +232,7 @@ class ExportTemplateFilterSet(BaseFilterSet): class Meta: model = ExportTemplate - fields = ['id', 'content_types', 'name', 'description', 'data_synced'] + fields = ['id', 'object_types', 'name', 'description', 'data_synced'] def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/extras/forms/bulk_import.py b/netbox/extras/forms/bulk_import.py index cbb7b4e44..9c68c7ba3 100644 --- a/netbox/extras/forms/bulk_import.py +++ b/netbox/extras/forms/bulk_import.py @@ -126,8 +126,8 @@ class CustomLinkImportForm(CSVModelForm): class ExportTemplateImportForm(CSVModelForm): - content_types = CSVMultipleContentTypeField( - label=_('Content types'), + object_types = CSVMultipleContentTypeField( + label=_('Object types'), queryset=ObjectType.objects.with_feature('export_templates'), help_text=_("One or more assigned object types") ) @@ -135,7 +135,7 @@ class ExportTemplateImportForm(CSVModelForm): class Meta: model = ExportTemplate fields = ( - 'name', 'content_types', 'description', 'mime_type', 'file_extension', 'as_attachment', 'template_code', + 'name', 'object_types', 'description', 'mime_type', 'file_extension', 'as_attachment', 'template_code', ) diff --git a/netbox/extras/forms/filtersets.py b/netbox/extras/forms/filtersets.py index f19695f57..daad010c1 100644 --- a/netbox/extras/forms/filtersets.py +++ b/netbox/extras/forms/filtersets.py @@ -139,7 +139,7 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm): fieldsets = ( (None, ('q', 'filter_id')), (_('Data'), ('data_source_id', 'data_file_id')), - (_('Attributes'), ('content_type_id', 'mime_type', 'file_extension', 'as_attachment')), + (_('Attributes'), ('object_types_id', 'mime_type', 'file_extension', 'as_attachment')), ) data_source_id = DynamicModelMultipleChoiceField( queryset=DataSource.objects.all(), @@ -154,7 +154,7 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm): 'source_id': '$data_source_id' } ) - content_type_id = ContentTypeMultipleChoiceField( + object_types_id = ContentTypeMultipleChoiceField( queryset=ObjectType.objects.with_feature('export_templates'), required=False, label=_('Content types') diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py index 9423e35e2..95af8fb3b 100644 --- a/netbox/extras/forms/model_forms.py +++ b/netbox/extras/forms/model_forms.py @@ -151,8 +151,8 @@ class CustomLinkForm(forms.ModelForm): class ExportTemplateForm(SyncedDataMixin, forms.ModelForm): - content_types = ContentTypeMultipleChoiceField( - label=_('Content types'), + object_types = ContentTypeMultipleChoiceField( + label=_('Object types'), queryset=ObjectType.objects.with_feature('export_templates') ) template_code = forms.CharField( @@ -162,7 +162,7 @@ class ExportTemplateForm(SyncedDataMixin, forms.ModelForm): ) fieldsets = ( - (_('Export Template'), ('name', 'content_types', 'description', 'template_code')), + (_('Export Template'), ('name', 'object_types', 'description', 'template_code')), (_('Data Source'), ('data_source', 'data_file', 'auto_sync_enabled')), (_('Rendering'), ('mime_type', 'file_extension', 'as_attachment')), ) diff --git a/netbox/extras/graphql/types.py b/netbox/extras/graphql/types.py index 15ef76b6b..1000925bb 100644 --- a/netbox/extras/graphql/types.py +++ b/netbox/extras/graphql/types.py @@ -71,7 +71,7 @@ class ExportTemplateType(ObjectType): class Meta: model = models.ExportTemplate - exclude = ('content_types', ) + exclude = ('object_types', ) filterset_class = filtersets.ExportTemplateFilterSet diff --git a/netbox/extras/migrations/0111_rename_content_types.py b/netbox/extras/migrations/0111_rename_content_types.py index f44b01e34..87f721589 100644 --- a/netbox/extras/migrations/0111_rename_content_types.py +++ b/netbox/extras/migrations/0111_rename_content_types.py @@ -50,4 +50,16 @@ class Migration(migrations.Migration): name='object_types', field=models.ManyToManyField(related_name='event_rules', to='core.objecttype'), ), + + # Export templates + migrations.RenameField( + model_name='exporttemplate', + old_name='content_types', + new_name='object_types', + ), + migrations.AlterField( + model_name='exporttemplate', + name='object_types', + field=models.ManyToManyField(related_name='export_templates', to='core.objecttype'), + ), ] diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index bfc11b7c5..77e703dc0 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -409,8 +409,8 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): class ExportTemplate(SyncedDataMixin, CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): - content_types = models.ManyToManyField( - to='contenttypes.ContentType', + object_types = models.ManyToManyField( + to='core.ObjectType', related_name='export_templates', help_text=_('The object type(s) to which this template applies.') ) diff --git a/netbox/extras/tables/tables.py b/netbox/extras/tables/tables.py index 7775916b9..abc73c5ba 100644 --- a/netbox/extras/tables/tables.py +++ b/netbox/extras/tables/tables.py @@ -139,8 +139,8 @@ class ExportTemplateTable(NetBoxTable): verbose_name=_('Name'), linkify=True ) - content_types = columns.ContentTypesColumn( - verbose_name=_('Content Types'), + object_types = columns.ContentTypesColumn( + verbose_name=_('Object Types'), ) as_attachment = columns.BooleanColumn( verbose_name=_('As Attachment'), @@ -161,11 +161,11 @@ class ExportTemplateTable(NetBoxTable): class Meta(NetBoxTable.Meta): model = ExportTemplate fields = ( - 'pk', 'id', 'name', 'content_types', 'description', 'mime_type', 'file_extension', 'as_attachment', + 'pk', 'id', 'name', 'object_types', 'description', 'mime_type', 'file_extension', 'as_attachment', 'data_source', 'data_file', 'data_synced', 'created', 'last_updated', ) default_columns = ( - 'pk', 'name', 'content_types', 'description', 'mime_type', 'file_extension', 'as_attachment', 'is_synced', + 'pk', 'name', 'object_types', 'description', 'mime_type', 'file_extension', 'as_attachment', 'is_synced', ) diff --git a/netbox/extras/tests/test_api.py b/netbox/extras/tests/test_api.py index 7bed486a0..34a1bdc40 100644 --- a/netbox/extras/tests/test_api.py +++ b/netbox/extras/tests/test_api.py @@ -458,17 +458,17 @@ class ExportTemplateTest(APIViewTestCases.APIViewTestCase): brief_fields = ['description', 'display', 'id', 'name', 'url'] create_data = [ { - 'content_types': ['dcim.device'], + 'object_types': ['dcim.device'], 'name': 'Test Export Template 4', 'template_code': '{% for obj in queryset %}{{ obj.name }}\n{% endfor %}', }, { - 'content_types': ['dcim.device'], + 'object_types': ['dcim.device'], 'name': 'Test Export Template 5', 'template_code': '{% for obj in queryset %}{{ obj.name }}\n{% endfor %}', }, { - 'content_types': ['dcim.device'], + 'object_types': ['dcim.device'], 'name': 'Test Export Template 6', 'template_code': '{% for obj in queryset %}{{ obj.name }}\n{% endfor %}', }, @@ -495,7 +495,7 @@ class ExportTemplateTest(APIViewTestCases.APIViewTestCase): ) ExportTemplate.objects.bulk_create(export_templates) for et in export_templates: - et.content_types.set([ContentType.objects.get_for_model(Device)]) + et.object_types.set([ObjectType.objects.get_for_model(Device)]) class TagTest(APIViewTestCases.APIViewTestCase): diff --git a/netbox/extras/tests/test_filtersets.py b/netbox/extras/tests/test_filtersets.py index 43ee605fb..af079786b 100644 --- a/netbox/extras/tests/test_filtersets.py +++ b/netbox/extras/tests/test_filtersets.py @@ -639,7 +639,7 @@ class ExportTemplateTestCase(TestCase, BaseFilterSetTests): @classmethod def setUpTestData(cls): - content_types = ContentType.objects.filter(model__in=['site', 'rack', 'device']) + object_types = ObjectType.objects.filter(model__in=['site', 'rack', 'device']) export_templates = ( ExportTemplate(name='Export Template 1', template_code='TESTING', description='foobar1'), @@ -648,7 +648,7 @@ class ExportTemplateTestCase(TestCase, BaseFilterSetTests): ) ExportTemplate.objects.bulk_create(export_templates) for i, et in enumerate(export_templates): - et.content_types.set([content_types[i]]) + et.object_types.set([object_types[i]]) def test_q(self): params = {'q': 'foobar1'} @@ -658,10 +658,10 @@ class ExportTemplateTestCase(TestCase, BaseFilterSetTests): params = {'name': ['Export Template 1', 'Export Template 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - def test_content_types(self): - params = {'content_types': 'dcim.site'} + def test_object_types(self): + params = {'object_types': 'dcim.site'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) - params = {'content_type_id': [ContentType.objects.get_for_model(Site).pk]} + params = {'object_types_id': [ContentType.objects.get_for_model(Site).pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) def test_description(self): diff --git a/netbox/extras/tests/test_views.py b/netbox/extras/tests/test_views.py index ba7cd0818..02114d035 100644 --- a/netbox/extras/tests/test_views.py +++ b/netbox/extras/tests/test_views.py @@ -303,7 +303,7 @@ class ExportTemplateTestCase(ViewTestCases.PrimaryObjectViewTestCase): @classmethod def setUpTestData(cls): - site_ct = ContentType.objects.get_for_model(Site) + site_type = ObjectType.objects.get_for_model(Site) TEMPLATE_CODE = """{% for object in queryset %}{{ object }}{% endfor %}""" export_templates = ( @@ -313,16 +313,16 @@ class ExportTemplateTestCase(ViewTestCases.PrimaryObjectViewTestCase): ) ExportTemplate.objects.bulk_create(export_templates) for et in export_templates: - et.content_types.set([site_ct]) + et.object_types.set([site_type]) cls.form_data = { 'name': 'Export Template X', - 'content_types': [site_ct.pk], + 'object_types': [site_type.pk], 'template_code': TEMPLATE_CODE, } cls.csv_data = ( - "name,content_types,template_code", + "name,object_types,template_code", f"Export Template 4,dcim.site,{TEMPLATE_CODE}", f"Export Template 5,dcim.site,{TEMPLATE_CODE}", f"Export Template 6,dcim.site,{TEMPLATE_CODE}", diff --git a/netbox/templates/extras/exporttemplate.html b/netbox/templates/extras/exporttemplate.html index 0648a8191..16dd49ee2 100644 --- a/netbox/templates/extras/exporttemplate.html +++ b/netbox/templates/extras/exporttemplate.html @@ -70,9 +70,9 @@
{% trans "Assigned Models" %}
- {% for ct in object.content_types.all %} + {% for object_type in object.object_types.all %} - + {% endfor %}
{{ ct }}{{ object_type }}
diff --git a/netbox/utilities/templates/buttons/export.html b/netbox/utilities/templates/buttons/export.html index d24be88f7..2085356fa 100644 --- a/netbox/utilities/templates/buttons/export.html +++ b/netbox/utilities/templates/buttons/export.html @@ -25,7 +25,7 @@
  • - {% trans "Add export template" %}... + {% trans "Add export template" %}...
  • {% endif %} diff --git a/netbox/utilities/templatetags/buttons.py b/netbox/utilities/templatetags/buttons.py index 828af3b43..c0870d585 100644 --- a/netbox/utilities/templatetags/buttons.py +++ b/netbox/utilities/templatetags/buttons.py @@ -2,6 +2,7 @@ from django import template from django.contrib.contenttypes.models import ContentType from django.urls import NoReverseMatch, reverse +from core.models import ObjectType from extras.models import Bookmark, ExportTemplate from utilities.utils import get_viewname, prepare_cloned_fields @@ -132,18 +133,18 @@ def import_button(model, action='import'): @register.inclusion_tag('buttons/export.html', takes_context=True) def export_button(context, model): - content_type = ContentType.objects.get_for_model(model) + object_type = ObjectType.objects.get_for_model(model) user = context['request'].user # Determine if the "all data" export returns CSV or YAML - data_format = 'YAML' if hasattr(content_type.model_class(), 'to_yaml') else 'CSV' + data_format = 'YAML' if hasattr(object_type.model_class(), 'to_yaml') else 'CSV' # Retrieve all export templates for this model - export_templates = ExportTemplate.objects.restrict(user, 'view').filter(content_types=content_type) + export_templates = ExportTemplate.objects.restrict(user, 'view').filter(object_types=object_type) return { 'perms': context['perms'], - 'content_type': content_type, + 'object_type': object_type, 'url_params': context['request'].GET.urlencode() if context['request'].GET else '', 'export_templates': export_templates, 'data_format': data_format, From ce6b2666a99f11127827dd6c17f5da58ed979c23 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 1 Mar 2024 16:08:01 -0500 Subject: [PATCH 16/47] Rename SavedFilter.content_types to object_types & use ObjectType proxy --- netbox/extras/api/serializers.py | 4 ++-- netbox/extras/filtersets.py | 8 ++++---- netbox/extras/forms/bulk_import.py | 6 +++--- netbox/extras/forms/filtersets.py | 6 +++--- netbox/extras/forms/model_forms.py | 6 +++--- netbox/extras/graphql/types.py | 2 +- .../extras/migrations/0111_rename_content_types.py | 12 ++++++++++++ netbox/extras/models/models.py | 4 ++-- netbox/extras/tables/tables.py | 8 ++++---- netbox/extras/tests/test_api.py | 10 +++++----- netbox/extras/tests/test_filtersets.py | 10 +++++----- netbox/extras/tests/test_forms.py | 2 +- netbox/extras/tests/test_views.py | 8 ++++---- netbox/templates/extras/savedfilter.html | 4 ++-- 14 files changed, 51 insertions(+), 39 deletions(-) diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index 3871fcc70..d2e296ffa 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -243,7 +243,7 @@ class ExportTemplateSerializer(ValidatedModelSerializer): class SavedFilterSerializer(ValidatedModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='extras-api:savedfilter-detail') - content_types = ContentTypeField( + object_types = ContentTypeField( queryset=ObjectType.objects.all(), many=True ) @@ -251,7 +251,7 @@ class SavedFilterSerializer(ValidatedModelSerializer): class Meta: model = SavedFilter fields = [ - 'id', 'url', 'display', 'content_types', 'name', 'slug', 'description', 'user', 'weight', 'enabled', + 'id', 'url', 'display', 'object_types', 'name', 'slug', 'description', 'user', 'weight', 'enabled', 'shared', 'parameters', 'created', 'last_updated', ] brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description') diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py index 83978806a..fb5f972d1 100644 --- a/netbox/extras/filtersets.py +++ b/netbox/extras/filtersets.py @@ -248,10 +248,10 @@ class SavedFilterFilterSet(BaseFilterSet): method='search', label=_('Search'), ) - content_type_id = MultiValueNumberFilter( - field_name='content_types__id' + object_types_id = MultiValueNumberFilter( + field_name='object_types__id' ) - content_types = ContentTypeFilter() + object_types = ContentTypeFilter() user_id = django_filters.ModelMultipleChoiceFilter( queryset=get_user_model().objects.all(), label=_('User (ID)'), @@ -268,7 +268,7 @@ class SavedFilterFilterSet(BaseFilterSet): class Meta: model = SavedFilter - fields = ['id', 'content_types', 'name', 'slug', 'description', 'enabled', 'shared', 'weight'] + fields = ['id', 'object_types', 'name', 'slug', 'description', 'enabled', 'shared', 'weight'] def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/extras/forms/bulk_import.py b/netbox/extras/forms/bulk_import.py index 9c68c7ba3..39d2933a7 100644 --- a/netbox/extras/forms/bulk_import.py +++ b/netbox/extras/forms/bulk_import.py @@ -149,8 +149,8 @@ class ConfigTemplateImportForm(CSVModelForm): class SavedFilterImportForm(CSVModelForm): - content_types = CSVMultipleContentTypeField( - label=_('Content types'), + object_types = CSVMultipleContentTypeField( + label=_('Object types'), queryset=ObjectType.objects.all(), help_text=_("One or more assigned object types") ) @@ -158,7 +158,7 @@ class SavedFilterImportForm(CSVModelForm): class Meta: model = SavedFilter fields = ( - 'name', 'slug', 'content_types', 'description', 'weight', 'enabled', 'shared', 'parameters', + 'name', 'slug', 'object_types', 'description', 'weight', 'enabled', 'shared', 'parameters', ) diff --git a/netbox/extras/forms/filtersets.py b/netbox/extras/forms/filtersets.py index daad010c1..42ba5618c 100644 --- a/netbox/extras/forms/filtersets.py +++ b/netbox/extras/forms/filtersets.py @@ -195,10 +195,10 @@ class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm): class SavedFilterFilterForm(SavedFiltersMixin, FilterForm): fieldsets = ( (None, ('q', 'filter_id')), - (_('Attributes'), ('content_types', 'enabled', 'shared', 'weight')), + (_('Attributes'), ('object_types', 'enabled', 'shared', 'weight')), ) - content_types = ContentTypeMultipleChoiceField( - label=_('Content types'), + object_types = ContentTypeMultipleChoiceField( + label=_('Object types'), queryset=ObjectType.objects.public(), required=False ) diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py index 95af8fb3b..7f36db657 100644 --- a/netbox/extras/forms/model_forms.py +++ b/netbox/extras/forms/model_forms.py @@ -192,14 +192,14 @@ class ExportTemplateForm(SyncedDataMixin, forms.ModelForm): class SavedFilterForm(forms.ModelForm): slug = SlugField() - content_types = ContentTypeMultipleChoiceField( - label=_('Content types'), + object_types = ContentTypeMultipleChoiceField( + label=_('Object types'), queryset=ObjectType.objects.all() ) parameters = JSONField() fieldsets = ( - (_('Saved Filter'), ('name', 'slug', 'content_types', 'description', 'weight', 'enabled', 'shared')), + (_('Saved Filter'), ('name', 'slug', 'object_types', 'description', 'weight', 'enabled', 'shared')), (_('Parameters'), ('parameters',)), ) diff --git a/netbox/extras/graphql/types.py b/netbox/extras/graphql/types.py index 1000925bb..9e0444da6 100644 --- a/netbox/extras/graphql/types.py +++ b/netbox/extras/graphql/types.py @@ -103,7 +103,7 @@ class SavedFilterType(ObjectType): class Meta: model = models.SavedFilter - exclude = ('content_types', ) + exclude = ('object_types', ) filterset_class = filtersets.SavedFilterFilterSet diff --git a/netbox/extras/migrations/0111_rename_content_types.py b/netbox/extras/migrations/0111_rename_content_types.py index 87f721589..df347dffd 100644 --- a/netbox/extras/migrations/0111_rename_content_types.py +++ b/netbox/extras/migrations/0111_rename_content_types.py @@ -62,4 +62,16 @@ class Migration(migrations.Migration): name='object_types', field=models.ManyToManyField(related_name='export_templates', to='core.objecttype'), ), + + # Saved filters + migrations.RenameField( + model_name='savedfilter', + old_name='content_types', + new_name='object_types', + ), + migrations.AlterField( + model_name='savedfilter', + name='object_types', + field=models.ManyToManyField(related_name='saved_filters', to='core.objecttype'), + ), ] diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index 77e703dc0..c5e35c9c7 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -518,8 +518,8 @@ class SavedFilter(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): """ A set of predefined keyword parameters that can be reused to filter for specific objects. """ - content_types = models.ManyToManyField( - to='contenttypes.ContentType', + object_types = models.ManyToManyField( + to='core.ObjectType', related_name='saved_filters', help_text=_('The object type(s) to which this filter applies.') ) diff --git a/netbox/extras/tables/tables.py b/netbox/extras/tables/tables.py index abc73c5ba..479cb568e 100644 --- a/netbox/extras/tables/tables.py +++ b/netbox/extras/tables/tables.py @@ -204,8 +204,8 @@ class SavedFilterTable(NetBoxTable): verbose_name=_('Name'), linkify=True ) - content_types = columns.ContentTypesColumn( - verbose_name=_('Content Types'), + object_types = columns.ContentTypesColumn( + verbose_name=_('Object Types'), ) enabled = columns.BooleanColumn( verbose_name=_('Enabled'), @@ -220,11 +220,11 @@ class SavedFilterTable(NetBoxTable): class Meta(NetBoxTable.Meta): model = SavedFilter fields = ( - 'pk', 'id', 'name', 'slug', 'content_types', 'description', 'user', 'weight', 'enabled', 'shared', + 'pk', 'id', 'name', 'slug', 'object_types', 'description', 'user', 'weight', 'enabled', 'shared', 'created', 'last_updated', 'parameters' ) default_columns = ( - 'pk', 'name', 'content_types', 'user', 'description', 'enabled', 'shared', + 'pk', 'name', 'object_types', 'user', 'description', 'enabled', 'shared', ) diff --git a/netbox/extras/tests/test_api.py b/netbox/extras/tests/test_api.py index 34a1bdc40..eaa031837 100644 --- a/netbox/extras/tests/test_api.py +++ b/netbox/extras/tests/test_api.py @@ -333,7 +333,7 @@ class SavedFilterTest(APIViewTestCases.APIViewTestCase): brief_fields = ['description', 'display', 'id', 'name', 'slug', 'url'] create_data = [ { - 'content_types': ['dcim.site'], + 'object_types': ['dcim.site'], 'name': 'Saved Filter 4', 'slug': 'saved-filter-4', 'weight': 100, @@ -342,7 +342,7 @@ class SavedFilterTest(APIViewTestCases.APIViewTestCase): 'parameters': {'status': ['active']}, }, { - 'content_types': ['dcim.site'], + 'object_types': ['dcim.site'], 'name': 'Saved Filter 5', 'slug': 'saved-filter-5', 'weight': 200, @@ -351,7 +351,7 @@ class SavedFilterTest(APIViewTestCases.APIViewTestCase): 'parameters': {'status': ['planned']}, }, { - 'content_types': ['dcim.site'], + 'object_types': ['dcim.site'], 'name': 'Saved Filter 6', 'slug': 'saved-filter-6', 'weight': 300, @@ -368,7 +368,7 @@ class SavedFilterTest(APIViewTestCases.APIViewTestCase): @classmethod def setUpTestData(cls): - site_ct = ContentType.objects.get_for_model(Site) + site_type = ObjectType.objects.get_for_model(Site) saved_filters = ( SavedFilter( @@ -398,7 +398,7 @@ class SavedFilterTest(APIViewTestCases.APIViewTestCase): ) SavedFilter.objects.bulk_create(saved_filters) for i, savedfilter in enumerate(saved_filters): - savedfilter.content_types.set([site_ct]) + savedfilter.object_types.set([site_type]) class BookmarkTest( diff --git a/netbox/extras/tests/test_filtersets.py b/netbox/extras/tests/test_filtersets.py index af079786b..a45da8e20 100644 --- a/netbox/extras/tests/test_filtersets.py +++ b/netbox/extras/tests/test_filtersets.py @@ -466,7 +466,7 @@ class SavedFilterTestCase(TestCase, BaseFilterSetTests): @classmethod def setUpTestData(cls): - content_types = ContentType.objects.filter(model__in=['site', 'rack', 'device']) + object_types = ObjectType.objects.filter(model__in=['site', 'rack', 'device']) users = ( User(username='User 1'), @@ -509,7 +509,7 @@ class SavedFilterTestCase(TestCase, BaseFilterSetTests): ) SavedFilter.objects.bulk_create(saved_filters) for i, savedfilter in enumerate(saved_filters): - savedfilter.content_types.set([content_types[i]]) + savedfilter.object_types.set([object_types[i]]) def test_q(self): params = {'q': 'foobar1'} @@ -527,10 +527,10 @@ class SavedFilterTestCase(TestCase, BaseFilterSetTests): params = {'description': ['foobar1', 'foobar2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - def test_content_types(self): - params = {'content_types': 'dcim.site'} + def test_object_types(self): + params = {'object_types': 'dcim.site'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) - params = {'content_type_id': [ContentType.objects.get_for_model(Site).pk]} + params = {'object_types_id': [ContentType.objects.get_for_model(Site).pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) def test_user(self): diff --git a/netbox/extras/tests/test_forms.py b/netbox/extras/tests/test_forms.py index 5adb57fb2..7642ee2a8 100644 --- a/netbox/extras/tests/test_forms.py +++ b/netbox/extras/tests/test_forms.py @@ -100,7 +100,7 @@ class SavedFilterFormTest(TestCase): form = SavedFilterForm({ 'name': 'test-sf', 'slug': 'test-sf', - 'content_types': [ContentType.objects.get_for_model(Site).pk], + 'object_types': [ContentType.objects.get_for_model(Site).pk], 'weight': 100, 'parameters': { "status": [ diff --git a/netbox/extras/tests/test_views.py b/netbox/extras/tests/test_views.py index 02114d035..ca6ad9174 100644 --- a/netbox/extras/tests/test_views.py +++ b/netbox/extras/tests/test_views.py @@ -184,7 +184,7 @@ class SavedFilterTestCase(ViewTestCases.PrimaryObjectViewTestCase): @classmethod def setUpTestData(cls): - site_ct = ContentType.objects.get_for_model(Site) + site_type = ObjectType.objects.get_for_model(Site) users = ( User(username='User 1'), @@ -218,12 +218,12 @@ class SavedFilterTestCase(ViewTestCases.PrimaryObjectViewTestCase): ) SavedFilter.objects.bulk_create(saved_filters) for i, savedfilter in enumerate(saved_filters): - savedfilter.content_types.set([site_ct]) + savedfilter.object_types.set([site_type]) cls.form_data = { 'name': 'Saved Filter X', 'slug': 'saved-filter-x', - 'content_types': [site_ct.pk], + 'object_types': [site_type.pk], 'description': 'Foo', 'weight': 1000, 'enabled': True, @@ -232,7 +232,7 @@ class SavedFilterTestCase(ViewTestCases.PrimaryObjectViewTestCase): } cls.csv_data = ( - 'name,slug,content_types,weight,enabled,shared,parameters', + 'name,slug,object_types,weight,enabled,shared,parameters', 'Saved Filter 4,saved-filter-4,dcim.device,400,True,True,{"foo": "a"}', 'Saved Filter 5,saved-filter-5,dcim.device,500,True,True,{"foo": "b"}', 'Saved Filter 6,saved-filter-6,dcim.device,600,True,True,{"foo": "c"}', diff --git a/netbox/templates/extras/savedfilter.html b/netbox/templates/extras/savedfilter.html index 840852c7f..9b10f1375 100644 --- a/netbox/templates/extras/savedfilter.html +++ b/netbox/templates/extras/savedfilter.html @@ -38,9 +38,9 @@
    {% trans "Assigned Models" %}
    - {% for ct in object.content_types.all %} + {% for object_type in object.object_types.all %} - + {% endfor %}
    {{ ct }}{{ object_type }}
    From e0165539b31119d3ef0f670e7636d7255bfc3a11 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 1 Mar 2024 16:34:52 -0500 Subject: [PATCH 17/47] Rename ImageAttachment.content_type to object_type --- netbox/extras/api/serializers.py | 8 ++++---- netbox/extras/filtersets.py | 4 ++-- netbox/extras/forms/filtersets.py | 6 +++--- .../migrations/0111_rename_content_types.py | 15 +++++++++++++++ netbox/extras/models/models.py | 12 ++++++------ netbox/extras/tables/tables.py | 8 ++++---- netbox/extras/tests/test_api.py | 6 +++--- netbox/extras/tests/test_filtersets.py | 16 ++++++++-------- netbox/extras/utils.py | 2 +- netbox/netbox/models/features.py | 4 +++- netbox/tenancy/forms/bulk_import.py | 4 ++-- 11 files changed, 51 insertions(+), 34 deletions(-) diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index d2e296ffa..46189cf4e 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -312,7 +312,7 @@ class TagSerializer(ValidatedModelSerializer): class ImageAttachmentSerializer(ValidatedModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='extras-api:imageattachment-detail') - content_type = ContentTypeField( + object_type = ContentTypeField( queryset=ObjectType.objects.all() ) parent = serializers.SerializerMethodField(read_only=True) @@ -320,7 +320,7 @@ class ImageAttachmentSerializer(ValidatedModelSerializer): class Meta: model = ImageAttachment fields = [ - 'id', 'url', 'display', 'content_type', 'object_id', 'parent', 'name', 'image', 'image_height', + 'id', 'url', 'display', 'object_type', 'object_id', 'parent', 'name', 'image', 'image_height', 'image_width', 'created', 'last_updated', ] brief_fields = ('id', 'url', 'display', 'name', 'image') @@ -329,10 +329,10 @@ class ImageAttachmentSerializer(ValidatedModelSerializer): # Validate that the parent object exists try: - data['content_type'].get_object_for_this_type(id=data['object_id']) + data['object_type'].get_object_for_this_type(id=data['object_id']) except ObjectDoesNotExist: raise serializers.ValidationError( - "Invalid parent object: {} ID {}".format(data['content_type'], data['object_id']) + "Invalid parent object: {} ID {}".format(data['object_type'], data['object_id']) ) # Enforce model validation diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py index fb5f972d1..797c76b51 100644 --- a/netbox/extras/filtersets.py +++ b/netbox/extras/filtersets.py @@ -318,11 +318,11 @@ class ImageAttachmentFilterSet(BaseFilterSet): label=_('Search'), ) created = django_filters.DateTimeFilter() - content_type = ContentTypeFilter() + object_type = ContentTypeFilter() class Meta: model = ImageAttachment - fields = ['id', 'content_type_id', 'object_id', 'name'] + fields = ['id', 'object_type_id', 'object_id', 'name'] def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/extras/forms/filtersets.py b/netbox/extras/forms/filtersets.py index 42ba5618c..75724b108 100644 --- a/netbox/extras/forms/filtersets.py +++ b/netbox/extras/forms/filtersets.py @@ -179,10 +179,10 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm): class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm): fieldsets = ( (None, ('q', 'filter_id')), - (_('Attributes'), ('content_type_id', 'name',)), + (_('Attributes'), ('object_type_id', 'name',)), ) - content_type_id = ContentTypeChoiceField( - label=_('Content type'), + object_type_id = ContentTypeChoiceField( + label=_('Object type'), queryset=ObjectType.objects.with_feature('image_attachments'), required=False ) diff --git a/netbox/extras/migrations/0111_rename_content_types.py b/netbox/extras/migrations/0111_rename_content_types.py index df347dffd..3d6529692 100644 --- a/netbox/extras/migrations/0111_rename_content_types.py +++ b/netbox/extras/migrations/0111_rename_content_types.py @@ -74,4 +74,19 @@ class Migration(migrations.Migration): name='object_types', field=models.ManyToManyField(related_name='saved_filters', to='core.objecttype'), ), + + # Image attachments + migrations.RemoveIndex( + model_name='imageattachment', + name='extras_imag_content_94728e_idx', + ), + migrations.RenameField( + model_name='imageattachment', + old_name='content_type', + new_name='object_type', + ), + migrations.AddIndex( + model_name='imageattachment', + index=models.Index(fields=['object_type', 'object_id'], name='extras_imag_object__96bebc_idx'), + ), ] diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index c5e35c9c7..4a57c6ada 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -598,13 +598,13 @@ class ImageAttachment(ChangeLoggedModel): """ An uploaded image which is associated with an object. """ - content_type = models.ForeignKey( + object_type = models.ForeignKey( to='contenttypes.ContentType', on_delete=models.CASCADE ) object_id = models.PositiveBigIntegerField() parent = GenericForeignKey( - ct_field='content_type', + ct_field='object_type', fk_field='object_id' ) image = models.ImageField( @@ -626,12 +626,12 @@ class ImageAttachment(ChangeLoggedModel): objects = RestrictedQuerySet.as_manager() - clone_fields = ('content_type', 'object_id') + clone_fields = ('object_type', 'object_id') class Meta: ordering = ('name', 'pk') # name may be non-unique indexes = ( - models.Index(fields=('content_type', 'object_id')), + models.Index(fields=('object_type', 'object_id')), ) verbose_name = _('image attachment') verbose_name_plural = _('image attachments') @@ -646,9 +646,9 @@ class ImageAttachment(ChangeLoggedModel): super().clean() # Validate the assigned object type - if self.content_type not in ObjectType.objects.with_feature('image_attachments'): + if self.object_type not in ObjectType.objects.with_feature('image_attachments'): raise ValidationError( - _("Image attachments cannot be assigned to this object type ({type}).").format(type=self.content_type) + _("Image attachments cannot be assigned to this object type ({type}).").format(type=self.object_type) ) def delete(self, *args, **kwargs): diff --git a/netbox/extras/tables/tables.py b/netbox/extras/tables/tables.py index 479cb568e..5bf4f1892 100644 --- a/netbox/extras/tables/tables.py +++ b/netbox/extras/tables/tables.py @@ -174,8 +174,8 @@ class ImageAttachmentTable(NetBoxTable): verbose_name=_('ID'), linkify=False ) - content_type = columns.ContentTypeColumn( - verbose_name=_('Content Type'), + object_type = columns.ContentTypeColumn( + verbose_name=_('Object Type'), ) parent = tables.Column( verbose_name=_('Parent'), @@ -193,10 +193,10 @@ class ImageAttachmentTable(NetBoxTable): class Meta(NetBoxTable.Meta): model = ImageAttachment fields = ( - 'pk', 'content_type', 'parent', 'image', 'name', 'image_height', 'image_width', 'size', 'created', + 'pk', 'object_type', 'parent', 'image', 'name', 'image_height', 'image_width', 'size', 'created', 'last_updated', ) - default_columns = ('content_type', 'parent', 'image', 'name', 'size', 'created') + default_columns = ('object_type', 'parent', 'image', 'name', 'size', 'created') class SavedFilterTable(NetBoxTable): diff --git a/netbox/extras/tests/test_api.py b/netbox/extras/tests/test_api.py index eaa031837..53d981123 100644 --- a/netbox/extras/tests/test_api.py +++ b/netbox/extras/tests/test_api.py @@ -548,7 +548,7 @@ class ImageAttachmentTest( image_attachments = ( ImageAttachment( - content_type=ct, + object_type=ct, object_id=site.pk, name='Image Attachment 1', image='http://example.com/image1.png', @@ -556,7 +556,7 @@ class ImageAttachmentTest( image_width=100 ), ImageAttachment( - content_type=ct, + object_type=ct, object_id=site.pk, name='Image Attachment 2', image='http://example.com/image2.png', @@ -564,7 +564,7 @@ class ImageAttachmentTest( image_width=100 ), ImageAttachment( - content_type=ct, + object_type=ct, object_id=site.pk, name='Image Attachment 3', image='http://example.com/image3.png', diff --git a/netbox/extras/tests/test_filtersets.py b/netbox/extras/tests/test_filtersets.py index a45da8e20..762818d6d 100644 --- a/netbox/extras/tests/test_filtersets.py +++ b/netbox/extras/tests/test_filtersets.py @@ -693,7 +693,7 @@ class ImageAttachmentTestCase(TestCase, BaseFilterSetTests): image_attachments = ( ImageAttachment( - content_type=site_ct, + object_type=site_ct, object_id=sites[0].pk, name='Image Attachment 1', image='http://example.com/image1.png', @@ -701,7 +701,7 @@ class ImageAttachmentTestCase(TestCase, BaseFilterSetTests): image_width=100 ), ImageAttachment( - content_type=site_ct, + object_type=site_ct, object_id=sites[1].pk, name='Image Attachment 2', image='http://example.com/image2.png', @@ -709,7 +709,7 @@ class ImageAttachmentTestCase(TestCase, BaseFilterSetTests): image_width=100 ), ImageAttachment( - content_type=rack_ct, + object_type=rack_ct, object_id=racks[0].pk, name='Image Attachment 3', image='http://example.com/image3.png', @@ -717,7 +717,7 @@ class ImageAttachmentTestCase(TestCase, BaseFilterSetTests): image_width=100 ), ImageAttachment( - content_type=rack_ct, + object_type=rack_ct, object_id=racks[1].pk, name='Image Attachment 4', image='http://example.com/image4.png', @@ -735,13 +735,13 @@ class ImageAttachmentTestCase(TestCase, BaseFilterSetTests): params = {'name': ['Image Attachment 1', 'Image Attachment 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - def test_content_type(self): - params = {'content_type': 'dcim.site'} + def test_object_type(self): + params = {'object_type': 'dcim.site'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - def test_content_type_id_and_object_id(self): + def test_object_type_id_and_object_id(self): params = { - 'content_type_id': ContentType.objects.get(app_label='dcim', model='site').pk, + 'object_type_id': ContentType.objects.get(app_label='dcim', model='site').pk, 'object_id': [Site.objects.first().pk], } self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) diff --git a/netbox/extras/utils.py b/netbox/extras/utils.py index 4464af718..e67b9b50c 100644 --- a/netbox/extras/utils.py +++ b/netbox/extras/utils.py @@ -24,7 +24,7 @@ def image_upload(instance, filename): elif instance.name: filename = instance.name - return '{}{}_{}_{}'.format(path, instance.content_type.name, instance.object_id, filename) + return '{}{}_{}_{}'.format(path, instance.object_type.name, instance.object_id, filename) def is_script(obj): diff --git a/netbox/netbox/models/features.py b/netbox/netbox/models/features.py index fb6cf8498..c8137ec66 100644 --- a/netbox/netbox/models/features.py +++ b/netbox/netbox/models/features.py @@ -329,7 +329,9 @@ class ImageAttachmentsMixin(models.Model): Enables the assignments of ImageAttachments. """ images = GenericRelation( - to='extras.ImageAttachment' + to='extras.ImageAttachment', + content_type_field='object_type', + object_id_field='object_id' ) class Meta: diff --git a/netbox/tenancy/forms/bulk_import.py b/netbox/tenancy/forms/bulk_import.py index f38b3293d..f37317549 100644 --- a/netbox/tenancy/forms/bulk_import.py +++ b/netbox/tenancy/forms/bulk_import.py @@ -91,7 +91,7 @@ class ContactImportForm(NetBoxModelImportForm): class ContactAssignmentImportForm(NetBoxModelImportForm): - content_type = CSVContentTypeField( + object_type = CSVContentTypeField( queryset=ContentType.objects.all(), help_text=_("One or more assigned object types") ) @@ -108,4 +108,4 @@ class ContactAssignmentImportForm(NetBoxModelImportForm): class Meta: model = ContactAssignment - fields = ('content_type', 'object_id', 'contact', 'priority', 'role') + fields = ('object_type', 'object_id', 'contact', 'priority', 'role') From 5f43eabab1a5f727fc98ec6ba297b52d09788917 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 1 Mar 2024 16:54:01 -0500 Subject: [PATCH 18/47] Rename ContactAssignment.content_type to object_type --- netbox/netbox/models/features.py | 4 +- netbox/tenancy/api/serializers.py | 6 +-- netbox/tenancy/filtersets.py | 4 +- netbox/tenancy/forms/filtersets.py | 4 +- netbox/tenancy/forms/model_forms.py | 4 +- ...5_contactassignment_rename_content_type.py | 40 +++++++++++++++++++ netbox/tenancy/models/contacts.py | 14 +++---- netbox/tenancy/tables/contacts.py | 6 +-- netbox/tenancy/tests/test_api.py | 6 +-- netbox/tenancy/tests/test_filtersets.py | 4 +- netbox/tenancy/tests/test_views.py | 6 +-- netbox/tenancy/views.py | 8 ++-- 12 files changed, 74 insertions(+), 32 deletions(-) create mode 100644 netbox/tenancy/migrations/0015_contactassignment_rename_content_type.py diff --git a/netbox/netbox/models/features.py b/netbox/netbox/models/features.py index c8137ec66..74b24aa85 100644 --- a/netbox/netbox/models/features.py +++ b/netbox/netbox/models/features.py @@ -343,7 +343,9 @@ class ContactsMixin(models.Model): Enables the assignments of Contacts (via ContactAssignment). """ contacts = GenericRelation( - to='tenancy.ContactAssignment' + to='tenancy.ContactAssignment', + content_type_field='object_type', + object_id_field='object_id' ) class Meta: diff --git a/netbox/tenancy/api/serializers.py b/netbox/tenancy/api/serializers.py index 325d3b439..5e7382b70 100644 --- a/netbox/tenancy/api/serializers.py +++ b/netbox/tenancy/api/serializers.py @@ -100,7 +100,7 @@ class ContactSerializer(NetBoxModelSerializer): class ContactAssignmentSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactassignment-detail') - content_type = ContentTypeField( + object_type = ContentTypeField( queryset=ContentType.objects.all() ) object = serializers.SerializerMethodField(read_only=True) @@ -111,13 +111,13 @@ class ContactAssignmentSerializer(NetBoxModelSerializer): class Meta: model = ContactAssignment fields = [ - 'id', 'url', 'display', 'content_type', 'object_id', 'object', 'contact', 'role', 'priority', 'tags', + 'id', 'url', 'display', 'object_type', 'object_id', 'object', 'contact', 'role', 'priority', 'tags', 'custom_fields', 'created', 'last_updated', ] brief_fields = ('id', 'url', 'display', 'contact', 'role', 'priority') @extend_schema_field(OpenApiTypes.OBJECT) def get_object(self, instance): - serializer = get_serializer_for_model(instance.content_type.model_class(), prefix=NESTED_SERIALIZER_PREFIX) + serializer = get_serializer_for_model(instance.object_type.model_class(), prefix=NESTED_SERIALIZER_PREFIX) context = {'request': self.context['request']} return serializer(instance.object, context=context).data diff --git a/netbox/tenancy/filtersets.py b/netbox/tenancy/filtersets.py index 8079b4035..295d20774 100644 --- a/netbox/tenancy/filtersets.py +++ b/netbox/tenancy/filtersets.py @@ -86,7 +86,7 @@ class ContactAssignmentFilterSet(NetBoxModelFilterSet): method='search', label=_('Search'), ) - content_type = ContentTypeFilter() + object_type = ContentTypeFilter() contact_id = django_filters.ModelMultipleChoiceFilter( queryset=Contact.objects.all(), label=_('Contact (ID)'), @@ -118,7 +118,7 @@ class ContactAssignmentFilterSet(NetBoxModelFilterSet): class Meta: model = ContactAssignment - fields = ['id', 'content_type_id', 'object_id', 'priority', 'tag'] + fields = ['id', 'object_type_id', 'object_id', 'priority', 'tag'] def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/tenancy/forms/filtersets.py b/netbox/tenancy/forms/filtersets.py index e5f038923..fbd0f2ad0 100644 --- a/netbox/tenancy/forms/filtersets.py +++ b/netbox/tenancy/forms/filtersets.py @@ -83,9 +83,9 @@ class ContactAssignmentFilterForm(NetBoxModelFilterSetForm): model = ContactAssignment fieldsets = ( (None, ('q', 'filter_id', 'tag')), - (_('Assignment'), ('content_type_id', 'group_id', 'contact_id', 'role_id', 'priority')), + (_('Assignment'), ('object_type_id', 'group_id', 'contact_id', 'role_id', 'priority')), ) - content_type_id = ContentTypeMultipleChoiceField( + object_type_id = ContentTypeMultipleChoiceField( queryset=ObjectType.objects.with_feature('contacts'), required=False, label=_('Object type') diff --git a/netbox/tenancy/forms/model_forms.py b/netbox/tenancy/forms/model_forms.py index 9a53eba17..140d9cf9a 100644 --- a/netbox/tenancy/forms/model_forms.py +++ b/netbox/tenancy/forms/model_forms.py @@ -143,9 +143,9 @@ class ContactAssignmentForm(NetBoxModelForm): class Meta: model = ContactAssignment fields = ( - 'content_type', 'object_id', 'group', 'contact', 'role', 'priority', 'tags' + 'object_type', 'object_id', 'group', 'contact', 'role', 'priority', 'tags' ) widgets = { - 'content_type': forms.HiddenInput(), + 'object_type': forms.HiddenInput(), 'object_id': forms.HiddenInput(), } diff --git a/netbox/tenancy/migrations/0015_contactassignment_rename_content_type.py b/netbox/tenancy/migrations/0015_contactassignment_rename_content_type.py new file mode 100644 index 000000000..58b14e10f --- /dev/null +++ b/netbox/tenancy/migrations/0015_contactassignment_rename_content_type.py @@ -0,0 +1,40 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('extras', '0111_rename_content_types'), + ('tenancy', '0014_contactassignment_ordering'), + ] + + operations = [ + migrations.RemoveConstraint( + model_name='contactassignment', + name='tenancy_contactassignment_unique_object_contact_role', + ), + migrations.RemoveIndex( + model_name='contactassignment', + name='tenancy_con_content_693ff4_idx', + ), + migrations.RenameField( + model_name='contactassignment', + old_name='content_type', + new_name='object_type', + ), + migrations.AddIndex( + model_name='contactassignment', + index=models.Index( + fields=['object_type', 'object_id'], + name='tenancy_con_object__6f20f7_idx' + ), + ), + migrations.AddConstraint( + model_name='contactassignment', + constraint=models.UniqueConstraint( + fields=('object_type', 'object_id', 'contact', 'role'), + name='tenancy_contactassignment_unique_object_contact_role' + ), + ), + ] diff --git a/netbox/tenancy/models/contacts.py b/netbox/tenancy/models/contacts.py index 1ea62db0c..e31330657 100644 --- a/netbox/tenancy/models/contacts.py +++ b/netbox/tenancy/models/contacts.py @@ -111,13 +111,13 @@ class Contact(PrimaryModel): class ContactAssignment(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel): - content_type = models.ForeignKey( + object_type = models.ForeignKey( to='contenttypes.ContentType', on_delete=models.CASCADE ) object_id = models.PositiveBigIntegerField() object = GenericForeignKey( - ct_field='content_type', + ct_field='object_type', fk_field='object_id' ) contact = models.ForeignKey( @@ -137,16 +137,16 @@ class ContactAssignment(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, Chan blank=True ) - clone_fields = ('content_type', 'object_id', 'role', 'priority') + clone_fields = ('object_type', 'object_id', 'role', 'priority') class Meta: ordering = ('contact', 'priority', 'role', 'pk') indexes = ( - models.Index(fields=('content_type', 'object_id')), + models.Index(fields=('object_type', 'object_id')), ) constraints = ( models.UniqueConstraint( - fields=('content_type', 'object_id', 'contact', 'role'), + fields=('object_type', 'object_id', 'contact', 'role'), name='%(app_label)s_%(class)s_unique_object_contact_role' ), ) @@ -165,9 +165,9 @@ class ContactAssignment(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, Chan super().clean() # Validate the assigned object type - if self.content_type not in ObjectType.objects.with_feature('contacts'): + if self.object_type not in ObjectType.objects.with_feature('contacts'): raise ValidationError( - _("Contacts cannot be assigned to this object type ({type}).").format(type=self.content_type) + _("Contacts cannot be assigned to this object type ({type}).").format(type=self.object_type) ) def to_objectchange(self, action): diff --git a/netbox/tenancy/tables/contacts.py b/netbox/tenancy/tables/contacts.py index a22c04569..946058218 100644 --- a/netbox/tenancy/tables/contacts.py +++ b/netbox/tenancy/tables/contacts.py @@ -86,7 +86,7 @@ class ContactTable(NetBoxTable): class ContactAssignmentTable(NetBoxTable): - content_type = columns.ContentTypeColumn( + object_type = columns.ContentTypeColumn( verbose_name=_('Object Type') ) object = tables.Column( @@ -141,10 +141,10 @@ class ContactAssignmentTable(NetBoxTable): class Meta(NetBoxTable.Meta): model = ContactAssignment fields = ( - 'pk', 'content_type', 'object', 'contact', 'role', 'priority', 'contact_title', 'contact_phone', + 'pk', 'object_type', 'object', 'contact', 'role', 'priority', 'contact_title', 'contact_phone', 'contact_email', 'contact_address', 'contact_link', 'contact_description', 'contact_group', 'tags', 'actions' ) default_columns = ( - 'pk', 'content_type', 'object', 'contact', 'role', 'priority', 'contact_email', 'contact_phone' + 'pk', 'object_type', 'object', 'contact', 'role', 'priority', 'contact_email', 'contact_phone' ) diff --git a/netbox/tenancy/tests/test_api.py b/netbox/tenancy/tests/test_api.py index 175bfa947..de6b36fc6 100644 --- a/netbox/tenancy/tests/test_api.py +++ b/netbox/tenancy/tests/test_api.py @@ -246,21 +246,21 @@ class ContactAssignmentTest(APIViewTestCases.APIViewTestCase): cls.create_data = [ { - 'content_type': 'dcim.site', + 'object_type': 'dcim.site', 'object_id': sites[1].pk, 'contact': contacts[3].pk, 'role': contact_roles[0].pk, 'priority': ContactPriorityChoices.PRIORITY_PRIMARY, }, { - 'content_type': 'dcim.site', + 'object_type': 'dcim.site', 'object_id': sites[1].pk, 'contact': contacts[4].pk, 'role': contact_roles[1].pk, 'priority': ContactPriorityChoices.PRIORITY_SECONDARY, }, { - 'content_type': 'dcim.site', + 'object_type': 'dcim.site', 'object_id': sites[1].pk, 'contact': contacts[5].pk, 'role': contact_roles[2].pk, diff --git a/netbox/tenancy/tests/test_filtersets.py b/netbox/tenancy/tests/test_filtersets.py index ab72bd39f..729dd7204 100644 --- a/netbox/tenancy/tests/test_filtersets.py +++ b/netbox/tenancy/tests/test_filtersets.py @@ -295,8 +295,8 @@ class ContactAssignmentTestCase(TestCase, ChangeLoggedFilterSetTests): ) ContactAssignment.objects.bulk_create(assignments) - def test_content_type(self): - params = {'content_type_id': ContentType.objects.get_by_natural_key('dcim', 'site')} + def test_object_type(self): + params = {'object_type_id': ContentType.objects.get_by_natural_key('dcim', 'site')} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) def test_contact(self): diff --git a/netbox/tenancy/tests/test_views.py b/netbox/tenancy/tests/test_views.py index 2151a6e8b..cbdecc0d0 100644 --- a/netbox/tenancy/tests/test_views.py +++ b/netbox/tenancy/tests/test_views.py @@ -292,7 +292,7 @@ class ContactAssignmentTestCase( tags = create_tags('Alpha', 'Bravo', 'Charlie') cls.form_data = { - 'content_type': ContentType.objects.get_for_model(Site).pk, + 'object_type': ContentType.objects.get_for_model(Site).pk, 'object_id': sites[3].pk, 'contact': contacts[3].pk, 'role': contact_roles[3].pk, @@ -306,11 +306,11 @@ class ContactAssignmentTestCase( } def _get_url(self, action, instance=None): - # Override creation URL to append content_type & object_id parameters + # Override creation URL to append object_type & object_id parameters if action == 'add': url = reverse('tenancy:contactassignment_add') content_type = ContentType.objects.get_for_model(Site).pk object_id = Site.objects.first().pk - return f"{url}?content_type={content_type}&object_id={object_id}" + return f"{url}?object_type={content_type}&object_id={object_id}" return super()._get_url(action, instance=instance) diff --git a/netbox/tenancy/views.py b/netbox/tenancy/views.py index 1d2fceb04..4c4d263df 100644 --- a/netbox/tenancy/views.py +++ b/netbox/tenancy/views.py @@ -23,7 +23,7 @@ class ObjectContactsView(generic.ObjectChildrenView): def get_children(self, request, parent): return ContactAssignment.objects.restrict(request.user, 'view').filter( - content_type=ContentType.objects.get_for_model(parent), + object_type=ContentType.objects.get_for_model(parent), object_id=parent.pk ).order_by('priority', 'contact', 'role') @@ -31,7 +31,7 @@ class ObjectContactsView(generic.ObjectChildrenView): table = super().get_table(*args, **kwargs) # Hide object columns - table.columns.hide('content_type') + table.columns.hide('object_type') table.columns.hide('object') return table @@ -374,8 +374,8 @@ class ContactAssignmentEditView(generic.ObjectEditView): def alter_object(self, instance, request, args, kwargs): if not instance.pk: # Assign the object based on URL kwargs - content_type = get_object_or_404(ContentType, pk=request.GET.get('content_type')) - instance.object = get_object_or_404(content_type.model_class(), pk=request.GET.get('object_id')) + object_type = get_object_or_404(ContentType, pk=request.GET.get('object_type')) + instance.object = get_object_or_404(object_type.model_class(), pk=request.GET.get('object_id')) return instance def get_extra_addanother_params(self, request): From 570f64784fd100bedbca9ab8b113677670ab10b0 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 4 Mar 2024 09:23:06 -0500 Subject: [PATCH 19/47] Update Tag.object_types to reference ObjectType --- netbox/extras/graphql/types.py | 2 +- .../migrations/0112_tag_update_object_types.py | 17 +++++++++++++++++ netbox/extras/models/tags.py | 2 +- netbox/extras/signals.py | 5 +++-- netbox/extras/tests/test_filtersets.py | 10 +++++----- netbox/extras/tests/test_models.py | 4 ++-- 6 files changed, 29 insertions(+), 11 deletions(-) create mode 100644 netbox/extras/migrations/0112_tag_update_object_types.py diff --git a/netbox/extras/graphql/types.py b/netbox/extras/graphql/types.py index 9e0444da6..65819a75a 100644 --- a/netbox/extras/graphql/types.py +++ b/netbox/extras/graphql/types.py @@ -111,7 +111,7 @@ class TagType(ObjectType): class Meta: model = models.Tag - exclude = ('extras_taggeditem_items',) + exclude = ('object_types', 'extras_taggeditem_items',) filterset_class = filtersets.TagFilterSet diff --git a/netbox/extras/migrations/0112_tag_update_object_types.py b/netbox/extras/migrations/0112_tag_update_object_types.py new file mode 100644 index 000000000..87ec117a4 --- /dev/null +++ b/netbox/extras/migrations/0112_tag_update_object_types.py @@ -0,0 +1,17 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0010_gfk_indexes'), + ('extras', '0111_rename_content_types'), + ] + + operations = [ + migrations.AlterField( + model_name='tag', + name='object_types', + field=models.ManyToManyField(blank=True, related_name='+', to='core.objecttype'), + ), + ] diff --git a/netbox/extras/models/tags.py b/netbox/extras/models/tags.py index 3aba6df60..27b05638e 100644 --- a/netbox/extras/models/tags.py +++ b/netbox/extras/models/tags.py @@ -34,7 +34,7 @@ class Tag(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel, TagBase): blank=True, ) object_types = models.ManyToManyField( - to='contenttypes.ContentType', + to='core.ObjectType', related_name='+', blank=True, help_text=_("The object type(s) to which this this tag can be applied.") diff --git a/netbox/extras/signals.py b/netbox/extras/signals.py index 85c00169c..cacc5a83a 100644 --- a/netbox/extras/signals.py +++ b/netbox/extras/signals.py @@ -8,6 +8,7 @@ from django.dispatch import receiver, Signal from django.utils.translation import gettext_lazy as _ from django_prometheus.models import model_deletes, model_inserts, model_updates +from core.models import ObjectType from core.signals import job_end, job_start from extras.constants import EVENT_JOB_END, EVENT_JOB_START from extras.events import process_event_rules @@ -240,8 +241,8 @@ def validate_assigned_tags(sender, instance, action, model, pk_set, **kwargs): """ if action != 'pre_add': return - ct = ContentType.objects.get_for_model(instance) - # Retrieve any applied Tags that are restricted to certain object_types + ct = ObjectType.objects.get_for_model(instance) + # Retrieve any applied Tags that are restricted to certain object types for tag in model.objects.filter(pk__in=pk_set, object_types__isnull=False).prefetch_related('object_types'): if ct not in tag.object_types.all(): raise AbortRequest(f"Tag {tag} cannot be assigned to {ct.model} objects.") diff --git a/netbox/extras/tests/test_filtersets.py b/netbox/extras/tests/test_filtersets.py index 762818d6d..7be00a5a3 100644 --- a/netbox/extras/tests/test_filtersets.py +++ b/netbox/extras/tests/test_filtersets.py @@ -1114,9 +1114,9 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests): @classmethod def setUpTestData(cls): - content_types = { - 'site': ContentType.objects.get_by_natural_key('dcim', 'site'), - 'provider': ContentType.objects.get_by_natural_key('circuits', 'provider'), + object_types = { + 'site': ObjectType.objects.get_by_natural_key('dcim', 'site'), + 'provider': ObjectType.objects.get_by_natural_key('circuits', 'provider'), } tags = ( @@ -1125,8 +1125,8 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests): Tag(name='Tag 3', slug='tag-3', color='0000ff'), ) Tag.objects.bulk_create(tags) - tags[0].object_types.add(content_types['site']) - tags[1].object_types.add(content_types['provider']) + tags[0].object_types.add(object_types['site']) + tags[1].object_types.add(object_types['provider']) # Apply some tags so we can filter by content type site = Site.objects.create(name='Site 1', slug='site-1') diff --git a/netbox/extras/tests/test_models.py b/netbox/extras/tests/test_models.py index cb3f08acb..c92a1bc54 100644 --- a/netbox/extras/tests/test_models.py +++ b/netbox/extras/tests/test_models.py @@ -1,6 +1,6 @@ -from django.contrib.contenttypes.models import ContentType from django.test import TestCase +from core.models import ObjectType from dcim.models import Device, DeviceRole, DeviceType, Location, Manufacturer, Platform, Region, Site, SiteGroup from extras.models import ConfigContext, Tag from tenancy.models import Tenant, TenantGroup @@ -22,7 +22,7 @@ class TagTest(TestCase): # Create a Tag that can only be applied to Regions tag = Tag.objects.create(name='Tag 1', slug='tag-1') - tag.object_types.add(ContentType.objects.get_by_natural_key('dcim', 'region')) + tag.object_types.add(ObjectType.objects.get_by_natural_key('dcim', 'region')) # Apply the Tag to a Region region.tags.add(tag) From 01ee9c87b8c1c0e2edc19fcf0f17ce54ce0bdb6a Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 4 Mar 2024 09:52:45 -0500 Subject: [PATCH 20/47] Update ObjectPermission.object_types to reference ObjectType --- netbox/netbox/tests/test_authentication.py | 12 ++--- netbox/netbox/tests/test_import.py | 6 +-- netbox/users/api/serializers.py | 4 +- netbox/users/forms/model_forms.py | 4 +- ...07_objectpermission_update_object_types.py | 19 ++++++++ netbox/users/models.py | 2 +- netbox/users/tests/test_api.py | 6 +-- netbox/users/tests/test_filtersets.py | 7 +-- netbox/utilities/permissions.py | 8 ++-- netbox/utilities/testing/api.py | 21 ++++----- netbox/utilities/testing/views.py | 45 ++++++++++--------- 11 files changed, 78 insertions(+), 56 deletions(-) create mode 100644 netbox/users/migrations/0007_objectpermission_update_object_types.py diff --git a/netbox/netbox/tests/test_authentication.py b/netbox/netbox/tests/test_authentication.py index 6a894edcd..6e049dcaf 100644 --- a/netbox/netbox/tests/test_authentication.py +++ b/netbox/netbox/tests/test_authentication.py @@ -2,13 +2,13 @@ import datetime from django.conf import settings from django.contrib.auth import get_user_model -from django.contrib.contenttypes.models import ContentType from django.test import Client from django.test.utils import override_settings from django.urls import reverse from netaddr import IPNetwork from rest_framework.test import APIClient +from core.models import ObjectType from dcim.models import Site from ipam.models import Prefix from users.models import Group, ObjectPermission, Token @@ -452,7 +452,7 @@ class ObjectPermissionAPIViewTestCase(TestCase): ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(Prefix)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(Prefix)) # Retrieve permitted object url = reverse('ipam-api:prefix-detail', @@ -482,7 +482,7 @@ class ObjectPermissionAPIViewTestCase(TestCase): ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(Prefix)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(Prefix)) # Retrieve all objects. Only permitted objects should be returned. response = self.client.get(url, **self.header) @@ -510,7 +510,7 @@ class ObjectPermissionAPIViewTestCase(TestCase): ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(Prefix)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(Prefix)) # Attempt to create a non-permitted object response = self.client.post(url, data, format='json', **self.header) @@ -541,7 +541,7 @@ class ObjectPermissionAPIViewTestCase(TestCase): ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(Prefix)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(Prefix)) # Attempt to edit a non-permitted object data = {'site': self.sites[0].pk} @@ -581,7 +581,7 @@ class ObjectPermissionAPIViewTestCase(TestCase): ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(Prefix)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(Prefix)) # Attempt to delete a non-permitted object url = reverse('ipam-api:prefix-detail', diff --git a/netbox/netbox/tests/test_import.py b/netbox/netbox/tests/test_import.py index bd07886e8..b0b21a07d 100644 --- a/netbox/netbox/tests/test_import.py +++ b/netbox/netbox/tests/test_import.py @@ -1,6 +1,6 @@ -from django.contrib.contenttypes.models import ContentType from django.test import override_settings +from core.models import ObjectType from dcim.models import * from users.models import ObjectPermission from utilities.choices import CSVDelimiterChoices, ImportFormatChoices @@ -67,7 +67,7 @@ class CSVImportTestCase(ModelViewTestCase): obj_perm = ObjectPermission(name='Test permission', actions=['add']) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Try GET with model-level permission self.assertHttpStatus(self.client.get(self._get_url('import')), 200) @@ -108,7 +108,7 @@ class CSVImportTestCase(ModelViewTestCase): obj_perm = ObjectPermission(name='Test permission', actions=['add']) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Try GET with model-level permission self.assertHttpStatus(self.client.get(self._get_url('import')), 200) diff --git a/netbox/users/api/serializers.py b/netbox/users/api/serializers.py index b9bd55e75..b99371b6c 100644 --- a/netbox/users/api/serializers.py +++ b/netbox/users/api/serializers.py @@ -1,12 +1,12 @@ from django.conf import settings from django.contrib.auth import authenticate from django.contrib.auth import get_user_model -from django.contrib.contenttypes.models import ContentType from drf_spectacular.utils import extend_schema_field from drf_spectacular.types import OpenApiTypes from rest_framework import serializers from rest_framework.exceptions import AuthenticationFailed, PermissionDenied +from core.models import ObjectType from netbox.api.fields import ContentTypeField, IPNetworkSerializer, SerializedPKRelatedField from netbox.api.serializers import ValidatedModelSerializer from users.models import Group, ObjectPermission, Token @@ -161,7 +161,7 @@ class TokenProvisionSerializer(TokenSerializer): class ObjectPermissionSerializer(ValidatedModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='users-api:objectpermission-detail') object_types = ContentTypeField( - queryset=ContentType.objects.all(), + queryset=ObjectType.objects.all(), many=True ) groups = SerializedPKRelatedField( diff --git a/netbox/users/forms/model_forms.py b/netbox/users/forms/model_forms.py index 2a024bf47..6c717d1ea 100644 --- a/netbox/users/forms/model_forms.py +++ b/netbox/users/forms/model_forms.py @@ -1,12 +1,12 @@ from django import forms from django.conf import settings from django.contrib.auth import get_user_model -from django.contrib.contenttypes.models import ContentType from django.contrib.postgres.forms import SimpleArrayField from django.core.exceptions import FieldError from django.utils.html import mark_safe from django.utils.translation import gettext_lazy as _ +from core.models import ObjectType from ipam.formfields import IPNetworkFormField from ipam.validators import prefix_validator from netbox.preferences import PREFERENCES @@ -278,7 +278,7 @@ class GroupForm(forms.ModelForm): class ObjectPermissionForm(forms.ModelForm): object_types = ContentTypeMultipleChoiceField( label=_('Object types'), - queryset=ContentType.objects.all(), + queryset=ObjectType.objects.all(), limit_choices_to=OBJECTPERMISSION_OBJECT_TYPES, widget=forms.SelectMultiple(attrs={'size': 6}) ) diff --git a/netbox/users/migrations/0007_objectpermission_update_object_types.py b/netbox/users/migrations/0007_objectpermission_update_object_types.py new file mode 100644 index 000000000..d3018a602 --- /dev/null +++ b/netbox/users/migrations/0007_objectpermission_update_object_types.py @@ -0,0 +1,19 @@ +# Generated by Django 5.0.1 on 2024-03-04 14:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0010_gfk_indexes'), + ('users', '0006_custom_group_model'), + ] + + operations = [ + migrations.AlterField( + model_name='objectpermission', + name='object_types', + field=models.ManyToManyField(limit_choices_to=models.Q(models.Q(models.Q(('app_label__in', ['account', 'admin', 'auth', 'contenttypes', 'sessions', 'taggit', 'users']), _negated=True), models.Q(('app_label', 'auth'), ('model__in', ['group', 'user'])), models.Q(('app_label', 'users'), ('model__in', ['objectpermission', 'token'])), _connector='OR')), related_name='object_permissions', to='core.objecttype'), + ), + ] diff --git a/netbox/users/models.py b/netbox/users/models.py index 94eb0ad58..d2ee16e5e 100644 --- a/netbox/users/models.py +++ b/netbox/users/models.py @@ -383,7 +383,7 @@ class ObjectPermission(models.Model): default=True ) object_types = models.ManyToManyField( - to='contenttypes.ContentType', + to='core.ObjectType', limit_choices_to=OBJECTPERMISSION_OBJECT_TYPES, related_name='object_permissions' ) diff --git a/netbox/users/tests/test_api.py b/netbox/users/tests/test_api.py index 51fc21c97..2ff3545a6 100644 --- a/netbox/users/tests/test_api.py +++ b/netbox/users/tests/test_api.py @@ -1,7 +1,7 @@ from django.contrib.auth import get_user_model -from django.contrib.contenttypes.models import ContentType from django.urls import reverse +from core.models import ObjectType from users.models import Group, ObjectPermission, Token from utilities.testing import APIViewTestCases, APITestCase, create_test_user from utilities.utils import deepmerge @@ -64,7 +64,7 @@ class UserTest(APIViewTestCases.APIViewTestCase): ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) user_credentials = { 'username': 'user1', @@ -261,7 +261,7 @@ class ObjectPermissionTest( ) User.objects.bulk_create(users) - object_type = ContentType.objects.get(app_label='dcim', model='device') + object_type = ObjectType.objects.get(app_label='dcim', model='device') for i in range(3): objectpermission = ObjectPermission( diff --git a/netbox/users/tests/test_filtersets.py b/netbox/users/tests/test_filtersets.py index 5d373628f..d42133d8d 100644 --- a/netbox/users/tests/test_filtersets.py +++ b/netbox/users/tests/test_filtersets.py @@ -5,6 +5,7 @@ from django.contrib.contenttypes.models import ContentType from django.test import TestCase from django.utils.timezone import make_aware +from core.models import ObjectType from users import filtersets from users.models import Group, ObjectPermission, Token from utilities.testing import BaseFilterSetTests @@ -151,9 +152,9 @@ class ObjectPermissionTestCase(TestCase, BaseFilterSetTests): User.objects.bulk_create(users) object_types = ( - ContentType.objects.get(app_label='dcim', model='site'), - ContentType.objects.get(app_label='dcim', model='rack'), - ContentType.objects.get(app_label='dcim', model='device'), + ObjectType.objects.get(app_label='dcim', model='site'), + ObjectType.objects.get(app_label='dcim', model='rack'), + ObjectType.objects.get(app_label='dcim', model='device'), ) permissions = ( diff --git a/netbox/utilities/permissions.py b/netbox/utilities/permissions.py index c72a72db7..f4b7061ee 100644 --- a/netbox/utilities/permissions.py +++ b/netbox/utilities/permissions.py @@ -1,5 +1,4 @@ from django.conf import settings -from django.contrib.contenttypes.models import ContentType from django.db.models import Q from django.utils.translation import gettext_lazy as _ @@ -50,13 +49,14 @@ def resolve_permission_ct(name): :param name: Permission name in the format ._ """ + from core.models import ObjectType app_label, action, model_name = resolve_permission(name) try: - content_type = ContentType.objects.get(app_label=app_label, model=model_name) - except ContentType.DoesNotExist: + object_type = ObjectType.objects.get(app_label=app_label, model=model_name) + except ObjectType.DoesNotExist: raise ValueError(_("Unknown app_label/model_name for {name}").format(name=name)) - return content_type, action + return object_type, action def permission_is_exempt(name): diff --git a/netbox/utilities/testing/api.py b/netbox/utilities/testing/api.py index 20c607906..f5e12246b 100644 --- a/netbox/utilities/testing/api.py +++ b/netbox/utilities/testing/api.py @@ -10,6 +10,7 @@ from graphene.types import Dynamic as GQLDynamic, List as GQLList, Union as GQLU from rest_framework import status from rest_framework.test import APIClient +from core.models import ObjectType from extras.choices import ObjectChangeActionChoices from extras.models import ObjectChange from users.models import ObjectPermission, Token @@ -109,7 +110,7 @@ class APIViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Try GET to permitted object url = self._get_detail_url(instance1) @@ -183,7 +184,7 @@ class APIViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Try GET to permitted objects response = self.client.get(self._get_list_url(), **self.header) @@ -224,7 +225,7 @@ class APIViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) initial_count = self._get_queryset().count() response = self.client.post(self._get_list_url(), self.create_data[0], format='json', **self.header) @@ -258,7 +259,7 @@ class APIViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) initial_count = self._get_queryset().count() response = self.client.post(self._get_list_url(), self.create_data, format='json', **self.header) @@ -309,7 +310,7 @@ class APIViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) response = self.client.patch(url, update_data, format='json', **self.header) self.assertHttpStatus(response, status.HTTP_200_OK) @@ -344,7 +345,7 @@ class APIViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) id_list = list(self._get_queryset().values_list('id', flat=True)[:3]) self.assertEqual(len(id_list), 3, "Insufficient number of objects to test bulk update") @@ -387,7 +388,7 @@ class APIViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) response = self.client.delete(url, **self.header) self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT) @@ -413,7 +414,7 @@ class APIViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Target the three most recently created objects to avoid triggering recursive deletions # (e.g. with MPTT objects) @@ -504,7 +505,7 @@ class APIViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) response = self.client.post(url, data={'query': query}, **self.header) self.assertHttpStatus(response, status.HTTP_200_OK) @@ -529,7 +530,7 @@ class APIViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) response = self.client.post(url, data={'query': query}, **self.header) self.assertHttpStatus(response, status.HTTP_200_OK) diff --git a/netbox/utilities/testing/views.py b/netbox/utilities/testing/views.py index daa44b905..7bc776b1e 100644 --- a/netbox/utilities/testing/views.py +++ b/netbox/utilities/testing/views.py @@ -8,6 +8,7 @@ from django.test import override_settings from django.urls import reverse from django.utils.translation import gettext as _ +from core.models import ObjectType from extras.choices import ObjectChangeActionChoices from extras.models import ObjectChange from netbox.models.features import ChangeLoggingMixin @@ -93,7 +94,7 @@ class ViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Try GET with model-level permission self.assertHttpStatus(self.client.get(instance.get_absolute_url()), 200) @@ -109,7 +110,7 @@ class ViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Try GET to permitted object self.assertHttpStatus(self.client.get(instance1.get_absolute_url()), 200) @@ -161,7 +162,7 @@ class ViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Try GET with model-level permission self.assertHttpStatus(self.client.get(self._get_url('add')), 200) @@ -197,7 +198,7 @@ class ViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Try GET with object-level permission self.assertHttpStatus(self.client.get(self._get_url('add')), 200) @@ -260,7 +261,7 @@ class ViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Try GET with model-level permission self.assertHttpStatus(self.client.get(self._get_url('edit', instance)), 200) @@ -295,7 +296,7 @@ class ViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Try GET with a permitted object self.assertHttpStatus(self.client.get(self._get_url('edit', instance1)), 200) @@ -349,7 +350,7 @@ class ViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Try GET with model-level permission self.assertHttpStatus(self.client.get(self._get_url('delete', instance)), 200) @@ -384,7 +385,7 @@ class ViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Try GET with a permitted object self.assertHttpStatus(self.client.get(self._get_url('delete', instance1)), 200) @@ -442,7 +443,7 @@ class ViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Try GET with model-level permission self.assertHttpStatus(self.client.get(self._get_url('list')), 200) @@ -458,7 +459,7 @@ class ViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Try GET with object-level permission response = self.client.get(self._get_url('list')) @@ -477,7 +478,7 @@ class ViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Test default CSV export response = self.client.get(f'{url}?export') @@ -524,7 +525,7 @@ class ViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Bulk create objects response = self.client.post(**request) @@ -548,7 +549,7 @@ class ViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Attempt to make the request with unmet constraints self.assertHttpStatus(self.client.post(**request), 200) @@ -610,7 +611,7 @@ class ViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Try GET with model-level permission self.assertHttpStatus(self.client.get(self._get_url('import')), 200) @@ -639,7 +640,7 @@ class ViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Test POST with permission self.assertHttpStatus(self.client.post(self._get_url('import'), data), 302) @@ -674,7 +675,7 @@ class ViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Attempt to import non-permitted objects self.assertHttpStatus(self.client.post(self._get_url('import'), data), 200) @@ -730,7 +731,7 @@ class ViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Try POST with model-level permission self.assertHttpStatus(self.client.post(self._get_url('bulk_edit'), data), 302) @@ -761,7 +762,7 @@ class ViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Attempt to bulk edit permitted objects into a non-permitted state response = self.client.post(self._get_url('bulk_edit'), data) @@ -811,7 +812,7 @@ class ViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Try POST with model-level permission self.assertHttpStatus(self.client.post(self._get_url('bulk_delete'), data), 302) @@ -833,7 +834,7 @@ class ViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Attempt to bulk delete non-permitted objects initial_count = self._get_queryset().count() @@ -891,7 +892,7 @@ class ViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Try POST with model-level permission self.assertHttpStatus(self.client.post(self._get_url('bulk_rename'), data), 302) @@ -916,7 +917,7 @@ class ViewTestCases: ) obj_perm.save() obj_perm.users.add(self.user) - obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model)) # Attempt to bulk edit permitted objects into a non-permitted state response = self.client.post(self._get_url('bulk_rename'), data) From d5380100698cfc6a6165fcbd8cdb4e27f096e67a Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 4 Mar 2024 10:06:28 -0500 Subject: [PATCH 21/47] Add GraphQL type for ObjectType --- netbox/extras/graphql/types.py | 12 ++++++------ netbox/netbox/graphql/types.py | 10 ++++++++++ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/netbox/extras/graphql/types.py b/netbox/extras/graphql/types.py index 65819a75a..d99e54976 100644 --- a/netbox/extras/graphql/types.py +++ b/netbox/extras/graphql/types.py @@ -39,7 +39,7 @@ class CustomFieldType(ObjectType): class Meta: model = models.CustomField - exclude = ('object_types', 'object_type') + fields = '__all__' filterset_class = filtersets.CustomFieldFilterSet @@ -55,7 +55,7 @@ class CustomLinkType(ObjectType): class Meta: model = models.CustomLink - exclude = ('object_types', ) + fields = '__all__' filterset_class = filtersets.CustomLinkFilterSet @@ -63,7 +63,7 @@ class EventRuleType(OrganizationalObjectType): class Meta: model = models.EventRule - exclude = ('object_types',) + fields = '__all__' filterset_class = filtersets.EventRuleFilterSet @@ -71,7 +71,7 @@ class ExportTemplateType(ObjectType): class Meta: model = models.ExportTemplate - exclude = ('object_types', ) + fields = '__all__' filterset_class = filtersets.ExportTemplateFilterSet @@ -103,7 +103,7 @@ class SavedFilterType(ObjectType): class Meta: model = models.SavedFilter - exclude = ('object_types', ) + fields = '__all__' filterset_class = filtersets.SavedFilterFilterSet @@ -111,7 +111,7 @@ class TagType(ObjectType): class Meta: model = models.Tag - exclude = ('object_types', 'extras_taggeditem_items',) + exclude = ('extras_taggeditem_items',) filterset_class = filtersets.TagFilterSet diff --git a/netbox/netbox/graphql/types.py b/netbox/netbox/graphql/types.py index 10847742b..f131f07cf 100644 --- a/netbox/netbox/graphql/types.py +++ b/netbox/netbox/graphql/types.py @@ -1,5 +1,6 @@ import graphene +from core.models import ObjectType as ObjectType_ from django.contrib.contenttypes.models import ContentType from extras.graphql.mixins import ( ChangelogMixin, @@ -11,7 +12,9 @@ from graphene_django import DjangoObjectType __all__ = ( 'BaseObjectType', + 'ContentTypeType', 'ObjectType', + 'ObjectTypeType', 'OrganizationalObjectType', 'NetBoxObjectType', ) @@ -90,3 +93,10 @@ class ContentTypeType(DjangoObjectType): class Meta: model = ContentType fields = ('id', 'app_label', 'model') + + +class ObjectTypeType(DjangoObjectType): + + class Meta: + model = ObjectType_ + fields = ('id', 'app_label', 'model') From 0419a69ae8236f367cae26bf8fca1eed7fe2b7d5 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 4 Mar 2024 10:46:34 -0500 Subject: [PATCH 22/47] Clean up outdated references to ContentType --- netbox/dcim/tests/test_views.py | 2 -- netbox/extras/api/customfields.py | 10 +++++----- netbox/extras/signals.py | 4 ++-- netbox/extras/tests/test_filtersets.py | 14 ++++++-------- netbox/extras/tests/test_forms.py | 7 +++---- netbox/netbox/api/serializers/features.py | 2 -- netbox/netbox/api/viewsets/mixins.py | 5 ++--- netbox/netbox/forms/base.py | 11 ++++++----- netbox/netbox/forms/mixins.py | 7 +++---- netbox/netbox/search/backends.py | 16 ++++++++-------- netbox/netbox/views/generic/bulk_views.py | 6 +++--- netbox/tenancy/tests/test_filtersets.py | 4 ++-- netbox/users/api/nested_serializers.py | 4 ++-- netbox/users/tests/test_filtersets.py | 3 +-- netbox/users/tests/test_views.py | 7 +++---- netbox/utilities/templatetags/helpers.py | 8 ++++---- netbox/utilities/tests/test_counters.py | 4 +--- 17 files changed, 51 insertions(+), 63 deletions(-) diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index 53d7f3d34..e9e5a557b 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -3,7 +3,6 @@ from zoneinfo import ZoneInfo import yaml from django.contrib.auth import get_user_model -from django.contrib.contenttypes.models import ContentType from django.test import override_settings from django.urls import reverse from netaddr import EUI @@ -2982,7 +2981,6 @@ class CableTestCase( tags = create_tags('Alpha', 'Bravo', 'Charlie') - interface_ct = ContentType.objects.get_for_model(Interface) cls.form_data = { # TODO: Revisit this limitation # Changing terminations not supported when editing an existing Cable diff --git a/netbox/extras/api/customfields.py b/netbox/extras/api/customfields.py index 77c3a170e..7ecee01f8 100644 --- a/netbox/extras/api/customfields.py +++ b/netbox/extras/api/customfields.py @@ -1,10 +1,10 @@ -from django.contrib.contenttypes.models import ContentType from django.utils.translation import gettext as _ from drf_spectacular.utils import extend_schema_field from drf_spectacular.types import OpenApiTypes from rest_framework.fields import Field from rest_framework.serializers import ValidationError +from core.models import ObjectType from extras.choices import CustomFieldTypeChoices from extras.models import CustomField from netbox.constants import NESTED_SERIALIZER_PREFIX @@ -25,8 +25,8 @@ class CustomFieldDefaultValues: self.model = serializer_field.parent.Meta.model # Retrieve the CustomFields for the parent model - content_type = ContentType.objects.get_for_model(self.model) - fields = CustomField.objects.filter(object_types=content_type) + object_type = ObjectType.objects.get_for_model(self.model) + fields = CustomField.objects.filter(object_types=object_type) # Populate the default value for each CustomField value = {} @@ -47,8 +47,8 @@ class CustomFieldsDataField(Field): Cache CustomFields assigned to this model to avoid redundant database queries """ if not hasattr(self, '_custom_fields'): - content_type = ContentType.objects.get_for_model(self.parent.Meta.model) - self._custom_fields = CustomField.objects.filter(object_types=content_type) + object_type = ObjectType.objects.get_for_model(self.parent.Meta.model) + self._custom_fields = CustomField.objects.filter(object_types=object_type) return self._custom_fields def to_representation(self, obj): diff --git a/netbox/extras/signals.py b/netbox/extras/signals.py index cacc5a83a..833ce0036 100644 --- a/netbox/extras/signals.py +++ b/netbox/extras/signals.py @@ -257,7 +257,7 @@ def process_job_start_event_rules(sender, **kwargs): """ Process event rules for jobs starting. """ - event_rules = EventRule.objects.filter(type_job_start=True, enabled=True, content_types=sender.object_type) + event_rules = EventRule.objects.filter(type_job_start=True, enabled=True, object_types=sender.object_type) username = sender.user.username if sender.user else None process_event_rules(event_rules, sender.object_type.model, EVENT_JOB_START, sender.data, username) @@ -267,6 +267,6 @@ def process_job_end_event_rules(sender, **kwargs): """ Process event rules for jobs terminating. """ - event_rules = EventRule.objects.filter(type_job_end=True, enabled=True, content_types=sender.object_type) + event_rules = EventRule.objects.filter(type_job_end=True, enabled=True, object_types=sender.object_type) username = sender.user.username if sender.user else None process_event_rules(event_rules, sender.object_type.model, EVENT_JOB_END, sender.data, username) diff --git a/netbox/extras/tests/test_filtersets.py b/netbox/extras/tests/test_filtersets.py index 7be00a5a3..cec0ffe94 100644 --- a/netbox/extras/tests/test_filtersets.py +++ b/netbox/extras/tests/test_filtersets.py @@ -175,8 +175,6 @@ class WebhookTestCase(TestCase, BaseFilterSetTests): @classmethod def setUpTestData(cls): - content_types = ContentType.objects.filter(model__in=['region', 'site', 'rack', 'location', 'device']) - webhooks = ( Webhook( name='Webhook 1', @@ -355,7 +353,7 @@ class EventRuleTestCase(TestCase, BaseFilterSetTests): def test_object_types(self): params = {'object_types': 'dcim.region'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) - params = {'object_types_id': [ContentType.objects.get_for_model(Region).pk]} + params = {'object_types_id': [ObjectType.objects.get_for_model(Region).pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) def test_action_type(self): @@ -440,7 +438,7 @@ class CustomLinkTestCase(TestCase, BaseFilterSetTests): def test_object_types(self): params = {'object_types': 'dcim.site'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) - params = {'object_types_id': [ContentType.objects.get_for_model(Site).pk]} + params = {'object_types_id': [ObjectType.objects.get_for_model(Site).pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) def test_weight(self): @@ -530,7 +528,7 @@ class SavedFilterTestCase(TestCase, BaseFilterSetTests): def test_object_types(self): params = {'object_types': 'dcim.site'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) - params = {'object_types_id': [ContentType.objects.get_for_model(Site).pk]} + params = {'object_types_id': [ObjectType.objects.get_for_model(Site).pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) def test_user(self): @@ -661,7 +659,7 @@ class ExportTemplateTestCase(TestCase, BaseFilterSetTests): def test_object_types(self): params = {'object_types': 'dcim.site'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) - params = {'object_types_id': [ContentType.objects.get_for_model(Site).pk]} + params = {'object_types_id': [ObjectType.objects.get_for_model(Site).pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) def test_description(self): @@ -1164,12 +1162,12 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_object_types(self): - params = {'for_object_type_id': [ContentType.objects.get_by_natural_key('dcim', 'site').pk]} + params = {'for_object_type_id': [ObjectType.objects.get_by_natural_key('dcim', 'site').pk]} self.assertEqual( list(self.filterset(params, self.queryset).qs.values_list('name', flat=True)), ['Tag 1', 'Tag 3'] ) - params = {'for_object_type_id': [ContentType.objects.get_by_natural_key('circuits', 'provider').pk]} + params = {'for_object_type_id': [ObjectType.objects.get_by_natural_key('circuits', 'provider').pk]} self.assertEqual( list(self.filterset(params, self.queryset).qs.values_list('name', flat=True)), ['Tag 2', 'Tag 3'] diff --git a/netbox/extras/tests/test_forms.py b/netbox/extras/tests/test_forms.py index 7642ee2a8..4c96e72d6 100644 --- a/netbox/extras/tests/test_forms.py +++ b/netbox/extras/tests/test_forms.py @@ -1,4 +1,3 @@ -from django.contrib.contenttypes.models import ContentType from django.test import TestCase from core.models import ObjectType @@ -63,14 +62,14 @@ class CustomFieldModelFormTest(TestCase): cf_object = CustomField.objects.create( name='object', type=CustomFieldTypeChoices.TYPE_OBJECT, - object_type=ContentType.objects.get_for_model(Site) + object_type=ObjectType.objects.get_for_model(Site) ) cf_object.object_types.set([object_type]) cf_multiobject = CustomField.objects.create( name='multiobject', type=CustomFieldTypeChoices.TYPE_MULTIOBJECT, - object_type=ContentType.objects.get_for_model(Site) + object_type=ObjectType.objects.get_for_model(Site) ) cf_multiobject.object_types.set([object_type]) @@ -100,7 +99,7 @@ class SavedFilterFormTest(TestCase): form = SavedFilterForm({ 'name': 'test-sf', 'slug': 'test-sf', - 'object_types': [ContentType.objects.get_for_model(Site).pk], + 'object_types': [ObjectType.objects.get_for_model(Site).pk], 'weight': 100, 'parameters': { "status": [ diff --git a/netbox/netbox/api/serializers/features.py b/netbox/netbox/api/serializers/features.py index 1374ba526..3bd5c8a2d 100644 --- a/netbox/netbox/api/serializers/features.py +++ b/netbox/netbox/api/serializers/features.py @@ -1,9 +1,7 @@ -from django.contrib.contenttypes.models import ContentType from rest_framework import serializers from rest_framework.fields import CreateOnlyDefault from extras.api.customfields import CustomFieldsDataField, CustomFieldDefaultValues -from extras.models import CustomField from .nested import NestedTagSerializer __all__ = ( diff --git a/netbox/netbox/api/viewsets/mixins.py b/netbox/netbox/api/viewsets/mixins.py index 7df498ac6..e07e2c78b 100644 --- a/netbox/netbox/api/viewsets/mixins.py +++ b/netbox/netbox/api/viewsets/mixins.py @@ -1,4 +1,3 @@ -from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist from django.db import transaction from django.http import Http404 @@ -41,8 +40,8 @@ class ExportTemplatesMixin: """ def list(self, request, *args, **kwargs): if 'export' in request.GET: - content_type = ContentType.objects.get_for_model(self.get_serializer_class().Meta.model) - et = ExportTemplate.objects.filter(content_types=content_type, name=request.GET['export']).first() + object_type = ObjectType.objects.get_for_model(self.get_serializer_class().Meta.model) + et = ExportTemplate.objects.filter(object_types=object_type, name=request.GET['export']).first() if et is None: raise Http404 queryset = self.filter_queryset(self.get_queryset()) diff --git a/netbox/netbox/forms/base.py b/netbox/netbox/forms/base.py index 12de3b12b..85064e79d 100644 --- a/netbox/netbox/forms/base.py +++ b/netbox/netbox/forms/base.py @@ -3,6 +3,7 @@ from django.contrib.contenttypes.models import ContentType from django.db.models import Q from django.utils.translation import gettext_lazy as _ +from core.models import ObjectType from extras.choices import * from extras.models import CustomField, Tag from utilities.forms import CSVModelForm @@ -129,9 +130,9 @@ class NetBoxModelBulkEditForm(CustomFieldsMixin, forms.Form): self.fields['pk'].queryset = self.model.objects.all() # Restrict tag fields by model - ct = ContentType.objects.get_for_model(self.model) - self.fields['add_tags'].widget.add_query_param('for_object_type_id', ct.pk) - self.fields['remove_tags'].widget.add_query_param('for_object_type_id', ct.pk) + object_type = ObjectType.objects.get_for_model(self.model) + self.fields['add_tags'].widget.add_query_param('for_object_type_id', object_type.pk) + self.fields['remove_tags'].widget.add_query_param('for_object_type_id', object_type.pk) self._extend_nullable_fields() @@ -169,9 +170,9 @@ class NetBoxModelFilterSetForm(CustomFieldsMixin, SavedFiltersMixin, forms.Form) super().__init__(*args, **kwargs) # Limit saved filters to those applicable to the form's model - content_type = ContentType.objects.get_for_model(self.model) + object_type = ObjectType.objects.get_for_model(self.model) self.fields['filter_id'].widget.add_query_params({ - 'content_type_id': content_type.pk, + 'object_types_id': object_type.pk, }) def _get_custom_fields(self, content_type): diff --git a/netbox/netbox/forms/mixins.py b/netbox/netbox/forms/mixins.py index 2f903db5d..6b1f31265 100644 --- a/netbox/netbox/forms/mixins.py +++ b/netbox/netbox/forms/mixins.py @@ -1,5 +1,4 @@ from django import forms -from django.contrib.contenttypes.models import ContentType from django.utils.translation import gettext as _ from core.models import ObjectType @@ -86,6 +85,6 @@ class TagsMixin(forms.Form): super().__init__(*args, **kwargs) # Limit tags to those applicable to the object type - content_type = ContentType.objects.get_for_model(self._meta.model) - if content_type and hasattr(self.fields['tags'].widget, 'add_query_param'): - self.fields['tags'].widget.add_query_param('for_object_type_id', content_type.pk) + object_type = ObjectType.objects.get_for_model(self._meta.model) + if object_type and hasattr(self.fields['tags'].widget, 'add_query_param'): + self.fields['tags'].widget.add_query_param('for_object_type_id', object_type.pk) diff --git a/netbox/netbox/search/backends.py b/netbox/netbox/search/backends.py index 0d7667087..a9e867b9f 100644 --- a/netbox/netbox/search/backends.py +++ b/netbox/netbox/search/backends.py @@ -131,11 +131,11 @@ class CachedValueSearchBackend(SearchBackend): ) )[:MAX_RESULTS] - # Gather all ContentTypes present in the search results (used for prefetching related + # Gather all ObjectTypes present in the search results (used for prefetching related # objects). This must be done before generating the final results list, which returns # a RawQuerySet. - content_type_ids = set(queryset.values_list('object_type', flat=True)) - object_types = ObjectType.objects.filter(pk__in=content_type_ids) + object_type_ids = set(queryset.values_list('object_type', flat=True)) + object_types = ObjectType.objects.filter(pk__in=object_type_ids) # Construct a Prefetch to pre-fetch only those related objects for which the # user has permission to view. @@ -152,11 +152,11 @@ class CachedValueSearchBackend(SearchBackend): params ) - # Iterate through each ContentType represented in the search results and prefetch any + # Iterate through each ObjectType represented in the search results and prefetch any # related objects necessary to render the prescribed display attributes (display_attrs). - for ct in object_types: - model = ct.model_class() - indexer = registry['search'].get(content_type_identifier(ct)) + for object_type in object_types: + model = object_type.model_class() + indexer = registry['search'].get(content_type_identifier(object_type)) if not (display_attrs := getattr(indexer, 'display_attrs', None)): continue @@ -170,7 +170,7 @@ class CachedValueSearchBackend(SearchBackend): # Compile a list of all CachedValues referencing this object type, and prefetch # any related objects if prefetch_fields: - objects = [r for r in results if r.object_type == ct] + objects = [r for r in results if r.object_type == object_type] prefetch_related_objects(objects, *prefetch_fields) # Omit any results pertaining to an object the user does not have permission to view diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py index f1f4e90dd..022059e51 100644 --- a/netbox/netbox/views/generic/bulk_views.py +++ b/netbox/netbox/views/generic/bulk_views.py @@ -4,7 +4,6 @@ from copy import deepcopy from django.contrib import messages from django.contrib.contenttypes.fields import GenericRel -from django.contrib.contenttypes.models import ContentType from django.core.exceptions import FieldDoesNotExist, ObjectDoesNotExist, ValidationError from django.db import transaction, IntegrityError from django.db.models import ManyToManyField, ProtectedError, RestrictedError @@ -17,6 +16,7 @@ from django.utils.safestring import mark_safe from django.utils.translation import gettext as _ from django_tables2.export import TableExport +from core.models import ObjectType from extras.models import ExportTemplate from extras.signals import clear_events from utilities.error_handlers import handle_protectederror @@ -124,7 +124,7 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin): request: The current request """ model = self.queryset.model - content_type = ContentType.objects.get_for_model(model) + object_type = ObjectType.objects.get_for_model(model) if self.filterset: self.queryset = self.filterset(request.GET, self.queryset, request=request).qs @@ -143,7 +143,7 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin): # Render an ExportTemplate elif request.GET['export']: - template = get_object_or_404(ExportTemplate, content_types=content_type, name=request.GET['export']) + template = get_object_or_404(ExportTemplate, object_types=object_type, name=request.GET['export']) return self.export_template(template, request) # Check for YAML export support on the model diff --git a/netbox/tenancy/tests/test_filtersets.py b/netbox/tenancy/tests/test_filtersets.py index 729dd7204..3bcbddd4b 100644 --- a/netbox/tenancy/tests/test_filtersets.py +++ b/netbox/tenancy/tests/test_filtersets.py @@ -1,6 +1,6 @@ -from django.contrib.contenttypes.models import ContentType from django.test import TestCase +from core.models import ObjectType from dcim.models import Manufacturer, Site from tenancy.filtersets import * from tenancy.models import * @@ -296,7 +296,7 @@ class ContactAssignmentTestCase(TestCase, ChangeLoggedFilterSetTests): ContactAssignment.objects.bulk_create(assignments) def test_object_type(self): - params = {'object_type_id': ContentType.objects.get_by_natural_key('dcim', 'site')} + params = {'object_type_id': ObjectType.objects.get_by_natural_key('dcim', 'site')} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) def test_contact(self): diff --git a/netbox/users/api/nested_serializers.py b/netbox/users/api/nested_serializers.py index 552c24906..2ab5d3aa5 100644 --- a/netbox/users/api/nested_serializers.py +++ b/netbox/users/api/nested_serializers.py @@ -1,9 +1,9 @@ from django.contrib.auth import get_user_model -from django.contrib.contenttypes.models import ContentType from drf_spectacular.utils import extend_schema_field from drf_spectacular.types import OpenApiTypes from rest_framework import serializers +from core.models import ObjectType from netbox.api.fields import ContentTypeField from netbox.api.serializers import WritableNestedSerializer from users.models import Group, ObjectPermission, Token @@ -49,7 +49,7 @@ class NestedTokenSerializer(WritableNestedSerializer): class NestedObjectPermissionSerializer(WritableNestedSerializer): url = serializers.HyperlinkedIdentityField(view_name='users-api:objectpermission-detail') object_types = ContentTypeField( - queryset=ContentType.objects.all(), + queryset=ObjectType.objects.all(), many=True ) groups = serializers.SerializerMethodField(read_only=True) diff --git a/netbox/users/tests/test_filtersets.py b/netbox/users/tests/test_filtersets.py index d42133d8d..5930285a9 100644 --- a/netbox/users/tests/test_filtersets.py +++ b/netbox/users/tests/test_filtersets.py @@ -1,7 +1,6 @@ import datetime from django.contrib.auth import get_user_model -from django.contrib.contenttypes.models import ContentType from django.test import TestCase from django.utils.timezone import make_aware @@ -199,7 +198,7 @@ class ObjectPermissionTestCase(TestCase, BaseFilterSetTests): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_object_types(self): - object_types = ContentType.objects.filter(model__in=['site', 'rack']) + object_types = ObjectType.objects.filter(model__in=['site', 'rack']) params = {'object_types': [object_types[0].pk, object_types[1].pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) diff --git a/netbox/users/tests/test_views.py b/netbox/users/tests/test_views.py index 27d2aeab1..588730dbd 100644 --- a/netbox/users/tests/test_views.py +++ b/netbox/users/tests/test_views.py @@ -1,5 +1,4 @@ -from django.contrib.contenttypes.models import ContentType - +from core.models import ObjectType from users.models import * from utilities.testing import ViewTestCases, create_test_user @@ -115,7 +114,7 @@ class ObjectPermissionTestCase( @classmethod def setUpTestData(cls): - ct = ContentType.objects.get_by_natural_key('dcim', 'site') + object_type = ObjectType.objects.get_by_natural_key('dcim', 'site') permissions = ( ObjectPermission(name='Permission 1', actions=['view', 'add', 'delete']), @@ -127,7 +126,7 @@ class ObjectPermissionTestCase( cls.form_data = { 'name': 'Permission X', 'description': 'A new permission', - 'object_types': [ct.pk], + 'object_types': [object_type.pk], 'actions': 'view,edit,delete', } diff --git a/netbox/utilities/templatetags/helpers.py b/netbox/utilities/templatetags/helpers.py index aaee9679c..b71848411 100644 --- a/netbox/utilities/templatetags/helpers.py +++ b/netbox/utilities/templatetags/helpers.py @@ -1,16 +1,16 @@ import datetime import json -from urllib.parse import quote from typing import Dict, Any +from urllib.parse import quote from django import template from django.conf import settings -from django.contrib.contenttypes.models import ContentType from django.template.defaultfilters import date from django.urls import NoReverseMatch, reverse from django.utils import timezone from django.utils.safestring import mark_safe +from core.models import ObjectType from utilities.forms import get_selected_values, TableConfigForm from utilities.utils import get_viewname @@ -322,10 +322,10 @@ def applied_filters(context, model, form, query_params): save_link = None if user.has_perm('extras.add_savedfilter') and 'filter_id' not in context['request'].GET: - content_type = ContentType.objects.get_for_model(model).pk + object_type = ObjectType.objects.get_for_model(model).pk parameters = json.dumps(dict(context['request'].GET.lists())) url = reverse('extras:savedfilter_add') - save_link = f"{url}?content_types={content_type}¶meters={quote(parameters)}" + save_link = f"{url}?object_types={object_type}¶meters={quote(parameters)}" return { 'applied_filters': applied_filters, diff --git a/netbox/utilities/tests/test_counters.py b/netbox/utilities/tests/test_counters.py index 014c758e9..b87e73ace 100644 --- a/netbox/utilities/tests/test_counters.py +++ b/netbox/utilities/tests/test_counters.py @@ -1,11 +1,9 @@ -from django.contrib.contenttypes.models import ContentType from django.test import override_settings from django.urls import reverse from dcim.models import * -from users.models import ObjectPermission from utilities.testing.base import TestCase -from utilities.testing.utils import create_test_device, create_test_user +from utilities.testing.utils import create_test_device class CountersTest(TestCase): From 0e89f4660143de4b1a2a5f5375d16eba26feb9fa Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 4 Mar 2024 11:49:39 -0500 Subject: [PATCH 23/47] #15277: Clean up references to object types in templates --- netbox/extras/models/customfields.py | 2 +- netbox/extras/models/models.py | 6 +++--- netbox/extras/tables/tables.py | 2 +- netbox/extras/views.py | 6 +++--- netbox/templates/extras/exporttemplate.html | 5 ----- netbox/templates/inc/panels/image_attachments.html | 2 +- netbox/templates/tenancy/object_contacts.html | 2 +- 7 files changed, 10 insertions(+), 15 deletions(-) diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py index 311ccce76..681bd4f2a 100644 --- a/netbox/extras/models/customfields.py +++ b/netbox/extras/models/customfields.py @@ -209,7 +209,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): objects = CustomFieldManager() clone_fields = ( - 'content_types', 'type', 'object_type', 'group_name', 'description', 'required', 'search_weight', + 'object_types', 'type', 'object_type', 'group_name', 'description', 'required', 'search_weight', 'filter_logic', 'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choice_set', 'ui_visible', 'ui_editable', 'is_cloneable', ) diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index 4a57c6ada..b55aaa11d 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -359,7 +359,7 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): ) clone_fields = ( - 'content_types', 'enabled', 'weight', 'group_name', 'button_class', 'new_window', + 'object_types', 'enabled', 'weight', 'group_name', 'button_class', 'new_window', ) class Meta: @@ -448,7 +448,7 @@ class ExportTemplate(SyncedDataMixin, CloningMixin, ExportTemplatesMixin, Change ) clone_fields = ( - 'content_types', 'template_code', 'mime_type', 'file_extension', 'as_attachment', + 'object_types', 'template_code', 'mime_type', 'file_extension', 'as_attachment', ) class Meta: @@ -561,7 +561,7 @@ class SavedFilter(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): ) clone_fields = ( - 'content_types', 'weight', 'enabled', 'parameters', + 'object_types', 'weight', 'enabled', 'parameters', ) class Meta: diff --git a/netbox/extras/tables/tables.py b/netbox/extras/tables/tables.py index 5bf4f1892..fee0c9f29 100644 --- a/netbox/extras/tables/tables.py +++ b/netbox/extras/tables/tables.py @@ -314,7 +314,7 @@ class EventRuleTable(NetBoxTable): 'last_updated', ) default_columns = ( - 'pk', 'name', 'enabled', 'action_type', 'action_object', 'content_types', 'type_create', 'type_update', + 'pk', 'name', 'enabled', 'action_type', 'action_object', 'object_types', 'type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end', ) diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 49c6dcab3..73fdb6b83 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -762,8 +762,8 @@ class ImageAttachmentEditView(generic.ObjectEditView): def alter_object(self, instance, request, args, kwargs): if not instance.pk: # Assign the parent object based on URL kwargs - content_type = get_object_or_404(ContentType, pk=request.GET.get('content_type')) - instance.parent = get_object_or_404(content_type.model_class(), pk=request.GET.get('object_id')) + object_type = get_object_or_404(ContentType, pk=request.GET.get('object_type')) + instance.parent = get_object_or_404(object_type.model_class(), pk=request.GET.get('object_id')) return instance def get_return_url(self, request, obj=None): @@ -771,7 +771,7 @@ class ImageAttachmentEditView(generic.ObjectEditView): def get_extra_addanother_params(self, request): return { - 'content_type': request.GET.get('content_type'), + 'object_type': request.GET.get('object_type'), 'object_id': request.GET.get('object_id'), } diff --git a/netbox/templates/extras/exporttemplate.html b/netbox/templates/extras/exporttemplate.html index 16dd49ee2..8d14e3ffb 100644 --- a/netbox/templates/extras/exporttemplate.html +++ b/netbox/templates/extras/exporttemplate.html @@ -5,11 +5,6 @@ {% block title %}{{ object.name }}{% endblock %} -{% block breadcrumbs %} - {{ block.super }} - -{% endblock %} - {% block content %}
    diff --git a/netbox/templates/inc/panels/image_attachments.html b/netbox/templates/inc/panels/image_attachments.html index a5f2ac18f..c3c7cf7e3 100644 --- a/netbox/templates/inc/panels/image_attachments.html +++ b/netbox/templates/inc/panels/image_attachments.html @@ -6,7 +6,7 @@ {% trans "Images" %} {% if perms.extras.add_imageattachment %} diff --git a/netbox/templates/tenancy/object_contacts.html b/netbox/templates/tenancy/object_contacts.html index 4b78e53d3..304972e4a 100644 --- a/netbox/templates/tenancy/object_contacts.html +++ b/netbox/templates/tenancy/object_contacts.html @@ -5,7 +5,7 @@ {% block extra_controls %} {% if perms.tenancy.add_contactassignment %} {% with viewname=object|viewname:"contacts" %} - + {% trans "Add a contact" %} {% endwith %} From 4533c8dae069fc98324ae92edd333a888df65540 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 4 Mar 2024 12:17:32 -0500 Subject: [PATCH 24/47] Rename sequences for ObjectType M2M tables --- .../migrations/0111_rename_content_types.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/netbox/extras/migrations/0111_rename_content_types.py b/netbox/extras/migrations/0111_rename_content_types.py index 3d6529692..7b0fa9459 100644 --- a/netbox/extras/migrations/0111_rename_content_types.py +++ b/netbox/extras/migrations/0111_rename_content_types.py @@ -26,6 +26,9 @@ class Migration(migrations.Migration): name='object_type', field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='core.objecttype'), ), + migrations.RunSQL( + "ALTER TABLE extras_customfield_content_types_id_seq RENAME TO extras_customfield_object_types_id_seq" + ), # Custom links migrations.RenameField( @@ -38,6 +41,9 @@ class Migration(migrations.Migration): name='object_types', field=models.ManyToManyField(related_name='custom_links', to='core.objecttype'), ), + migrations.RunSQL( + "ALTER TABLE extras_customlink_content_types_id_seq RENAME TO extras_customlink_object_types_id_seq" + ), # Event rules migrations.RenameField( @@ -50,6 +56,9 @@ class Migration(migrations.Migration): name='object_types', field=models.ManyToManyField(related_name='event_rules', to='core.objecttype'), ), + migrations.RunSQL( + "ALTER TABLE extras_eventrule_content_types_id_seq RENAME TO extras_eventrule_object_types_id_seq" + ), # Export templates migrations.RenameField( @@ -62,6 +71,9 @@ class Migration(migrations.Migration): name='object_types', field=models.ManyToManyField(related_name='export_templates', to='core.objecttype'), ), + migrations.RunSQL( + "ALTER TABLE extras_exporttemplate_content_types_id_seq RENAME TO extras_exporttemplate_object_types_id_seq" + ), # Saved filters migrations.RenameField( @@ -74,6 +86,9 @@ class Migration(migrations.Migration): name='object_types', field=models.ManyToManyField(related_name='saved_filters', to='core.objecttype'), ), + migrations.RunSQL( + "ALTER TABLE extras_savedfilter_content_types_id_seq RENAME TO extras_savedfilter_object_types_id_seq" + ), # Image attachments migrations.RemoveIndex( From 239d21870b00a3336549806773a25d888039fba4 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 4 Mar 2024 15:55:01 -0500 Subject: [PATCH 25/47] Closes #14871: Complete work on UI cleanup (#15341) * Fix left padding of login button in top menu * Relocate "add" buttons for embedded object tables * Remove unused data template block & getNetboxData() utility function * Remove bottom margin from last

    element in rendered Markdown inside a table cell * Prevent TomSelect from initializing on elements with statically-defined options export function initStaticSelects(): void { for (const select of getElements( - 'select:not(.api-select):not(.color-select)', + 'select:not(.tomselected):not(.no-ts):not([size]):not(.api-select):not(.color-select)', )) { new TomSelect(select, { ...config, @@ -24,7 +24,7 @@ export function initColorSelects(): void { )}"> ${escape(item.text)}

    `; } - for (const select of getElements('select.color-select')) { + for (const select of getElements('select.color-select:not(.tomselected)')) { new TomSelect(select, { ...config, maxOptions: undefined, diff --git a/netbox/project-static/src/util.ts b/netbox/project-static/src/util.ts index e1ada2e19..3aa8b6676 100644 --- a/netbox/project-static/src/util.ts +++ b/netbox/project-static/src/util.ts @@ -244,29 +244,6 @@ export function getSelectedOptions( return selected; } -/** - * Get data that can only be accessed via Django context, and is thus already rendered in the HTML - * template. - * - * @see Templates requiring Django context data have a `{% block data %}` block. - * - * @param key Property name, which must exist on the HTML element. If not already prefixed with - * `data-`, `data-` will be prepended to the property. - * @returns Value if it exists, `null` if not. - */ -export function getNetboxData(key: string): string | null { - if (!key.startsWith('data-')) { - key = `data-${key}`; - } - for (const element of getElements('body > div#netbox-data > *')) { - const value = element.getAttribute(key); - if (isTruthy(value)) { - return value; - } - } - return null; -} - /** * Toggle visibility of an element. */ diff --git a/netbox/project-static/styles/custom/_markdown.scss b/netbox/project-static/styles/custom/_markdown.scss index 08de23581..cb4527f37 100644 --- a/netbox/project-static/styles/custom/_markdown.scss +++ b/netbox/project-static/styles/custom/_markdown.scss @@ -28,6 +28,13 @@ } +// Remove the bottom margin of

    elements inside a table cell +td > .rendered-markdown { + p:last-of-type { + margin-bottom: 0; + } +} + // Markdown preview .markdown-widget { .preview { diff --git a/netbox/project-static/styles/custom/_misc.scss b/netbox/project-static/styles/custom/_misc.scss index ebf66d547..9779bf583 100644 --- a/netbox/project-static/styles/custom/_misc.scss +++ b/netbox/project-static/styles/custom/_misc.scss @@ -2,7 +2,7 @@ // Color labels span.color-label { - display: block; + display: inline-block; width: 5rem; height: 1rem; padding: $badge-padding-y $badge-padding-x; diff --git a/netbox/project-static/styles/overrides/_tabler.scss b/netbox/project-static/styles/overrides/_tabler.scss index a5ae3c647..f855daf0c 100644 --- a/netbox/project-static/styles/overrides/_tabler.scss +++ b/netbox/project-static/styles/overrides/_tabler.scss @@ -9,6 +9,10 @@ pre { // Tabler sets display: flex display: inline-block; } +.btn-sm { + // $border-radius-sm (2px) is too small + border-radius: $border-radius; +} // Tabs .nav-tabs { diff --git a/netbox/project-static/styles/transitional/_tables.scss b/netbox/project-static/styles/transitional/_tables.scss index 6ac17c59c..0af11f9cd 100644 --- a/netbox/project-static/styles/transitional/_tables.scss +++ b/netbox/project-static/styles/transitional/_tables.scss @@ -23,7 +23,6 @@ table.attr-table { // Restyle row header th { - color: $gray-700; font-weight: normal; width: min-content; } diff --git a/netbox/templates/base/base.html b/netbox/templates/base/base.html index b7d4f6fc6..1c58047ef 100644 --- a/netbox/templates/base/base.html +++ b/netbox/templates/base/base.html @@ -70,10 +70,5 @@ {# User messages #} {% include 'inc/messages.html' %} - {# Data container #} -

    - {% block data %}{% endblock %} -
    - diff --git a/netbox/templates/dcim/rack.html b/netbox/templates/dcim/rack.html index 97be5e839..b28ee6e31 100644 --- a/netbox/templates/dcim/rack.html +++ b/netbox/templates/dcim/rack.html @@ -163,7 +163,7 @@
    - diff --git a/netbox/templates/dcim/rack_elevation_list.html b/netbox/templates/dcim/rack_elevation_list.html index 752fe6913..e50380ba0 100644 --- a/netbox/templates/dcim/rack_elevation_list.html +++ b/netbox/templates/dcim/rack_elevation_list.html @@ -11,13 +11,11 @@ {% trans "View List" %} -
    - -
    +
    {% trans "Front" %} {% trans "Rear" %} diff --git a/netbox/templates/inc/toast.html b/netbox/templates/inc/toast.html index 85eff2d7a..0cf04b93b 100644 --- a/netbox/templates/inc/toast.html +++ b/netbox/templates/inc/toast.html @@ -1,6 +1,6 @@ {% load helpers %} -