diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py index 90767d081..539ff9466 100644 --- a/netbox/circuits/api/serializers.py +++ b/netbox/circuits/api/serializers.py @@ -5,7 +5,7 @@ from circuits.models import * from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer from dcim.api.serializers import LinkTerminationSerializer from netbox.api import ChoiceField -from netbox.api.serializers import PrimaryModelSerializer, ValidatedModelSerializer, WritableNestedSerializer +from netbox.api.serializers import NetBoxModelSerializer, ValidatedModelSerializer, WritableNestedSerializer from tenancy.api.nested_serializers import NestedTenantSerializer from .nested_serializers import * @@ -14,7 +14,7 @@ from .nested_serializers import * # Providers # -class ProviderSerializer(PrimaryModelSerializer): +class ProviderSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail') circuit_count = serializers.IntegerField(read_only=True) @@ -30,7 +30,7 @@ class ProviderSerializer(PrimaryModelSerializer): # Provider networks # -class ProviderNetworkSerializer(PrimaryModelSerializer): +class ProviderNetworkSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='circuits-api:providernetwork-detail') provider = NestedProviderSerializer() @@ -46,7 +46,7 @@ class ProviderNetworkSerializer(PrimaryModelSerializer): # Circuits # -class CircuitTypeSerializer(PrimaryModelSerializer): +class CircuitTypeSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail') circuit_count = serializers.IntegerField(read_only=True) @@ -70,7 +70,7 @@ class CircuitCircuitTerminationSerializer(WritableNestedSerializer): ] -class CircuitSerializer(PrimaryModelSerializer): +class CircuitSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail') provider = NestedProviderSerializer() status = ChoiceField(choices=CircuitStatusChoices, required=False) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index bfc2b2d00..155eca716 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -12,7 +12,7 @@ from ipam.api.nested_serializers import ( from ipam.models import ASN, VLAN from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField from netbox.api.serializers import ( - NestedGroupModelSerializer, PrimaryModelSerializer, ValidatedModelSerializer, WritableNestedSerializer, + NestedGroupModelSerializer, NetBoxModelSerializer, ValidatedModelSerializer, WritableNestedSerializer, ) from netbox.config import ConfigItem from tenancy.api.nested_serializers import NestedTenantSerializer @@ -109,7 +109,7 @@ class SiteGroupSerializer(NestedGroupModelSerializer): ] -class SiteSerializer(PrimaryModelSerializer): +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) @@ -161,7 +161,7 @@ class LocationSerializer(NestedGroupModelSerializer): ] -class RackRoleSerializer(PrimaryModelSerializer): +class RackRoleSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail') rack_count = serializers.IntegerField(read_only=True) @@ -173,7 +173,7 @@ class RackRoleSerializer(PrimaryModelSerializer): ] -class RackSerializer(PrimaryModelSerializer): +class RackSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail') site = NestedSiteSerializer() location = NestedLocationSerializer(required=False, allow_null=True, default=None) @@ -212,7 +212,7 @@ class RackUnitSerializer(serializers.Serializer): return obj['name'] -class RackReservationSerializer(PrimaryModelSerializer): +class RackReservationSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackreservation-detail') rack = NestedRackSerializer() user = NestedUserSerializer() @@ -266,7 +266,7 @@ class RackElevationDetailFilterSerializer(serializers.Serializer): # Device/module types # -class ManufacturerSerializer(PrimaryModelSerializer): +class ManufacturerSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail') devicetype_count = serializers.IntegerField(read_only=True) inventoryitem_count = serializers.IntegerField(read_only=True) @@ -280,7 +280,7 @@ class ManufacturerSerializer(PrimaryModelSerializer): ] -class DeviceTypeSerializer(PrimaryModelSerializer): +class DeviceTypeSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail') manufacturer = NestedManufacturerSerializer() subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False) @@ -296,7 +296,7 @@ class DeviceTypeSerializer(PrimaryModelSerializer): ] -class ModuleTypeSerializer(PrimaryModelSerializer): +class ModuleTypeSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:moduletype-detail') manufacturer = NestedManufacturerSerializer() # module_count = serializers.IntegerField(read_only=True) @@ -487,7 +487,7 @@ class InventoryItemTemplateSerializer(ValidatedModelSerializer): # Devices # -class DeviceRoleSerializer(PrimaryModelSerializer): +class DeviceRoleSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail') device_count = serializers.IntegerField(read_only=True) virtualmachine_count = serializers.IntegerField(read_only=True) @@ -500,7 +500,7 @@ class DeviceRoleSerializer(PrimaryModelSerializer): ] -class PlatformSerializer(PrimaryModelSerializer): +class PlatformSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail') manufacturer = NestedManufacturerSerializer(required=False, allow_null=True) device_count = serializers.IntegerField(read_only=True) @@ -514,7 +514,7 @@ class PlatformSerializer(PrimaryModelSerializer): ] -class DeviceSerializer(PrimaryModelSerializer): +class DeviceSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail') device_type = NestedDeviceTypeSerializer() device_role = NestedDeviceRoleSerializer() @@ -556,7 +556,7 @@ class DeviceSerializer(PrimaryModelSerializer): return data -class ModuleSerializer(PrimaryModelSerializer): +class ModuleSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail') device = NestedDeviceSerializer() module_bay = NestedModuleBaySerializer() @@ -594,7 +594,7 @@ class DeviceNAPALMSerializer(serializers.Serializer): # Device components # -class ConsoleServerPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer): +class ConsoleServerPortSerializer(NetBoxModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail') device = NestedDeviceSerializer() module = ComponentNestedModuleSerializer( @@ -622,7 +622,7 @@ class ConsoleServerPortSerializer(PrimaryModelSerializer, LinkTerminationSeriali ] -class ConsolePortSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer): +class ConsolePortSerializer(NetBoxModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail') device = NestedDeviceSerializer() module = ComponentNestedModuleSerializer( @@ -650,7 +650,7 @@ class ConsolePortSerializer(PrimaryModelSerializer, LinkTerminationSerializer, C ] -class PowerOutletSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer): +class PowerOutletSerializer(NetBoxModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail') device = NestedDeviceSerializer() module = ComponentNestedModuleSerializer( @@ -685,7 +685,7 @@ class PowerOutletSerializer(PrimaryModelSerializer, LinkTerminationSerializer, C ] -class PowerPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer): +class PowerPortSerializer(NetBoxModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail') device = NestedDeviceSerializer() module = ComponentNestedModuleSerializer( @@ -709,7 +709,7 @@ class PowerPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer, Con ] -class InterfaceSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer): +class InterfaceSerializer(NetBoxModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail') device = NestedDeviceSerializer() module = ComponentNestedModuleSerializer( @@ -768,7 +768,7 @@ class InterfaceSerializer(PrimaryModelSerializer, LinkTerminationSerializer, Con return super().validate(data) -class RearPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer): +class RearPortSerializer(NetBoxModelSerializer, LinkTerminationSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail') device = NestedDeviceSerializer() module = ComponentNestedModuleSerializer( @@ -798,7 +798,7 @@ class FrontPortRearPortSerializer(WritableNestedSerializer): fields = ['id', 'url', 'display', 'name', 'label'] -class FrontPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer): +class FrontPortSerializer(NetBoxModelSerializer, LinkTerminationSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail') device = NestedDeviceSerializer() module = ComponentNestedModuleSerializer( @@ -818,7 +818,7 @@ class FrontPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer): ] -class ModuleBaySerializer(PrimaryModelSerializer): +class ModuleBaySerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail') device = NestedDeviceSerializer() # installed_module = NestedModuleSerializer(required=False, allow_null=True) @@ -831,7 +831,7 @@ class ModuleBaySerializer(PrimaryModelSerializer): ] -class DeviceBaySerializer(PrimaryModelSerializer): +class DeviceBaySerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail') device = NestedDeviceSerializer() installed_device = NestedDeviceSerializer(required=False, allow_null=True) @@ -844,7 +844,7 @@ class DeviceBaySerializer(PrimaryModelSerializer): ] -class InventoryItemSerializer(PrimaryModelSerializer): +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) @@ -879,7 +879,7 @@ class InventoryItemSerializer(PrimaryModelSerializer): # Device component roles # -class InventoryItemRoleSerializer(PrimaryModelSerializer): +class InventoryItemRoleSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemrole-detail') inventoryitem_count = serializers.IntegerField(read_only=True) @@ -895,7 +895,7 @@ class InventoryItemRoleSerializer(PrimaryModelSerializer): # Cables # -class CableSerializer(PrimaryModelSerializer): +class CableSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail') termination_a_type = ContentTypeField( queryset=ContentType.objects.filter(CABLE_TERMINATION_MODELS) @@ -1001,7 +1001,7 @@ class CablePathSerializer(serializers.ModelSerializer): # Virtual chassis # -class VirtualChassisSerializer(PrimaryModelSerializer): +class VirtualChassisSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail') master = NestedDeviceSerializer(required=False) member_count = serializers.IntegerField(read_only=True) @@ -1018,7 +1018,7 @@ class VirtualChassisSerializer(PrimaryModelSerializer): # Power panels # -class PowerPanelSerializer(PrimaryModelSerializer): +class PowerPanelSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail') site = NestedSiteSerializer() location = NestedLocationSerializer( @@ -1036,7 +1036,7 @@ class PowerPanelSerializer(PrimaryModelSerializer): ] -class PowerFeedSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer): +class PowerFeedSerializer(NetBoxModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerfeed-detail') power_panel = NestedPowerPanelSerializer() rack = NestedRackSerializer( diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index 8fb3bdea5..c26575f1f 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -9,7 +9,7 @@ from ipam.choices import * from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS, VLANGROUP_SCOPE_TYPES from ipam.models import * from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField -from netbox.api.serializers import PrimaryModelSerializer +from netbox.api.serializers import NetBoxModelSerializer from tenancy.api.nested_serializers import NestedTenantSerializer from utilities.api import get_serializer_for_model from virtualization.api.nested_serializers import NestedVirtualMachineSerializer @@ -20,7 +20,7 @@ from .nested_serializers import * # ASNs # -class ASNSerializer(PrimaryModelSerializer): +class ASNSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asn-detail') tenant = NestedTenantSerializer(required=False, allow_null=True) site_count = serializers.IntegerField(read_only=True) @@ -37,7 +37,7 @@ class ASNSerializer(PrimaryModelSerializer): # VRFs # -class VRFSerializer(PrimaryModelSerializer): +class VRFSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vrf-detail') tenant = NestedTenantSerializer(required=False, allow_null=True) import_targets = SerializedPKRelatedField( @@ -67,7 +67,7 @@ class VRFSerializer(PrimaryModelSerializer): # Route targets # -class RouteTargetSerializer(PrimaryModelSerializer): +class RouteTargetSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:routetarget-detail') tenant = NestedTenantSerializer(required=False, allow_null=True) @@ -82,7 +82,7 @@ class RouteTargetSerializer(PrimaryModelSerializer): # RIRs/aggregates # -class RIRSerializer(PrimaryModelSerializer): +class RIRSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail') aggregate_count = serializers.IntegerField(read_only=True) @@ -94,7 +94,7 @@ class RIRSerializer(PrimaryModelSerializer): ] -class AggregateSerializer(PrimaryModelSerializer): +class AggregateSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:aggregate-detail') family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True) rir = NestedRIRSerializer() @@ -113,7 +113,7 @@ class AggregateSerializer(PrimaryModelSerializer): # FHRP Groups # -class FHRPGroupSerializer(PrimaryModelSerializer): +class FHRPGroupSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:fhrpgroup-detail') ip_addresses = NestedIPAddressSerializer(many=True, read_only=True) @@ -125,7 +125,7 @@ class FHRPGroupSerializer(PrimaryModelSerializer): ] -class FHRPGroupAssignmentSerializer(PrimaryModelSerializer): +class FHRPGroupAssignmentSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:fhrpgroupassignment-detail') group = NestedFHRPGroupSerializer() interface_type = ContentTypeField( @@ -153,7 +153,7 @@ class FHRPGroupAssignmentSerializer(PrimaryModelSerializer): # VLANs # -class RoleSerializer(PrimaryModelSerializer): +class RoleSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:role-detail') prefix_count = serializers.IntegerField(read_only=True) vlan_count = serializers.IntegerField(read_only=True) @@ -166,7 +166,7 @@ class RoleSerializer(PrimaryModelSerializer): ] -class VLANGroupSerializer(PrimaryModelSerializer): +class VLANGroupSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail') scope_type = ContentTypeField( queryset=ContentType.objects.filter( @@ -196,7 +196,7 @@ class VLANGroupSerializer(PrimaryModelSerializer): return serializer(obj.scope, context=context).data -class VLANSerializer(PrimaryModelSerializer): +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) @@ -230,7 +230,7 @@ class AvailableVLANSerializer(serializers.Serializer): ]) -class CreateAvailableVLANSerializer(PrimaryModelSerializer): +class CreateAvailableVLANSerializer(NetBoxModelSerializer): site = NestedSiteSerializer(required=False, allow_null=True) tenant = NestedTenantSerializer(required=False, allow_null=True) status = ChoiceField(choices=VLANStatusChoices, required=False) @@ -251,7 +251,7 @@ class CreateAvailableVLANSerializer(PrimaryModelSerializer): # Prefixes # -class PrefixSerializer(PrimaryModelSerializer): +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) @@ -323,7 +323,7 @@ class AvailablePrefixSerializer(serializers.Serializer): # IP ranges # -class IPRangeSerializer(PrimaryModelSerializer): +class IPRangeSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:iprange-detail') family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True) vrf = NestedVRFSerializer(required=False, allow_null=True) @@ -345,7 +345,7 @@ class IPRangeSerializer(PrimaryModelSerializer): # IP addresses # -class IPAddressSerializer(PrimaryModelSerializer): +class IPAddressSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail') family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True) vrf = NestedVRFSerializer(required=False, allow_null=True) @@ -403,7 +403,7 @@ class AvailableIPSerializer(serializers.Serializer): # Services # -class ServiceTemplateSerializer(PrimaryModelSerializer): +class ServiceTemplateSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:servicetemplate-detail') protocol = ChoiceField(choices=ServiceProtocolChoices, required=False) @@ -415,7 +415,7 @@ class ServiceTemplateSerializer(PrimaryModelSerializer): ] -class ServiceSerializer(PrimaryModelSerializer): +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) diff --git a/netbox/netbox/api/serializers.py b/netbox/netbox/api/serializers.py deleted file mode 100644 index 4c26dbada..000000000 --- a/netbox/netbox/api/serializers.py +++ /dev/null @@ -1,193 +0,0 @@ -from django.contrib.contenttypes.models import ContentType -from django.core.exceptions import FieldError, MultipleObjectsReturned, ObjectDoesNotExist -from django.db.models import ManyToManyField -from rest_framework import serializers -from rest_framework.exceptions import ValidationError -from rest_framework.fields import CreateOnlyDefault - -from extras.api.customfields import CustomFieldsDataField, CustomFieldDefaultValues -from extras.models import CustomField, Tag -from utilities.utils import dict_to_filter_params - - -class BaseModelSerializer(serializers.ModelSerializer): - display = serializers.SerializerMethodField(read_only=True) - - def get_display(self, obj): - return str(obj) - - -class ValidatedModelSerializer(BaseModelSerializer): - """ - Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during - validation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144) - """ - def validate(self, data): - - # Remove custom fields data and tags (if any) prior to model validation - attrs = data.copy() - attrs.pop('custom_fields', None) - attrs.pop('tags', None) - - # Skip ManyToManyFields - for field in self.Meta.model._meta.get_fields(): - if isinstance(field, ManyToManyField): - attrs.pop(field.name, None) - - # Run clean() on an instance of the model - if self.instance is None: - instance = self.Meta.model(**attrs) - else: - instance = self.instance - for k, v in attrs.items(): - setattr(instance, k, v) - instance.full_clean() - - return data - - -class CustomFieldModelSerializer(ValidatedModelSerializer): - """ - Extends ModelSerializer to render any CustomFields and their values associated with an object. - """ - custom_fields = CustomFieldsDataField( - source='custom_field_data', - default=CreateOnlyDefault(CustomFieldDefaultValues()) - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - if self.instance is not None: - - # Retrieve the set of CustomFields which apply to this type of object - content_type = ContentType.objects.get_for_model(self.Meta.model) - fields = CustomField.objects.filter(content_types=content_type) - - # Populate CustomFieldValues for each instance from database - if type(self.instance) in (list, tuple): - for obj in self.instance: - self._populate_custom_fields(obj, fields) - else: - self._populate_custom_fields(self.instance, fields) - - def _populate_custom_fields(self, instance, custom_fields): - instance.custom_fields = {} - for field in custom_fields: - instance.custom_fields[field.name] = instance.cf.get(field.name) - - -# -# Nested serializers -# - -class WritableNestedSerializer(BaseModelSerializer): - """ - Returns a nested representation of an object on read, but accepts only a primary key on write. - """ - 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: {}".format(params) - ) - except MultipleObjectsReturned: - raise ValidationError( - "Multiple objects match the provided attributes: {}".format(params) - ) - except FieldError as e: - raise ValidationError(e) - - # Integer PK of related object - if isinstance(data, int): - pk = data - else: - try: - # PK might have been mistakenly passed 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: {}".format(data) - ) - - # Look up object by PK - queryset = self.Meta.model.objects - try: - return queryset.get(pk=int(data)) - except ObjectDoesNotExist: - raise ValidationError( - "Related object not found using the provided numeric ID: {}".format(pk) - ) - - -# -# Nested tags serialization -# - -# Declared here for use by PrimaryModelSerializer, but should be imported from extras.api.nested_serializers -class NestedTagSerializer(WritableNestedSerializer): - url = serializers.HyperlinkedIdentityField(view_name='extras-api:tag-detail') - - class Meta: - model = Tag - fields = ['id', 'url', 'display', 'name', 'slug', 'color'] - - -# -# Base model serializers -# - -class PrimaryModelSerializer(CustomFieldModelSerializer): - """ - Adds support for custom fields and tags. - """ - tags = NestedTagSerializer(many=True, required=False) - - def create(self, validated_data): - tags = validated_data.pop('tags', None) - instance = super().create(validated_data) - - if tags is not None: - return self._save_tags(instance, tags) - return instance - - def update(self, instance, validated_data): - tags = validated_data.pop('tags', None) - - # Cache tags on instance for change logging - instance._tags = tags or [] - - instance = super().update(instance, validated_data) - - if tags is not None: - return self._save_tags(instance, tags) - return instance - - def _save_tags(self, instance, tags): - if tags: - instance.tags.set([t.name for t in tags]) - else: - instance.tags.clear() - - return instance - - -class NestedGroupModelSerializer(PrimaryModelSerializer): - """ - Extends PrimaryModelSerializer to include MPTT support. - """ - _depth = serializers.IntegerField(source='level', read_only=True) - - -class BulkOperationSerializer(serializers.Serializer): - id = serializers.IntegerField() diff --git a/netbox/netbox/api/serializers/__init__.py b/netbox/netbox/api/serializers/__init__.py new file mode 100644 index 000000000..d7c363754 --- /dev/null +++ b/netbox/netbox/api/serializers/__init__.py @@ -0,0 +1,27 @@ +from rest_framework import serializers + +from .base import * +from .features import * +from .nested import * + + +# +# Base model serializers +# + +class NetBoxModelSerializer(TaggableObjectSerializer, CustomFieldModelSerializer, ValidatedModelSerializer): + """ + Adds support for custom fields and tags. + """ + pass + + +class NestedGroupModelSerializer(NetBoxModelSerializer): + """ + Extends PrimaryModelSerializer to include MPTT support. + """ + _depth = serializers.IntegerField(source='level', read_only=True) + + +class BulkOperationSerializer(serializers.Serializer): + id = serializers.IntegerField() diff --git a/netbox/netbox/api/serializers/base.py b/netbox/netbox/api/serializers/base.py new file mode 100644 index 000000000..f1aea0e2b --- /dev/null +++ b/netbox/netbox/api/serializers/base.py @@ -0,0 +1,43 @@ +from django.db.models import ManyToManyField +from rest_framework import serializers + +__all__ = ( + 'BaseModelSerializer', + 'ValidatedModelSerializer', +) + + +class BaseModelSerializer(serializers.ModelSerializer): + display = serializers.SerializerMethodField(read_only=True) + + def get_display(self, obj): + return str(obj) + + +class ValidatedModelSerializer(BaseModelSerializer): + """ + Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during + validation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144) + """ + def validate(self, data): + + # Remove custom fields data and tags (if any) prior to model validation + attrs = data.copy() + attrs.pop('custom_fields', None) + attrs.pop('tags', None) + + # Skip ManyToManyFields + for field in self.Meta.model._meta.get_fields(): + if isinstance(field, ManyToManyField): + attrs.pop(field.name, None) + + # Run clean() on an instance of the model + if self.instance is None: + instance = self.Meta.model(**attrs) + else: + instance = self.instance + for k, v in attrs.items(): + setattr(instance, k, v) + instance.full_clean() + + return data diff --git a/netbox/netbox/api/serializers/features.py b/netbox/netbox/api/serializers/features.py new file mode 100644 index 000000000..34572352c --- /dev/null +++ b/netbox/netbox/api/serializers/features.py @@ -0,0 +1,80 @@ +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__ = ( + 'CustomFieldModelSerializer', + 'TaggableObjectSerializer', +) + + +class CustomFieldModelSerializer(serializers.Serializer): + """ + Introduces support for custom field assignment. Adds `custom_fields` serialization and ensures + that custom field data is populated upon initialization. + """ + custom_fields = CustomFieldsDataField( + source='custom_field_data', + default=CreateOnlyDefault(CustomFieldDefaultValues()) + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + if self.instance is not None: + + # Retrieve the set of CustomFields which apply to this type of object + content_type = ContentType.objects.get_for_model(self.Meta.model) + fields = CustomField.objects.filter(content_types=content_type) + + # Populate custom field values for each instance from database + if type(self.instance) in (list, tuple): + for obj in self.instance: + self._populate_custom_fields(obj, fields) + else: + self._populate_custom_fields(self.instance, fields) + + def _populate_custom_fields(self, instance, custom_fields): + instance.custom_fields = {} + for field in custom_fields: + instance.custom_fields[field.name] = instance.cf.get(field.name) + + +class TaggableObjectSerializer(serializers.Serializer): + """ + Introduces support for Tag assignment. Adds `tags` serialization, and handles tag assignment + on create() and update(). + """ + tags = NestedTagSerializer(many=True, required=False) + + def create(self, validated_data): + tags = validated_data.pop('tags', None) + instance = super().create(validated_data) + + if tags is not None: + return self._save_tags(instance, tags) + return instance + + def update(self, instance, validated_data): + tags = validated_data.pop('tags', None) + + # Cache tags on instance for change logging + instance._tags = tags or [] + + instance = super().update(instance, validated_data) + + if tags is not None: + return self._save_tags(instance, tags) + return instance + + def _save_tags(self, instance, tags): + if tags: + instance.tags.set([t.name for t in tags]) + else: + instance.tags.clear() + + return instance diff --git a/netbox/netbox/api/serializers/nested.py b/netbox/netbox/api/serializers/nested.py new file mode 100644 index 000000000..95dcd560c --- /dev/null +++ b/netbox/netbox/api/serializers/nested.py @@ -0,0 +1,62 @@ +from django.core.exceptions import FieldError, MultipleObjectsReturned, ObjectDoesNotExist +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 .base import BaseModelSerializer + +__all__ = ( + 'NestedTagSerializer', + 'WritableNestedSerializer', +) + + +class WritableNestedSerializer(BaseModelSerializer): + """ + Represents an object related through a ForeignKey field. On write, it accepts a primary key (PK) value or a + dictionary of attributes which can be used to uniquely identify the related object. This class should be + 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(f"Related object not found using the provided attributes: {params}") + except MultipleObjectsReturned: + raise ValidationError(f"Multiple objects match the provided attributes: {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( + f"Related objects must be referenced by numeric ID or by dictionary of attributes. Received an " + f"unrecognized value: {data}" + ) + + # Look up object by PK + try: + return self.Meta.model.objects.get(pk=pk) + except ObjectDoesNotExist: + raise ValidationError(f"Related object not found using the provided numeric ID: {pk}") + + +# Declared here for use by PrimaryModelSerializer, but should be imported from extras.api.nested_serializers +class NestedTagSerializer(WritableNestedSerializer): + url = serializers.HyperlinkedIdentityField(view_name='extras-api:tag-detail') + + class Meta: + model = Tag + fields = ['id', 'url', 'display', 'name', 'slug', 'color'] diff --git a/netbox/tenancy/api/serializers.py b/netbox/tenancy/api/serializers.py index a0482aa1d..21a75a6bf 100644 --- a/netbox/tenancy/api/serializers.py +++ b/netbox/tenancy/api/serializers.py @@ -3,7 +3,7 @@ from drf_yasg.utils import swagger_serializer_method from rest_framework import serializers from netbox.api import ChoiceField, ContentTypeField -from netbox.api.serializers import NestedGroupModelSerializer, PrimaryModelSerializer +from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer from tenancy.choices import ContactPriorityChoices from tenancy.models import * from utilities.api import get_serializer_for_model @@ -27,7 +27,7 @@ class TenantGroupSerializer(NestedGroupModelSerializer): ] -class TenantSerializer(PrimaryModelSerializer): +class TenantSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenant-detail') group = NestedTenantGroupSerializer(required=False, allow_null=True) circuit_count = serializers.IntegerField(read_only=True) @@ -67,7 +67,7 @@ class ContactGroupSerializer(NestedGroupModelSerializer): ] -class ContactRoleSerializer(PrimaryModelSerializer): +class ContactRoleSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactrole-detail') class Meta: @@ -77,7 +77,7 @@ class ContactRoleSerializer(PrimaryModelSerializer): ] -class ContactSerializer(PrimaryModelSerializer): +class ContactSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contact-detail') group = NestedContactGroupSerializer(required=False, allow_null=True, default=None) @@ -89,7 +89,7 @@ class ContactSerializer(PrimaryModelSerializer): ] -class ContactAssignmentSerializer(PrimaryModelSerializer): +class ContactAssignmentSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactassignment-detail') content_type = ContentTypeField( queryset=ContentType.objects.all() diff --git a/netbox/virtualization/api/serializers.py b/netbox/virtualization/api/serializers.py index 3d3451062..afdf50b96 100644 --- a/netbox/virtualization/api/serializers.py +++ b/netbox/virtualization/api/serializers.py @@ -6,7 +6,7 @@ from dcim.choices import InterfaceModeChoices from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer, NestedVRFSerializer from ipam.models import VLAN from netbox.api import ChoiceField, SerializedPKRelatedField -from netbox.api.serializers import PrimaryModelSerializer +from netbox.api.serializers import NetBoxModelSerializer from tenancy.api.nested_serializers import NestedTenantSerializer from virtualization.choices import * from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface @@ -17,7 +17,7 @@ from .nested_serializers import * # Clusters # -class ClusterTypeSerializer(PrimaryModelSerializer): +class ClusterTypeSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustertype-detail') cluster_count = serializers.IntegerField(read_only=True) @@ -29,7 +29,7 @@ class ClusterTypeSerializer(PrimaryModelSerializer): ] -class ClusterGroupSerializer(PrimaryModelSerializer): +class ClusterGroupSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustergroup-detail') cluster_count = serializers.IntegerField(read_only=True) @@ -41,7 +41,7 @@ class ClusterGroupSerializer(PrimaryModelSerializer): ] -class ClusterSerializer(PrimaryModelSerializer): +class ClusterSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail') type = NestedClusterTypeSerializer() group = NestedClusterGroupSerializer(required=False, allow_null=True, default=None) @@ -62,7 +62,7 @@ class ClusterSerializer(PrimaryModelSerializer): # Virtual machines # -class VirtualMachineSerializer(PrimaryModelSerializer): +class VirtualMachineSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:virtualmachine-detail') status = ChoiceField(choices=VirtualMachineStatusChoices, required=False) site = NestedSiteSerializer(read_only=True) @@ -103,7 +103,7 @@ class VirtualMachineWithConfigContextSerializer(VirtualMachineSerializer): # VM interfaces # -class VMInterfaceSerializer(PrimaryModelSerializer): +class VMInterfaceSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:vminterface-detail') virtual_machine = NestedVirtualMachineSerializer() parent = NestedVMInterfaceSerializer(required=False, allow_null=True) diff --git a/netbox/wireless/api/serializers.py b/netbox/wireless/api/serializers.py index f1fa6d58d..4a6abe94d 100644 --- a/netbox/wireless/api/serializers.py +++ b/netbox/wireless/api/serializers.py @@ -4,7 +4,7 @@ from dcim.choices import LinkStatusChoices from dcim.api.serializers import NestedInterfaceSerializer from ipam.api.serializers import NestedVLANSerializer from netbox.api import ChoiceField -from netbox.api.serializers import NestedGroupModelSerializer, PrimaryModelSerializer +from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer from wireless.choices import * from wireless.models import * from .nested_serializers import * @@ -29,7 +29,7 @@ class WirelessLANGroupSerializer(NestedGroupModelSerializer): ] -class WirelessLANSerializer(PrimaryModelSerializer): +class WirelessLANSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslan-detail') group = NestedWirelessLANGroupSerializer(required=False, allow_null=True) vlan = NestedVLANSerializer(required=False, allow_null=True) @@ -44,7 +44,7 @@ class WirelessLANSerializer(PrimaryModelSerializer): ] -class WirelessLinkSerializer(PrimaryModelSerializer): +class WirelessLinkSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslink-detail') status = ChoiceField(choices=LinkStatusChoices, required=False) interface_a = NestedInterfaceSerializer()