Przeglądaj źródła

Clean up base serializers

Jeremy Stretch 5 lat temu
rodzic
commit
fca5accba8

+ 3 - 5
netbox/circuits/api/serializers.py

@@ -4,10 +4,8 @@ from circuits.choices import CircuitStatusChoices
 from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
 from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
 from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer
 from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer
 from dcim.api.serializers import CableTerminationSerializer, ConnectedEndpointSerializer
 from dcim.api.serializers import CableTerminationSerializer, ConnectedEndpointSerializer
-from netbox.api.serializers import CustomFieldModelSerializer
-from extras.api.serializers import TaggedObjectSerializer
 from netbox.api import ChoiceField
 from netbox.api import ChoiceField
-from netbox.api.serializers import OrganizationalModelSerializer, WritableNestedSerializer
+from netbox.api.serializers import OrganizationalModelSerializer, PrimaryModelSerializer, WritableNestedSerializer
 from tenancy.api.nested_serializers import NestedTenantSerializer
 from tenancy.api.nested_serializers import NestedTenantSerializer
 from .nested_serializers import *
 from .nested_serializers import *
 
 
@@ -16,7 +14,7 @@ from .nested_serializers import *
 # Providers
 # Providers
 #
 #
 
 
-class ProviderSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
+class ProviderSerializer(PrimaryModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
     url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
     circuit_count = serializers.IntegerField(read_only=True)
     circuit_count = serializers.IntegerField(read_only=True)
 
 
@@ -55,7 +53,7 @@ class CircuitCircuitTerminationSerializer(WritableNestedSerializer, ConnectedEnd
         ]
         ]
 
 
 
 
-class CircuitSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
+class CircuitSerializer(PrimaryModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
     url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
     provider = NestedProviderSerializer()
     provider = NestedProviderSerializer()
     status = ChoiceField(choices=CircuitStatusChoices, required=False)
     status = ChoiceField(choices=CircuitStatusChoices, required=False)

+ 25 - 27
netbox/dcim/api/serializers.py

@@ -7,13 +7,12 @@ from rest_framework.validators import UniqueTogetherValidator
 from dcim.choices import *
 from dcim.choices import *
 from dcim.constants import *
 from dcim.constants import *
 from dcim.models import *
 from dcim.models import *
-from netbox.api.serializers import CustomFieldModelSerializer
-from extras.api.serializers import TaggedObjectSerializer
 from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer
 from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer
 from ipam.models import VLAN
 from ipam.models import VLAN
 from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField, TimeZoneField
 from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField, TimeZoneField
 from netbox.api.serializers import (
 from netbox.api.serializers import (
-    NestedGroupModelSerializer, OrganizationalModelSerializer, ValidatedModelSerializer, WritableNestedSerializer,
+    NestedGroupModelSerializer, OrganizationalModelSerializer, PrimaryModelSerializer, ValidatedModelSerializer,
+    WritableNestedSerializer,
 )
 )
 from tenancy.api.nested_serializers import NestedTenantSerializer
 from tenancy.api.nested_serializers import NestedTenantSerializer
 from users.api.nested_serializers import NestedUserSerializer
 from users.api.nested_serializers import NestedUserSerializer
@@ -43,7 +42,7 @@ class CableTerminationSerializer(serializers.ModelSerializer):
         return None
         return None
 
 
 
 
-class ConnectedEndpointSerializer(CustomFieldModelSerializer):
+class ConnectedEndpointSerializer(serializers.ModelSerializer):
     connected_endpoint_type = serializers.SerializerMethodField(read_only=True)
     connected_endpoint_type = serializers.SerializerMethodField(read_only=True)
     connected_endpoint = serializers.SerializerMethodField(read_only=True)
     connected_endpoint = serializers.SerializerMethodField(read_only=True)
     connected_endpoint_reachable = serializers.SerializerMethodField(read_only=True)
     connected_endpoint_reachable = serializers.SerializerMethodField(read_only=True)
@@ -101,7 +100,7 @@ class SiteGroupSerializer(NestedGroupModelSerializer):
         ]
         ]
 
 
 
 
-class SiteSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
+class SiteSerializer(PrimaryModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:site-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:site-detail')
     status = ChoiceField(choices=SiteStatusChoices, required=False)
     status = ChoiceField(choices=SiteStatusChoices, required=False)
     region = NestedRegionSerializer(required=False, allow_null=True)
     region = NestedRegionSerializer(required=False, allow_null=True)
@@ -155,7 +154,7 @@ class RackRoleSerializer(OrganizationalModelSerializer):
         ]
         ]
 
 
 
 
-class RackSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
+class RackSerializer(PrimaryModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail')
     site = NestedSiteSerializer()
     site = NestedSiteSerializer()
     location = NestedLocationSerializer(required=False, allow_null=True, default=None)
     location = NestedLocationSerializer(required=False, allow_null=True, default=None)
@@ -206,7 +205,7 @@ class RackUnitSerializer(serializers.Serializer):
     occupied = serializers.BooleanField(read_only=True)
     occupied = serializers.BooleanField(read_only=True)
 
 
 
 
-class RackReservationSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
+class RackReservationSerializer(PrimaryModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackreservation-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackreservation-detail')
     rack = NestedRackSerializer()
     rack = NestedRackSerializer()
     user = NestedUserSerializer()
     user = NestedUserSerializer()
@@ -271,7 +270,7 @@ class ManufacturerSerializer(OrganizationalModelSerializer):
         ]
         ]
 
 
 
 
-class DeviceTypeSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
+class DeviceTypeSerializer(PrimaryModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
     manufacturer = NestedManufacturerSerializer()
     manufacturer = NestedManufacturerSerializer()
     subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False)
     subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False)
@@ -434,7 +433,7 @@ class PlatformSerializer(OrganizationalModelSerializer):
         ]
         ]
 
 
 
 
-class DeviceSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
+class DeviceSerializer(PrimaryModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
     device_type = NestedDeviceTypeSerializer()
     device_type = NestedDeviceTypeSerializer()
     device_role = NestedDeviceRoleSerializer()
     device_role = NestedDeviceRoleSerializer()
@@ -506,7 +505,11 @@ class DeviceNAPALMSerializer(serializers.Serializer):
     method = serializers.DictField()
     method = serializers.DictField()
 
 
 
 
-class ConsoleServerPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
+#
+# Device components
+#
+
+class ConsoleServerPortSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
     device = NestedDeviceSerializer()
     device = NestedDeviceSerializer()
     type = ChoiceField(
     type = ChoiceField(
@@ -530,7 +533,7 @@ class ConsoleServerPortSerializer(TaggedObjectSerializer, CableTerminationSerial
         ]
         ]
 
 
 
 
-class ConsolePortSerializer(TaggedObjectSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
+class ConsolePortSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail')
     device = NestedDeviceSerializer()
     device = NestedDeviceSerializer()
     type = ChoiceField(
     type = ChoiceField(
@@ -554,7 +557,7 @@ class ConsolePortSerializer(TaggedObjectSerializer, CableTerminationSerializer,
         ]
         ]
 
 
 
 
-class PowerOutletSerializer(TaggedObjectSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
+class PowerOutletSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
     device = NestedDeviceSerializer()
     device = NestedDeviceSerializer()
     type = ChoiceField(
     type = ChoiceField(
@@ -583,7 +586,7 @@ class PowerOutletSerializer(TaggedObjectSerializer, CableTerminationSerializer,
         ]
         ]
 
 
 
 
-class PowerPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
+class PowerPortSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail')
     device = NestedDeviceSerializer()
     device = NestedDeviceSerializer()
     type = ChoiceField(
     type = ChoiceField(
@@ -602,7 +605,7 @@ class PowerPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, Co
         ]
         ]
 
 
 
 
-class InterfaceSerializer(TaggedObjectSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
+class InterfaceSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
     device = NestedDeviceSerializer()
     device = NestedDeviceSerializer()
     type = ChoiceField(choices=InterfaceTypeChoices)
     type = ChoiceField(choices=InterfaceTypeChoices)
@@ -643,7 +646,7 @@ class InterfaceSerializer(TaggedObjectSerializer, CableTerminationSerializer, Co
         return super().validate(data)
         return super().validate(data)
 
 
 
 
-class RearPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, CustomFieldModelSerializer):
+class RearPortSerializer(PrimaryModelSerializer, CableTerminationSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
     device = NestedDeviceSerializer()
     device = NestedDeviceSerializer()
     type = ChoiceField(choices=PortTypeChoices)
     type = ChoiceField(choices=PortTypeChoices)
@@ -668,7 +671,7 @@ class FrontPortRearPortSerializer(WritableNestedSerializer):
         fields = ['id', 'url', 'name', 'label']
         fields = ['id', 'url', 'name', 'label']
 
 
 
 
-class FrontPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, CustomFieldModelSerializer):
+class FrontPortSerializer(PrimaryModelSerializer, CableTerminationSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail')
     device = NestedDeviceSerializer()
     device = NestedDeviceSerializer()
     type = ChoiceField(choices=PortTypeChoices)
     type = ChoiceField(choices=PortTypeChoices)
@@ -684,7 +687,7 @@ class FrontPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, Cu
         ]
         ]
 
 
 
 
-class DeviceBaySerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
+class DeviceBaySerializer(PrimaryModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail')
     device = NestedDeviceSerializer()
     device = NestedDeviceSerializer()
     installed_device = NestedDeviceSerializer(required=False, allow_null=True)
     installed_device = NestedDeviceSerializer(required=False, allow_null=True)
@@ -701,7 +704,7 @@ class DeviceBaySerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
 # Inventory items
 # Inventory items
 #
 #
 
 
-class InventoryItemSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
+class InventoryItemSerializer(PrimaryModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitem-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitem-detail')
     device = NestedDeviceSerializer()
     device = NestedDeviceSerializer()
     # Provide a default value to satisfy UniqueTogetherValidator
     # Provide a default value to satisfy UniqueTogetherValidator
@@ -721,7 +724,7 @@ class InventoryItemSerializer(TaggedObjectSerializer, CustomFieldModelSerializer
 # Cables
 # Cables
 #
 #
 
 
-class CableSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
+class CableSerializer(PrimaryModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail')
     termination_a_type = ContentTypeField(
     termination_a_type = ContentTypeField(
         queryset=ContentType.objects.filter(CABLE_TERMINATION_MODELS)
         queryset=ContentType.objects.filter(CABLE_TERMINATION_MODELS)
@@ -851,7 +854,7 @@ class InterfaceConnectionSerializer(ValidatedModelSerializer):
 # Virtual chassis
 # Virtual chassis
 #
 #
 
 
-class VirtualChassisSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
+class VirtualChassisSerializer(PrimaryModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
     master = NestedDeviceSerializer(required=False)
     master = NestedDeviceSerializer(required=False)
     member_count = serializers.IntegerField(read_only=True)
     member_count = serializers.IntegerField(read_only=True)
@@ -865,7 +868,7 @@ class VirtualChassisSerializer(TaggedObjectSerializer, CustomFieldModelSerialize
 # Power panels
 # Power panels
 #
 #
 
 
-class PowerPanelSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
+class PowerPanelSerializer(PrimaryModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail')
     site = NestedSiteSerializer()
     site = NestedSiteSerializer()
     location = NestedLocationSerializer(
     location = NestedLocationSerializer(
@@ -880,12 +883,7 @@ class PowerPanelSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
         fields = ['id', 'url', 'site', 'location', 'name', 'tags', 'custom_fields', 'powerfeed_count']
         fields = ['id', 'url', 'site', 'location', 'name', 'tags', 'custom_fields', 'powerfeed_count']
 
 
 
 
-class PowerFeedSerializer(
-    TaggedObjectSerializer,
-    CableTerminationSerializer,
-    ConnectedEndpointSerializer,
-    CustomFieldModelSerializer
-):
+class PowerFeedSerializer(PrimaryModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerfeed-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerfeed-detail')
     power_panel = NestedPowerPanelSerializer()
     power_panel = NestedPowerPanelSerializer()
     rack = NestedRackSerializer(
     rack = NestedRackSerializer(

+ 2 - 9
netbox/extras/api/nested_serializers.py

@@ -2,6 +2,7 @@ from rest_framework import serializers
 
 
 from extras import choices, models
 from extras import choices, models
 from netbox.api import ChoiceField, WritableNestedSerializer
 from netbox.api import ChoiceField, WritableNestedSerializer
+from netbox.api.serializers import NestedTagSerializer
 from users.api.nested_serializers import NestedUserSerializer
 from users.api.nested_serializers import NestedUserSerializer
 
 
 __all__ = [
 __all__ = [
@@ -11,7 +12,7 @@ __all__ = [
     'NestedExportTemplateSerializer',
     'NestedExportTemplateSerializer',
     'NestedImageAttachmentSerializer',
     'NestedImageAttachmentSerializer',
     'NestedJobResultSerializer',
     'NestedJobResultSerializer',
-    'NestedTagSerializer',
+    'NestedTagSerializer',  # Defined in netbox.api.serializers
     'NestedWebhookSerializer',
     'NestedWebhookSerializer',
 ]
 ]
 
 
@@ -64,14 +65,6 @@ class NestedImageAttachmentSerializer(WritableNestedSerializer):
         fields = ['id', 'url', 'name', 'image']
         fields = ['id', 'url', 'name', 'image']
 
 
 
 
-class NestedTagSerializer(WritableNestedSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='extras-api:tag-detail')
-
-    class Meta:
-        model = models.Tag
-        fields = ['id', 'url', 'name', 'slug', 'color']
-
-
 class NestedJobResultSerializer(serializers.ModelSerializer):
 class NestedJobResultSerializer(serializers.ModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='extras-api:jobresult-detail')
     url = serializers.HyperlinkedIdentityField(view_name='extras-api:jobresult-detail')
     status = ChoiceField(choices=choices.JobResultStatusChoices)
     status = ChoiceField(choices=choices.JobResultStatusChoices)

+ 0 - 34
netbox/extras/api/serializers.py

@@ -21,7 +21,6 @@ from virtualization.api.nested_serializers import NestedClusterGroupSerializer,
 from virtualization.models import Cluster, ClusterGroup
 from virtualization.models import Cluster, ClusterGroup
 from .nested_serializers import *
 from .nested_serializers import *
 
 
-
 __all__ = (
 __all__ = (
     'ConfigContextSerializer',
     'ConfigContextSerializer',
     'ContentTypeSerializer',
     'ContentTypeSerializer',
@@ -39,7 +38,6 @@ __all__ = (
     'ScriptOutputSerializer',
     'ScriptOutputSerializer',
     'ScriptSerializer',
     'ScriptSerializer',
     'TagSerializer',
     'TagSerializer',
-    'TaggedObjectSerializer',
     'WebhookSerializer',
     'WebhookSerializer',
 )
 )
 
 
@@ -131,38 +129,6 @@ class TagSerializer(ValidatedModelSerializer):
         fields = ['id', 'url', 'name', 'slug', 'color', 'description', 'tagged_items']
         fields = ['id', 'url', 'name', 'slug', 'color', 'description', 'tagged_items']
 
 
 
 
-class TaggedObjectSerializer(serializers.Serializer):
-    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
-
-
 #
 #
 # Image attachments
 # Image attachments
 #
 #

+ 8 - 9
netbox/ipam/api/serializers.py

@@ -6,13 +6,12 @@ from rest_framework import serializers
 from rest_framework.validators import UniqueTogetherValidator
 from rest_framework.validators import UniqueTogetherValidator
 
 
 from dcim.api.nested_serializers import NestedDeviceSerializer, NestedSiteSerializer
 from dcim.api.nested_serializers import NestedDeviceSerializer, NestedSiteSerializer
-from netbox.api.serializers import CustomFieldModelSerializer
-from extras.api.serializers import TaggedObjectSerializer
 from ipam.choices import *
 from ipam.choices import *
 from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS
 from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS
 from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, RouteTarget, Service, VLAN, VLANGroup, VRF
 from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, RouteTarget, Service, VLAN, VLANGroup, VRF
 from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField
 from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField
 from netbox.api.serializers import OrganizationalModelSerializer
 from netbox.api.serializers import OrganizationalModelSerializer
+from netbox.api.serializers import PrimaryModelSerializer
 from tenancy.api.nested_serializers import NestedTenantSerializer
 from tenancy.api.nested_serializers import NestedTenantSerializer
 from utilities.api import get_serializer_for_model
 from utilities.api import get_serializer_for_model
 from virtualization.api.nested_serializers import NestedVirtualMachineSerializer
 from virtualization.api.nested_serializers import NestedVirtualMachineSerializer
@@ -23,7 +22,7 @@ from .nested_serializers import *
 # VRFs
 # VRFs
 #
 #
 
 
-class VRFSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
+class VRFSerializer(PrimaryModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vrf-detail')
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vrf-detail')
     tenant = NestedTenantSerializer(required=False, allow_null=True)
     tenant = NestedTenantSerializer(required=False, allow_null=True)
     import_targets = SerializedPKRelatedField(
     import_targets = SerializedPKRelatedField(
@@ -53,7 +52,7 @@ class VRFSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
 # Route targets
 # Route targets
 #
 #
 
 
-class RouteTargetSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
+class RouteTargetSerializer(PrimaryModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:routetarget-detail')
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:routetarget-detail')
     tenant = NestedTenantSerializer(required=False, allow_null=True)
     tenant = NestedTenantSerializer(required=False, allow_null=True)
 
 
@@ -80,7 +79,7 @@ class RIRSerializer(OrganizationalModelSerializer):
         ]
         ]
 
 
 
 
-class AggregateSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
+class AggregateSerializer(PrimaryModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:aggregate-detail')
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:aggregate-detail')
     family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
     family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
     rir = NestedRIRSerializer()
     rir = NestedRIRSerializer()
@@ -139,7 +138,7 @@ class VLANGroupSerializer(OrganizationalModelSerializer):
         return data
         return data
 
 
 
 
-class VLANSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
+class VLANSerializer(PrimaryModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail')
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail')
     site = NestedSiteSerializer(required=False, allow_null=True)
     site = NestedSiteSerializer(required=False, allow_null=True)
     group = NestedVLANGroupSerializer(required=False, allow_null=True)
     group = NestedVLANGroupSerializer(required=False, allow_null=True)
@@ -174,7 +173,7 @@ class VLANSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
 # Prefixes
 # Prefixes
 #
 #
 
 
-class PrefixSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
+class PrefixSerializer(PrimaryModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:prefix-detail')
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:prefix-detail')
     family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
     family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
     site = NestedSiteSerializer(required=False, allow_null=True)
     site = NestedSiteSerializer(required=False, allow_null=True)
@@ -244,7 +243,7 @@ class AvailablePrefixSerializer(serializers.Serializer):
 # IP addresses
 # IP addresses
 #
 #
 
 
-class IPAddressSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
+class IPAddressSerializer(PrimaryModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail')
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail')
     family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
     family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
     vrf = NestedVRFSerializer(required=False, allow_null=True)
     vrf = NestedVRFSerializer(required=False, allow_null=True)
@@ -302,7 +301,7 @@ class AvailableIPSerializer(serializers.Serializer):
 # Services
 # Services
 #
 #
 
 
-class ServiceSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
+class ServiceSerializer(PrimaryModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:service-detail')
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:service-detail')
     device = NestedDeviceSerializer(required=False, allow_null=True)
     device = NestedDeviceSerializer(required=False, allow_null=True)
     virtual_machine = NestedVirtualMachineSerializer(required=False, allow_null=True)
     virtual_machine = NestedVirtualMachineSerializer(required=False, allow_null=True)

+ 70 - 9
netbox/netbox/api/serializers.py

@@ -6,7 +6,7 @@ from rest_framework.exceptions import ValidationError
 from rest_framework.fields import CreateOnlyDefault
 from rest_framework.fields import CreateOnlyDefault
 
 
 from extras.api.customfields import CustomFieldsDataField, CustomFieldDefaultValues
 from extras.api.customfields import CustomFieldsDataField, CustomFieldDefaultValues
-from extras.models import CustomField
+from extras.models import CustomField, Tag
 from utilities.utils import dict_to_filter_params
 from utilities.utils import dict_to_filter_params
 
 
 
 
@@ -70,19 +70,14 @@ class CustomFieldModelSerializer(ValidatedModelSerializer):
             instance.custom_fields[field.name] = instance.cf.get(field.name)
             instance.custom_fields[field.name] = instance.cf.get(field.name)
 
 
 
 
-class OrganizationalModelSerializer(CustomFieldModelSerializer):
-    pass
-
-
-class NestedGroupModelSerializer(CustomFieldModelSerializer):
-    _depth = serializers.IntegerField(source='level', read_only=True)
-
+#
+# Nested serializers
+#
 
 
 class WritableNestedSerializer(serializers.ModelSerializer):
 class WritableNestedSerializer(serializers.ModelSerializer):
     """
     """
     Returns a nested representation of an object on read, but accepts only a primary key on write.
     Returns a nested representation of an object on read, but accepts only a primary key on write.
     """
     """
-
     def to_internal_value(self, data):
     def to_internal_value(self, data):
 
 
         if data is None:
         if data is None:
@@ -128,5 +123,71 @@ class WritableNestedSerializer(serializers.ModelSerializer):
             )
             )
 
 
 
 
+#
+# 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', 'name', 'slug', 'color']
+
+
+#
+# Base model serializers
+#
+
+class OrganizationalModelSerializer(CustomFieldModelSerializer):
+    """
+    Adds support for custom fields.
+    """
+    pass
+
+
+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(CustomFieldModelSerializer):
+    """
+    Extends OrganizationalModelSerializer to include MPTT support.
+    """
+    _depth = serializers.IntegerField(source='level', read_only=True)
+
+
 class BulkOperationSerializer(serializers.Serializer):
 class BulkOperationSerializer(serializers.Serializer):
     id = serializers.IntegerField()
     id = serializers.IntegerField()

+ 4 - 5
netbox/secrets/api/serializers.py

@@ -2,11 +2,10 @@ from django.contrib.contenttypes.models import ContentType
 from drf_yasg.utils import swagger_serializer_method
 from drf_yasg.utils import swagger_serializer_method
 from rest_framework import serializers
 from rest_framework import serializers
 
 
-from netbox.api.serializers import CustomFieldModelSerializer
-from extras.api.serializers import TaggedObjectSerializer
+from netbox.api import ContentTypeField
+from netbox.api.serializers import OrganizationalModelSerializer, PrimaryModelSerializer
 from secrets.constants import SECRET_ASSIGNMENT_MODELS
 from secrets.constants import SECRET_ASSIGNMENT_MODELS
 from secrets.models import Secret, SecretRole
 from secrets.models import Secret, SecretRole
-from netbox.api import ContentTypeField, ValidatedModelSerializer
 from utilities.api import get_serializer_for_model
 from utilities.api import get_serializer_for_model
 from .nested_serializers import *
 from .nested_serializers import *
 
 
@@ -15,7 +14,7 @@ from .nested_serializers import *
 # Secrets
 # Secrets
 #
 #
 
 
-class SecretRoleSerializer(CustomFieldModelSerializer):
+class SecretRoleSerializer(OrganizationalModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='secrets-api:secretrole-detail')
     url = serializers.HyperlinkedIdentityField(view_name='secrets-api:secretrole-detail')
     secret_count = serializers.IntegerField(read_only=True)
     secret_count = serializers.IntegerField(read_only=True)
 
 
@@ -26,7 +25,7 @@ class SecretRoleSerializer(CustomFieldModelSerializer):
         ]
         ]
 
 
 
 
-class SecretSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
+class SecretSerializer(PrimaryModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='secrets-api:secret-detail')
     url = serializers.HyperlinkedIdentityField(view_name='secrets-api:secret-detail')
     assigned_object_type = ContentTypeField(
     assigned_object_type = ContentTypeField(
         queryset=ContentType.objects.filter(SECRET_ASSIGNMENT_MODELS)
         queryset=ContentType.objects.filter(SECRET_ASSIGNMENT_MODELS)

+ 2 - 3
netbox/tenancy/api/serializers.py

@@ -1,7 +1,6 @@
 from rest_framework import serializers
 from rest_framework import serializers
 
 
-from netbox.api.serializers import CustomFieldModelSerializer, NestedGroupModelSerializer
-from extras.api.serializers import TaggedObjectSerializer
+from netbox.api.serializers import NestedGroupModelSerializer, PrimaryModelSerializer
 from tenancy.models import Tenant, TenantGroup
 from tenancy.models import Tenant, TenantGroup
 from .nested_serializers import *
 from .nested_serializers import *
 
 
@@ -23,7 +22,7 @@ class TenantGroupSerializer(NestedGroupModelSerializer):
         ]
         ]
 
 
 
 
-class TenantSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
+class TenantSerializer(PrimaryModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenant-detail')
     url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenant-detail')
     group = NestedTenantGroupSerializer(required=False, allow_null=True)
     group = NestedTenantGroupSerializer(required=False, allow_null=True)
     circuit_count = serializers.IntegerField(read_only=True)
     circuit_count = serializers.IntegerField(read_only=True)

+ 4 - 6
netbox/virtualization/api/serializers.py

@@ -3,12 +3,10 @@ from rest_framework import serializers
 
 
 from dcim.api.nested_serializers import NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedSiteSerializer
 from dcim.api.nested_serializers import NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedSiteSerializer
 from dcim.choices import InterfaceModeChoices
 from dcim.choices import InterfaceModeChoices
-from netbox.api.serializers import CustomFieldModelSerializer
-from extras.api.serializers import TaggedObjectSerializer
 from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer
 from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer
 from ipam.models import VLAN
 from ipam.models import VLAN
 from netbox.api import ChoiceField, SerializedPKRelatedField
 from netbox.api import ChoiceField, SerializedPKRelatedField
-from netbox.api.serializers import OrganizationalModelSerializer, ValidatedModelSerializer
+from netbox.api.serializers import OrganizationalModelSerializer, PrimaryModelSerializer
 from tenancy.api.nested_serializers import NestedTenantSerializer
 from tenancy.api.nested_serializers import NestedTenantSerializer
 from virtualization.choices import *
 from virtualization.choices import *
 from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
 from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
@@ -41,7 +39,7 @@ class ClusterGroupSerializer(OrganizationalModelSerializer):
         ]
         ]
 
 
 
 
-class ClusterSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
+class ClusterSerializer(PrimaryModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail')
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail')
     type = NestedClusterTypeSerializer()
     type = NestedClusterTypeSerializer()
     group = NestedClusterGroupSerializer(required=False, allow_null=True)
     group = NestedClusterGroupSerializer(required=False, allow_null=True)
@@ -62,7 +60,7 @@ class ClusterSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
 # Virtual machines
 # Virtual machines
 #
 #
 
 
-class VirtualMachineSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
+class VirtualMachineSerializer(PrimaryModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:virtualmachine-detail')
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:virtualmachine-detail')
     status = ChoiceField(choices=VirtualMachineStatusChoices, required=False)
     status = ChoiceField(choices=VirtualMachineStatusChoices, required=False)
     site = NestedSiteSerializer(read_only=True)
     site = NestedSiteSerializer(read_only=True)
@@ -103,7 +101,7 @@ class VirtualMachineWithConfigContextSerializer(VirtualMachineSerializer):
 # VM interfaces
 # VM interfaces
 #
 #
 
 
-class VMInterfaceSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
+class VMInterfaceSerializer(PrimaryModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:vminterface-detail')
     url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:vminterface-detail')
     virtual_machine = NestedVirtualMachineSerializer()
     virtual_machine = NestedVirtualMachineSerializer()
     mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False)
     mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False)