From 182a927d34716e60acb1a68eb6236811a6251fa2 Mon Sep 17 00:00:00 2001 From: Brian Tiemann Date: Tue, 28 Oct 2025 14:23:19 -0400 Subject: [PATCH 1/4] Establish GFKSerializerField and replace get_* methods in circuits.py --- contrib/openapi.json | 183 ++++++++++++++++++- netbox/circuits/api/serializers_/circuits.py | 33 +--- netbox/netbox/api/fields2.py | 18 ++ 3 files changed, 199 insertions(+), 35 deletions(-) create mode 100644 netbox/netbox/api/fields2.py diff --git a/contrib/openapi.json b/contrib/openapi.json index 34263e6d299..920a07dab67 100644 --- a/contrib/openapi.json +++ b/contrib/openapi.json @@ -75760,6 +75760,157 @@ "operationId": "dcim_power_outlet_templates_list", "description": "Get a list of power outlet template objects.", "parameters": [ + { + "in": "query", + "name": "color", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "color__empty", + "schema": { + "type": "boolean" + } + }, + { + "in": "query", + "name": "color__ic", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "color__ie", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "color__iew", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "color__iregex", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "color__isw", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "color__n", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "color__nic", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "color__nie", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "color__niew", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "color__nisw", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, + { + "in": "query", + "name": "color__regex", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "explode": true, + "style": "form" + }, { "in": "query", "name": "created", @@ -211805,8 +211956,8 @@ "nullable": true }, "termination": { - "nullable": true, - "readOnly": true + "readOnly": true, + "nullable": true }, "port_speed": { "type": "integer", @@ -212009,8 +212160,8 @@ "format": "int64" }, "member": { - "nullable": true, - "readOnly": true + "readOnly": true, + "nullable": true }, "priority": { "type": "object", @@ -212394,8 +212545,8 @@ "nullable": true }, "termination": { - "nullable": true, - "readOnly": true + "readOnly": true, + "nullable": true }, "port_speed": { "type": "integer", @@ -242458,6 +242609,11 @@ "x-spec-enum-id": "8f9617d2648ab261", "nullable": true }, + "color": { + "type": "string", + "pattern": "^[0-9a-f]{6}$", + "maxLength": 6 + }, "power_port": { "oneOf": [ { @@ -247155,6 +247311,11 @@ }, "nullable": true }, + "color": { + "type": "string", + "pattern": "^[0-9a-f]{6}$", + "maxLength": 6 + }, "power_port": { "allOf": [ { @@ -247371,6 +247532,11 @@ "x-spec-enum-id": "8f9617d2648ab261", "nullable": true }, + "color": { + "type": "string", + "pattern": "^[0-9a-f]{6}$", + "maxLength": 6 + }, "power_port": { "oneOf": [ { @@ -264451,6 +264617,11 @@ "x-spec-enum-id": "8f9617d2648ab261", "nullable": true }, + "color": { + "type": "string", + "pattern": "^[0-9a-f]{6}$", + "maxLength": 6 + }, "power_port": { "oneOf": [ { diff --git a/netbox/circuits/api/serializers_/circuits.py b/netbox/circuits/api/serializers_/circuits.py index bc84f66fa69..bc86f5271a3 100644 --- a/netbox/circuits/api/serializers_/circuits.py +++ b/netbox/circuits/api/serializers_/circuits.py @@ -1,5 +1,4 @@ from django.contrib.contenttypes.models import ContentType -from drf_spectacular.utils import extend_schema_field from rest_framework import serializers from circuits.choices import CircuitPriorityChoices, CircuitStatusChoices, VirtualCircuitTerminationRoleChoices @@ -11,12 +10,12 @@ from dcim.api.serializers_.device_components import InterfaceSerializer from dcim.api.serializers_.cables import CabledObjectSerializer from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField +from netbox.api.fields2 import GFKSerializerField from netbox.api.serializers import ( NetBoxModelSerializer, OrganizationalModelSerializer, PrimaryModelSerializer, WritableNestedSerializer, ) from netbox.choices import DistanceUnitChoices from tenancy.api.serializers_.tenants import TenantSerializer -from utilities.api import get_serializer_for_model from .providers import ProviderAccountSerializer, ProviderNetworkSerializer, ProviderSerializer __all__ = ( @@ -55,7 +54,7 @@ class CircuitCircuitTerminationSerializer(WritableNestedSerializer): default=None ) termination_id = serializers.IntegerField(allow_null=True, required=False, default=None) - termination = serializers.SerializerMethodField(read_only=True) + termination = GFKSerializerField() class Meta: model = CircuitTermination @@ -64,14 +63,6 @@ class Meta: 'upstream_speed', 'xconnect_id', 'description', ] - @extend_schema_field(serializers.JSONField(allow_null=True)) - def get_termination(self, obj): - if obj.termination_id is None: - return None - serializer = get_serializer_for_model(obj.termination) - context = {'request': self.context['request']} - return serializer(obj.termination, nested=True, context=context).data - class CircuitGroupSerializer(OrganizationalModelSerializer): tenant = TenantSerializer(nested=True, required=False, allow_null=True) @@ -134,7 +125,7 @@ class CircuitTerminationSerializer(NetBoxModelSerializer, CabledObjectSerializer default=None ) termination_id = serializers.IntegerField(allow_null=True, required=False, default=None) - termination = serializers.SerializerMethodField(read_only=True) + termination = GFKSerializerField() class Meta: model = CircuitTermination @@ -146,20 +137,12 @@ class Meta: ] brief_fields = ('id', 'url', 'display', 'circuit', 'term_side', 'description', 'cable', '_occupied') - @extend_schema_field(serializers.JSONField(allow_null=True)) - def get_termination(self, obj): - if obj.termination_id is None: - return None - serializer = get_serializer_for_model(obj.termination) - context = {'request': self.context['request']} - return serializer(obj.termination, nested=True, context=context).data - class CircuitGroupAssignmentSerializer(CircuitGroupAssignmentSerializer_): member_type = ContentTypeField( queryset=ContentType.objects.filter(CIRCUIT_GROUP_ASSIGNMENT_MEMBER_MODELS) ) - member = serializers.SerializerMethodField(read_only=True) + member = GFKSerializerField() class Meta: model = CircuitGroupAssignment @@ -169,14 +152,6 @@ class Meta: ] brief_fields = ('id', 'url', 'display', 'group', 'member_type', 'member_id', 'member', 'priority') - @extend_schema_field(serializers.JSONField(allow_null=True)) - def get_member(self, obj): - if obj.member_id is None: - return None - serializer = get_serializer_for_model(obj.member) - context = {'request': self.context['request']} - return serializer(obj.member, nested=True, context=context).data - class VirtualCircuitTypeSerializer(OrganizationalModelSerializer): diff --git a/netbox/netbox/api/fields2.py b/netbox/netbox/api/fields2.py new file mode 100644 index 00000000000..d2bbeac4c56 --- /dev/null +++ b/netbox/netbox/api/fields2.py @@ -0,0 +1,18 @@ +from drf_spectacular.utils import extend_schema_field +from rest_framework import serializers +from utilities.api import get_serializer_for_model + +__all__ = ( + 'GFKSerializerField', +) + + +@extend_schema_field(serializers.JSONField(allow_null=True, read_only=True)) +class GFKSerializerField(serializers.Field): + + def to_representation(self, instance, **kwargs): + if instance is None: + return None + serializer = get_serializer_for_model(instance) + context = {'request': self.context['request']} + return serializer(instance, nested=True, context=context).data From 9e8fa6e61fb771a2e8ff25e36d82f480f5d4d11a Mon Sep 17 00:00:00 2001 From: Brian Tiemann Date: Tue, 28 Oct 2025 14:44:56 -0400 Subject: [PATCH 2/4] Set read_only=True --- netbox/circuits/api/serializers_/circuits.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/netbox/circuits/api/serializers_/circuits.py b/netbox/circuits/api/serializers_/circuits.py index bc86f5271a3..b0a6f45ceba 100644 --- a/netbox/circuits/api/serializers_/circuits.py +++ b/netbox/circuits/api/serializers_/circuits.py @@ -54,7 +54,7 @@ class CircuitCircuitTerminationSerializer(WritableNestedSerializer): default=None ) termination_id = serializers.IntegerField(allow_null=True, required=False, default=None) - termination = GFKSerializerField() + termination = GFKSerializerField(read_only=True) class Meta: model = CircuitTermination @@ -125,7 +125,7 @@ class CircuitTerminationSerializer(NetBoxModelSerializer, CabledObjectSerializer default=None ) termination_id = serializers.IntegerField(allow_null=True, required=False, default=None) - termination = GFKSerializerField() + termination = GFKSerializerField(read_only=True) class Meta: model = CircuitTermination @@ -142,7 +142,7 @@ class CircuitGroupAssignmentSerializer(CircuitGroupAssignmentSerializer_): member_type = ContentTypeField( queryset=ContentType.objects.filter(CIRCUIT_GROUP_ASSIGNMENT_MEMBER_MODELS) ) - member = GFKSerializerField() + member = GFKSerializerField(read_only=True) class Meta: model = CircuitGroupAssignment From 7b5349e1dc4c17a05600629b1be0039e0878caa0 Mon Sep 17 00:00:00 2001 From: Brian Tiemann Date: Wed, 29 Oct 2025 16:03:23 -0400 Subject: [PATCH 3/4] Apply GFKSerializerField to all matching SerializerMethodFields --- contrib/openapi.json | 77 +++++++++---------- netbox/circuits/api/serializers_/circuits.py | 2 +- netbox/dcim/api/serializers_/cables.py | 11 +-- .../api/serializers_/device_components.py | 13 +--- netbox/dcim/api/serializers_/devices.py | 12 +-- .../api/serializers_/devicetype_components.py | 13 +--- netbox/extras/api/serializers_/attachments.py | 11 +-- netbox/extras/api/serializers_/bookmarks.py | 13 +--- netbox/extras/api/serializers_/journaling.py | 11 +-- .../extras/api/serializers_/notifications.py | 21 +---- netbox/ipam/api/serializers_/fhrpgroups.py | 15 +--- netbox/ipam/api/serializers_/ip.py | 23 +----- netbox/ipam/api/serializers_/services.py | 14 +--- netbox/ipam/api/serializers_/vlans.py | 13 +--- .../netbox/api/{fields2.py => gfk_fields.py} | 0 netbox/tenancy/api/serializers_/contacts.py | 12 +-- .../api/serializers_/clusters.py | 13 +--- netbox/vpn/api/serializers_/l2vpn.py | 12 +-- netbox/vpn/api/serializers_/tunnels.py | 16 +--- .../wireless/api/serializers_/wirelesslans.py | 13 +--- 20 files changed, 75 insertions(+), 240 deletions(-) rename netbox/netbox/api/{fields2.py => gfk_fields.py} (100%) diff --git a/contrib/openapi.json b/contrib/openapi.json index c5d0db25c89..9c2a601c226 100644 --- a/contrib/openapi.json +++ b/contrib/openapi.json @@ -206935,8 +206935,8 @@ "format": "int64" }, "object": { - "nullable": true, - "readOnly": true + "readOnly": true, + "nullable": true }, "user": { "$ref": "#/components/schemas/BriefUser" @@ -211700,8 +211700,8 @@ "readOnly": true }, "termination": { - "nullable": true, - "readOnly": true + "readOnly": true, + "nullable": true }, "created": { "type": "string", @@ -212978,8 +212978,8 @@ "nullable": true }, "scope": { - "nullable": true, - "readOnly": true + "readOnly": true, + "nullable": true }, "description": { "type": "string", @@ -215293,9 +215293,8 @@ "format": "int64" }, "object": { - "type": "object", - "additionalProperties": {}, - "readOnly": true + "readOnly": true, + "nullable": true }, "contact": { "$ref": "#/components/schemas/BriefContact" @@ -219536,8 +219535,8 @@ "format": "int64" }, "interface": { - "nullable": true, - "readOnly": true + "readOnly": true, + "nullable": true }, "priority": { "type": "integer", @@ -221339,8 +221338,8 @@ "nullable": true }, "assigned_object": { - "nullable": true, - "readOnly": true + "readOnly": true, + "nullable": true }, "nat_inside": { "allOf": [ @@ -222501,8 +222500,8 @@ "format": "int64" }, "parent": { - "nullable": true, - "readOnly": true + "readOnly": true, + "nullable": true }, "name": { "type": "string", @@ -225660,8 +225659,8 @@ "nullable": true }, "component": { - "nullable": true, - "readOnly": true + "readOnly": true, + "nullable": true }, "tags": { "type": "array", @@ -226044,8 +226043,8 @@ "nullable": true }, "component": { - "nullable": true, - "readOnly": true + "readOnly": true, + "nullable": true }, "created": { "type": "string", @@ -226327,8 +226326,8 @@ "format": "int64" }, "assigned_object": { - "nullable": true, - "readOnly": true + "readOnly": true, + "nullable": true }, "created": { "type": "string", @@ -226777,8 +226776,8 @@ "format": "int64" }, "assigned_object": { - "nullable": true, - "readOnly": true + "readOnly": true, + "nullable": true }, "tags": { "type": "array", @@ -227155,8 +227154,8 @@ "nullable": true }, "assigned_object": { - "nullable": true, - "readOnly": true + "readOnly": true, + "nullable": true }, "description": { "type": "string", @@ -229542,8 +229541,8 @@ "format": "int64" }, "object": { - "nullable": true, - "readOnly": true + "readOnly": true, + "nullable": true }, "user": { "$ref": "#/components/schemas/BriefUser" @@ -248892,8 +248891,8 @@ "nullable": true }, "scope": { - "nullable": true, - "readOnly": true + "readOnly": true, + "nullable": true }, "tenant": { "allOf": [ @@ -252745,8 +252744,8 @@ "format": "int64" }, "parent": { - "nullable": true, - "readOnly": true + "readOnly": true, + "nullable": true }, "name": { "type": "string", @@ -253669,8 +253668,8 @@ "format": "int64" }, "object": { - "nullable": true, - "readOnly": true + "readOnly": true, + "nullable": true }, "user": { "$ref": "#/components/schemas/BriefUser" @@ -255193,8 +255192,8 @@ "nullable": true }, "termination": { - "nullable": true, - "readOnly": true + "readOnly": true, + "nullable": true }, "outside_ip": { "allOf": [ @@ -255671,8 +255670,8 @@ "nullable": true }, "scope": { - "nullable": true, - "readOnly": true + "readOnly": true, + "nullable": true }, "vid_ranges": { "type": "array", @@ -258528,8 +258527,8 @@ "nullable": true }, "scope": { - "nullable": true, - "readOnly": true + "readOnly": true, + "nullable": true }, "tenant": { "allOf": [ diff --git a/netbox/circuits/api/serializers_/circuits.py b/netbox/circuits/api/serializers_/circuits.py index b0a6f45ceba..79e50da28a2 100644 --- a/netbox/circuits/api/serializers_/circuits.py +++ b/netbox/circuits/api/serializers_/circuits.py @@ -10,7 +10,7 @@ from dcim.api.serializers_.device_components import InterfaceSerializer from dcim.api.serializers_.cables import CabledObjectSerializer from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField -from netbox.api.fields2 import GFKSerializerField +from netbox.api.gfk_fields import GFKSerializerField from netbox.api.serializers import ( NetBoxModelSerializer, OrganizationalModelSerializer, PrimaryModelSerializer, WritableNestedSerializer, ) diff --git a/netbox/dcim/api/serializers_/cables.py b/netbox/dcim/api/serializers_/cables.py index d72b0cbec1d..5f3017368de 100644 --- a/netbox/dcim/api/serializers_/cables.py +++ b/netbox/dcim/api/serializers_/cables.py @@ -5,6 +5,7 @@ from dcim.choices import * from dcim.models import Cable, CablePath, CableTermination from netbox.api.fields import ChoiceField, ContentTypeField +from netbox.api.gfk_fields import GFKSerializerField from netbox.api.serializers import ( BaseModelSerializer, GenericObjectSerializer, NetBoxModelSerializer, PrimaryModelSerializer, ) @@ -53,9 +54,7 @@ class CableTerminationSerializer(NetBoxModelSerializer): termination_type = ContentTypeField( read_only=True, ) - termination = serializers.SerializerMethodField( - read_only=True, - ) + termination = GFKSerializerField(read_only=True) class Meta: model = CableTermination @@ -66,12 +65,6 @@ class Meta: read_only_fields = fields brief_fields = ('id', 'url', 'display', 'cable', 'cable_end', 'termination_type', 'termination_id') - @extend_schema_field(serializers.JSONField(allow_null=True)) - def get_termination(self, obj): - serializer = get_serializer_for_model(obj.termination) - context = {'request': self.context['request']} - return serializer(obj.termination, nested=True, context=context).data - class CablePathSerializer(serializers.ModelSerializer): path = serializers.SerializerMethodField(read_only=True) diff --git a/netbox/dcim/api/serializers_/device_components.py b/netbox/dcim/api/serializers_/device_components.py index 468d75af968..b26cf9bbba3 100644 --- a/netbox/dcim/api/serializers_/device_components.py +++ b/netbox/dcim/api/serializers_/device_components.py @@ -1,6 +1,5 @@ from django.utils.translation import gettext as _ from django.contrib.contenttypes.models import ContentType -from drf_spectacular.utils import extend_schema_field from rest_framework import serializers from dcim.choices import * @@ -13,8 +12,8 @@ from ipam.api.serializers_.vrfs import VRFSerializer from ipam.models import VLAN from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField +from netbox.api.gfk_fields import GFKSerializerField from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer -from utilities.api import get_serializer_for_model from vpn.api.serializers_.l2vpn import L2VPNTerminationSerializer from wireless.api.serializers_.nested import NestedWirelessLinkSerializer from wireless.api.serializers_.wirelesslans import WirelessLANSerializer @@ -394,7 +393,7 @@ class InventoryItemSerializer(NetBoxModelSerializer): required=False, allow_null=True ) - component = serializers.SerializerMethodField(read_only=True, allow_null=True) + component = GFKSerializerField(read_only=True) _depth = serializers.IntegerField(source='level', read_only=True) status = ChoiceField(choices=InventoryItemStatusChoices, required=False) @@ -406,11 +405,3 @@ class Meta: '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) - context = {'request': self.context['request']} - return serializer(obj.component, nested=True, context=context).data diff --git a/netbox/dcim/api/serializers_/devices.py b/netbox/dcim/api/serializers_/devices.py index 2a3e0cd4212..c8de28fd8d7 100644 --- a/netbox/dcim/api/serializers_/devices.py +++ b/netbox/dcim/api/serializers_/devices.py @@ -11,9 +11,9 @@ from extras.api.serializers_.configtemplates import ConfigTemplateSerializer from ipam.api.serializers_.ip import IPAddressSerializer from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField +from netbox.api.gfk_fields import GFKSerializerField from netbox.api.serializers import PrimaryModelSerializer from tenancy.api.serializers_.tenants import TenantSerializer -from utilities.api import get_serializer_for_model from virtualization.api.serializers_.clusters import ClusterSerializer from .devicetypes import * from .nested import NestedDeviceBaySerializer, NestedDeviceSerializer, NestedModuleBaySerializer @@ -165,7 +165,7 @@ class MACAddressSerializer(PrimaryModelSerializer): required=False, allow_null=True ) - assigned_object = serializers.SerializerMethodField(read_only=True) + assigned_object = GFKSerializerField(read_only=True) class Meta: model = MACAddress @@ -174,11 +174,3 @@ class Meta: 'assigned_object', 'description', 'owner', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', ] brief_fields = ('id', 'url', 'display', 'mac_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) - context = {'request': self.context['request']} - return serializer(obj.assigned_object, nested=True, context=context).data diff --git a/netbox/dcim/api/serializers_/devicetype_components.py b/netbox/dcim/api/serializers_/devicetype_components.py index 5f2d4b36aa8..b44565d65c2 100644 --- a/netbox/dcim/api/serializers_/devicetype_components.py +++ b/netbox/dcim/api/serializers_/devicetype_components.py @@ -1,5 +1,4 @@ from django.contrib.contenttypes.models import ContentType -from drf_spectacular.utils import extend_schema_field from rest_framework import serializers from dcim.choices import * @@ -9,8 +8,8 @@ InventoryItemTemplate, ModuleBayTemplate, PowerOutletTemplate, PowerPortTemplate, RearPortTemplate, ) from netbox.api.fields import ChoiceField, ContentTypeField +from netbox.api.gfk_fields import GFKSerializerField from netbox.api.serializers import ChangeLogMessageSerializer, ValidatedModelSerializer -from utilities.api import get_serializer_for_model from wireless.choices import * from .devicetypes import DeviceTypeSerializer, ModuleTypeSerializer from .manufacturers import ManufacturerSerializer @@ -313,7 +312,7 @@ class InventoryItemTemplateSerializer(ComponentTemplateSerializer): required=False, allow_null=True ) - component = serializers.SerializerMethodField(read_only=True, allow_null=True) + component = GFKSerializerField(read_only=True) _depth = serializers.IntegerField(source='level', read_only=True) class Meta: @@ -324,11 +323,3 @@ class Meta: '_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) - context = {'request': self.context['request']} - return serializer(obj.component, nested=True, context=context).data diff --git a/netbox/extras/api/serializers_/attachments.py b/netbox/extras/api/serializers_/attachments.py index 6507a12bedf..613825203ea 100644 --- a/netbox/extras/api/serializers_/attachments.py +++ b/netbox/extras/api/serializers_/attachments.py @@ -1,12 +1,11 @@ from django.core.exceptions import ObjectDoesNotExist -from drf_spectacular.utils import extend_schema_field from rest_framework import serializers from core.models import ObjectType from extras.models import ImageAttachment from netbox.api.fields import ContentTypeField +from netbox.api.gfk_fields import GFKSerializerField from netbox.api.serializers import ValidatedModelSerializer -from utilities.api import get_serializer_for_model __all__ = ( 'ImageAttachmentSerializer', @@ -17,7 +16,7 @@ class ImageAttachmentSerializer(ValidatedModelSerializer): object_type = ContentTypeField( queryset=ObjectType.objects.all() ) - parent = serializers.SerializerMethodField(read_only=True) + parent = GFKSerializerField(read_only=True) image_width = serializers.IntegerField(read_only=True) image_height = serializers.IntegerField(read_only=True) @@ -43,9 +42,3 @@ def validate(self, data): 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) - 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 a404d83c347..a1c56f5f0be 100644 --- a/netbox/extras/api/serializers_/bookmarks.py +++ b/netbox/extras/api/serializers_/bookmarks.py @@ -1,12 +1,9 @@ -from drf_spectacular.utils import extend_schema_field -from rest_framework import serializers - from core.models import ObjectType from extras.models import Bookmark from netbox.api.fields import ContentTypeField +from netbox.api.gfk_fields import GFKSerializerField from netbox.api.serializers import ValidatedModelSerializer from users.api.serializers_.users import UserSerializer -from utilities.api import get_serializer_for_model __all__ = ( 'BookmarkSerializer', @@ -17,7 +14,7 @@ class BookmarkSerializer(ValidatedModelSerializer): object_type = ContentTypeField( queryset=ObjectType.objects.with_feature('bookmarks'), ) - object = serializers.SerializerMethodField(read_only=True) + object = GFKSerializerField(read_only=True) user = UserSerializer(nested=True) class Meta: @@ -26,9 +23,3 @@ class Meta: '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) - context = {'request': self.context['request']} - return serializer(instance.object, nested=True, context=context).data diff --git a/netbox/extras/api/serializers_/journaling.py b/netbox/extras/api/serializers_/journaling.py index cba56fc32fc..03ec3445126 100644 --- a/netbox/extras/api/serializers_/journaling.py +++ b/netbox/extras/api/serializers_/journaling.py @@ -1,14 +1,13 @@ from django.core.exceptions import ObjectDoesNotExist -from drf_spectacular.utils import extend_schema_field from rest_framework import serializers from core.models import ObjectType from extras.choices import * from extras.models import JournalEntry from netbox.api.fields import ChoiceField, ContentTypeField +from netbox.api.gfk_fields import GFKSerializerField from netbox.api.serializers import NetBoxModelSerializer from users.models import User -from utilities.api import get_serializer_for_model __all__ = ( 'JournalEntrySerializer', @@ -19,7 +18,7 @@ class JournalEntrySerializer(NetBoxModelSerializer): assigned_object_type = ContentTypeField( queryset=ObjectType.objects.all() ) - assigned_object = serializers.SerializerMethodField(read_only=True) + assigned_object = GFKSerializerField(read_only=True) created_by = serializers.PrimaryKeyRelatedField( allow_null=True, queryset=User.objects.all(), @@ -51,9 +50,3 @@ def validate(self, data): ) return super().validate(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()) - context = {'request': self.context['request']} - return serializer(instance.assigned_object, nested=True, context=context).data diff --git a/netbox/extras/api/serializers_/notifications.py b/netbox/extras/api/serializers_/notifications.py index 9f0c7cff3f7..4c9d0816987 100644 --- a/netbox/extras/api/serializers_/notifications.py +++ b/netbox/extras/api/serializers_/notifications.py @@ -1,13 +1,10 @@ -from drf_spectacular.utils import extend_schema_field -from rest_framework import serializers - from core.models import ObjectType from extras.models import Notification, NotificationGroup, Subscription from netbox.api.fields import ContentTypeField, SerializedPKRelatedField +from netbox.api.gfk_fields import GFKSerializerField from netbox.api.serializers import ChangeLogMessageSerializer, ValidatedModelSerializer from users.api.serializers_.users import GroupSerializer, UserSerializer from users.models import Group, User -from utilities.api import get_serializer_for_model __all__ = ( 'NotificationSerializer', @@ -20,7 +17,7 @@ class NotificationSerializer(ValidatedModelSerializer): object_type = ContentTypeField( queryset=ObjectType.objects.with_feature('notifications'), ) - object = serializers.SerializerMethodField(read_only=True) + object = GFKSerializerField(read_only=True) user = UserSerializer(nested=True) class Meta: @@ -30,12 +27,6 @@ class Meta: ] brief_fields = ('id', 'url', 'display', 'object_type', 'object_id', 'user', 'read', 'event_type') - @extend_schema_field(serializers.JSONField(allow_null=True)) - def get_object(self, instance): - serializer = get_serializer_for_model(instance.object) - context = {'request': self.context['request']} - return serializer(instance.object, nested=True, context=context).data - class NotificationGroupSerializer(ChangeLogMessageSerializer, ValidatedModelSerializer): groups = SerializedPKRelatedField( @@ -65,7 +56,7 @@ class SubscriptionSerializer(ValidatedModelSerializer): object_type = ContentTypeField( queryset=ObjectType.objects.with_feature('notifications'), ) - object = serializers.SerializerMethodField(read_only=True) + object = GFKSerializerField(read_only=True) user = UserSerializer(nested=True) class Meta: @@ -74,9 +65,3 @@ class Meta: 'id', 'url', 'display', 'object_type', 'object_id', 'object', 'user', 'created', ] brief_fields = ('id', 'url', 'display', 'object_type', 'object_id', 'user') - - @extend_schema_field(serializers.JSONField(allow_null=True)) - def get_object(self, instance): - 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/ipam/api/serializers_/fhrpgroups.py b/netbox/ipam/api/serializers_/fhrpgroups.py index 82750f1bab9..cf87706828b 100644 --- a/netbox/ipam/api/serializers_/fhrpgroups.py +++ b/netbox/ipam/api/serializers_/fhrpgroups.py @@ -1,11 +1,8 @@ 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.gfk_fields import GFKSerializerField from netbox.api.serializers import NetBoxModelSerializer, PrimaryModelSerializer -from utilities.api import get_serializer_for_model from .ip import IPAddressSerializer __all__ = ( @@ -31,7 +28,7 @@ class FHRPGroupAssignmentSerializer(NetBoxModelSerializer): interface_type = ContentTypeField( queryset=ContentType.objects.all() ) - interface = serializers.SerializerMethodField(read_only=True) + interface = GFKSerializerField(read_only=True) class Meta: model = FHRPGroupAssignment @@ -40,11 +37,3 @@ class Meta: '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) - context = {'request': self.context['request']} - 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 7dd277479c0..51f23f88d04 100644 --- a/netbox/ipam/api/serializers_/ip.py +++ b/netbox/ipam/api/serializers_/ip.py @@ -1,5 +1,4 @@ from django.contrib.contenttypes.models import ContentType -from drf_spectacular.utils import extend_schema_field from rest_framework import serializers from dcim.constants import LOCATION_SCOPE_TYPES @@ -7,9 +6,9 @@ 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.gfk_fields import GFKSerializerField from netbox.api.serializers import PrimaryModelSerializer from tenancy.api.serializers_.tenants import TenantSerializer -from utilities.api import get_serializer_for_model from .asns import RIRSerializer from .nested import NestedIPAddressSerializer from .roles import RoleSerializer @@ -55,7 +54,7 @@ class PrefixSerializer(PrimaryModelSerializer): default=None ) scope_id = serializers.IntegerField(allow_null=True, required=False, default=None) - scope = serializers.SerializerMethodField(read_only=True) + scope = GFKSerializerField(read_only=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) @@ -73,14 +72,6 @@ class Meta: ] brief_fields = ('id', 'url', 'display', 'family', 'prefix', 'description', '_depth') - @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) - context = {'request': self.context['request']} - return serializer(obj.scope, nested=True, context=context).data - class PrefixLengthSerializer(serializers.Serializer): @@ -168,7 +159,7 @@ class IPAddressSerializer(PrimaryModelSerializer): required=False, allow_null=True ) - assigned_object = serializers.SerializerMethodField(read_only=True) + assigned_object = GFKSerializerField(read_only=True) nat_inside = NestedIPAddressSerializer(required=False, allow_null=True) nat_outside = NestedIPAddressSerializer(many=True, read_only=True) @@ -181,14 +172,6 @@ class Meta: ] 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) - context = {'request': self.context['request']} - return serializer(obj.assigned_object, nested=True, context=context).data - class AvailableIPSerializer(serializers.Serializer): """ diff --git a/netbox/ipam/api/serializers_/services.py b/netbox/ipam/api/serializers_/services.py index ad7c3e00b19..824fc573891 100644 --- a/netbox/ipam/api/serializers_/services.py +++ b/netbox/ipam/api/serializers_/services.py @@ -1,13 +1,11 @@ from django.contrib.contenttypes.models import ContentType -from drf_spectacular.utils import extend_schema_field -from rest_framework import serializers from ipam.choices import * from ipam.constants import SERVICE_ASSIGNMENT_MODELS from ipam.models import IPAddress, Service, ServiceTemplate from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField +from netbox.api.gfk_fields import GFKSerializerField from netbox.api.serializers import PrimaryModelSerializer -from utilities.api import get_serializer_for_model from .ip import IPAddressSerializer __all__ = ( @@ -40,7 +38,7 @@ class ServiceSerializer(PrimaryModelSerializer): parent_object_type = ContentTypeField( queryset=ContentType.objects.filter(SERVICE_ASSIGNMENT_MODELS) ) - parent = serializers.SerializerMethodField(read_only=True) + parent = GFKSerializerField(read_only=True) class Meta: model = Service @@ -50,11 +48,3 @@ class Meta: 'created', 'last_updated', ] brief_fields = ('id', 'url', 'display', 'name', 'protocol', 'ports', 'description') - - @extend_schema_field(serializers.JSONField(allow_null=True)) - def get_parent(self, obj): - if obj.parent is None: - return None - 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/ipam/api/serializers_/vlans.py b/netbox/ipam/api/serializers_/vlans.py index 7f2633e273c..133adb12cc5 100644 --- a/netbox/ipam/api/serializers_/vlans.py +++ b/netbox/ipam/api/serializers_/vlans.py @@ -1,5 +1,4 @@ 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 @@ -7,9 +6,9 @@ from ipam.constants import VLANGROUP_SCOPE_TYPES from ipam.models import VLAN, VLANGroup, VLANTranslationPolicy, VLANTranslationRule from netbox.api.fields import ChoiceField, ContentTypeField, IntegerRangeSerializer, RelatedObjectCountField +from netbox.api.gfk_fields import GFKSerializerField from netbox.api.serializers import NetBoxModelSerializer, OrganizationalModelSerializer, PrimaryModelSerializer from tenancy.api.serializers_.tenants import TenantSerializer -from utilities.api import get_serializer_for_model from vpn.api.serializers_.l2vpn import L2VPNTerminationSerializer from .nested import NestedVLANSerializer from .roles import RoleSerializer @@ -34,7 +33,7 @@ class VLANGroupSerializer(OrganizationalModelSerializer): default=None ) scope_id = serializers.IntegerField(allow_null=True, required=False, default=None) - scope = serializers.SerializerMethodField(read_only=True) + scope = GFKSerializerField(read_only=True) vid_ranges = IntegerRangeSerializer(many=True, required=False) utilization = serializers.CharField(read_only=True) tenant = TenantSerializer(nested=True, required=False, allow_null=True) @@ -52,14 +51,6 @@ class Meta: 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) - context = {'request': self.context['request']} - return serializer(obj.scope, nested=True, context=context).data - class VLANSerializer(PrimaryModelSerializer): site = SiteSerializer(nested=True, required=False, allow_null=True) diff --git a/netbox/netbox/api/fields2.py b/netbox/netbox/api/gfk_fields.py similarity index 100% rename from netbox/netbox/api/fields2.py rename to netbox/netbox/api/gfk_fields.py diff --git a/netbox/tenancy/api/serializers_/contacts.py b/netbox/tenancy/api/serializers_/contacts.py index 19c49613913..718af4768f0 100644 --- a/netbox/tenancy/api/serializers_/contacts.py +++ b/netbox/tenancy/api/serializers_/contacts.py @@ -1,15 +1,13 @@ 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, SerializedPKRelatedField +from netbox.api.gfk_fields import GFKSerializerField from netbox.api.serializers import ( NestedGroupModelSerializer, NetBoxModelSerializer, OrganizationalModelSerializer, PrimaryModelSerializer, ) from tenancy.choices import ContactPriorityChoices from tenancy.models import ContactAssignment, Contact, ContactGroup, ContactRole -from utilities.api import get_serializer_for_model from .nested import NestedContactGroupSerializer __all__ = ( @@ -66,7 +64,7 @@ class ContactAssignmentSerializer(NetBoxModelSerializer): object_type = ContentTypeField( queryset=ContentType.objects.all() ) - object = serializers.SerializerMethodField(read_only=True) + object = GFKSerializerField(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: '') @@ -78,9 +76,3 @@ class Meta: '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.object_type.model_class()) - context = {'request': self.context['request']} - return serializer(instance.object, nested=True, context=context).data diff --git a/netbox/virtualization/api/serializers_/clusters.py b/netbox/virtualization/api/serializers_/clusters.py index a48af9ce0ff..e352460bf31 100644 --- a/netbox/virtualization/api/serializers_/clusters.py +++ b/netbox/virtualization/api/serializers_/clusters.py @@ -1,13 +1,12 @@ from dcim.constants import LOCATION_SCOPE_TYPES from django.contrib.contenttypes.models import ContentType -from drf_spectacular.utils import extend_schema_field from rest_framework import serializers from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField +from netbox.api.gfk_fields import GFKSerializerField from netbox.api.serializers import OrganizationalModelSerializer, PrimaryModelSerializer from tenancy.api.serializers_.tenants import TenantSerializer from virtualization.choices import * from virtualization.models import Cluster, ClusterGroup, ClusterType -from utilities.api import get_serializer_for_model __all__ = ( 'ClusterGroupSerializer', @@ -58,7 +57,7 @@ class ClusterSerializer(PrimaryModelSerializer): default=None ) scope_id = serializers.IntegerField(allow_null=True, required=False, default=None) - scope = serializers.SerializerMethodField(read_only=True) + scope = GFKSerializerField(read_only=True) allocated_vcpus = serializers.DecimalField( read_only=True, max_digits=8, @@ -80,11 +79,3 @@ class Meta: 'device_count', 'virtualmachine_count', 'allocated_vcpus', 'allocated_memory', 'allocated_disk' ] brief_fields = ('id', 'url', 'display', 'name', 'description', 'virtualmachine_count') - - @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) - context = {'request': self.context['request']} - return serializer(obj.scope, nested=True, context=context).data diff --git a/netbox/vpn/api/serializers_/l2vpn.py b/netbox/vpn/api/serializers_/l2vpn.py index f9e9a9a9749..874ae342eb0 100644 --- a/netbox/vpn/api/serializers_/l2vpn.py +++ b/netbox/vpn/api/serializers_/l2vpn.py @@ -1,13 +1,11 @@ from django.contrib.contenttypes.models import ContentType -from drf_spectacular.utils import extend_schema_field -from rest_framework import serializers from ipam.api.serializers_.vrfs import RouteTargetSerializer from ipam.models import RouteTarget from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField +from netbox.api.gfk_fields import GFKSerializerField from netbox.api.serializers import NetBoxModelSerializer, PrimaryModelSerializer 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 @@ -53,7 +51,7 @@ class L2VPNTerminationSerializer(NetBoxModelSerializer): assigned_object_type = ContentTypeField( queryset=ContentType.objects.all() ) - assigned_object = serializers.SerializerMethodField(read_only=True) + assigned_object = GFKSerializerField(read_only=True) class Meta: model = L2VPNTermination @@ -62,9 +60,3 @@ class Meta: '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) - context = {'request': self.context['request']} - 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 dfeb0339ff0..5bb12d9b4e2 100644 --- a/netbox/vpn/api/serializers_/tunnels.py +++ b/netbox/vpn/api/serializers_/tunnels.py @@ -1,12 +1,10 @@ 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.gfk_fields import GFKSerializerField from netbox.api.serializers import NetBoxModelSerializer, OrganizationalModelSerializer, PrimaryModelSerializer 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 @@ -83,9 +81,7 @@ class TunnelTerminationSerializer(NetBoxModelSerializer): termination_type = ContentTypeField( queryset=ContentType.objects.all() ) - termination = serializers.SerializerMethodField( - read_only=True - ) + termination = GFKSerializerField(read_only=True) outside_ip = IPAddressSerializer( nested=True, required=False, @@ -99,11 +95,3 @@ class Meta: '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): - if not obj.termination: - return None - serializer = get_serializer_for_model(obj.termination) - context = {'request': self.context['request']} - return serializer(obj.termination, nested=True, context=context).data diff --git a/netbox/wireless/api/serializers_/wirelesslans.py b/netbox/wireless/api/serializers_/wirelesslans.py index 7403fe860f8..517e93863c1 100644 --- a/netbox/wireless/api/serializers_/wirelesslans.py +++ b/netbox/wireless/api/serializers_/wirelesslans.py @@ -1,13 +1,12 @@ from django.contrib.contenttypes.models import ContentType -from drf_spectacular.utils import extend_schema_field from rest_framework import serializers from dcim.constants import LOCATION_SCOPE_TYPES from ipam.api.serializers_.vlans import VLANSerializer from netbox.api.fields import ChoiceField, ContentTypeField +from netbox.api.gfk_fields import GFKSerializerField from netbox.api.serializers import NestedGroupModelSerializer, PrimaryModelSerializer from tenancy.api.serializers_.tenants import TenantSerializer -from utilities.api import get_serializer_for_model from wireless.choices import * from wireless.models import WirelessLAN, WirelessLANGroup from .nested import NestedWirelessLANGroupSerializer @@ -47,7 +46,7 @@ class WirelessLANSerializer(PrimaryModelSerializer): default=None ) scope_id = serializers.IntegerField(allow_null=True, required=False, default=None) - scope = serializers.SerializerMethodField(read_only=True) + scope = GFKSerializerField(read_only=True) class Meta: model = WirelessLAN @@ -57,11 +56,3 @@ class Meta: 'tags', 'custom_fields', 'created', 'last_updated', ] brief_fields = ('id', 'url', 'display', 'ssid', 'description') - - @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) - context = {'request': self.context['request']} - return serializer(obj.scope, nested=True, context=context).data From 01362b8482a505d1788250f7a1ade0b3deee4c94 Mon Sep 17 00:00:00 2001 From: Brian Tiemann Date: Fri, 31 Oct 2025 22:33:16 -0400 Subject: [PATCH 4/4] Use GFKSerializerField for ObjectChangeSerializer.changed_object and EventRuleSerializer.action_object --- contrib/openapi.json | 12 +++++--- .../core/api/serializers_/change_logging.py | 29 +++++-------------- netbox/extras/api/serializers_/events.py | 20 ++----------- 3 files changed, 17 insertions(+), 44 deletions(-) diff --git a/contrib/openapi.json b/contrib/openapi.json index 9c2a601c226..a47a92f1b41 100644 --- a/contrib/openapi.json +++ b/contrib/openapi.json @@ -219028,9 +219028,8 @@ "nullable": true }, "action_object": { - "type": "object", - "additionalProperties": {}, - "readOnly": true + "readOnly": true, + "nullable": true }, "description": { "type": "string", @@ -229795,7 +229794,11 @@ "format": "int64" }, "changed_object": { - "nullable": true, + "readOnly": true, + "nullable": true + }, + "object_repr": { + "type": "string", "readOnly": true }, "message": { @@ -229820,6 +229823,7 @@ "display_url", "id", "message", + "object_repr", "postchange_data", "prechange_data", "request_id", diff --git a/netbox/core/api/serializers_/change_logging.py b/netbox/core/api/serializers_/change_logging.py index 575a849d5b4..4431b629894 100644 --- a/netbox/core/api/serializers_/change_logging.py +++ b/netbox/core/api/serializers_/change_logging.py @@ -1,13 +1,11 @@ -from drf_spectacular.utils import extend_schema_field from rest_framework import serializers from core.choices import * from core.models import ObjectChange -from netbox.api.exceptions import SerializerNotFound from netbox.api.fields import ChoiceField, ContentTypeField +from netbox.api.gfk_fields import GFKSerializerField from netbox.api.serializers import BaseModelSerializer from users.api.serializers_.users import UserSerializer -from utilities.api import get_serializer_for_model __all__ = ( 'ObjectChangeSerializer', @@ -26,7 +24,10 @@ class ObjectChangeSerializer(BaseModelSerializer): changed_object_type = ContentTypeField( read_only=True ) - changed_object = serializers.SerializerMethodField( + changed_object = GFKSerializerField( + read_only=True + ) + object_repr = serializers.CharField( read_only=True ) prechange_data = serializers.JSONField( @@ -44,22 +45,6 @@ class Meta: model = ObjectChange fields = [ 'id', 'url', 'display_url', 'display', 'time', 'user', 'user_name', 'request_id', 'action', - 'changed_object_type', 'changed_object_id', 'changed_object', 'message', 'prechange_data', - 'postchange_data', + 'changed_object_type', 'changed_object_id', 'changed_object', 'object_repr', 'message', + '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) - except SerializerNotFound: - return obj.object_repr - 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 ea33ef99df0..0d72874e70a 100644 --- a/netbox/extras/api/serializers_/events.py +++ b/netbox/extras/api/serializers_/events.py @@ -1,15 +1,10 @@ -from drf_spectacular.types import OpenApiTypes -from drf_spectacular.utils import extend_schema_field -from rest_framework import serializers - from core.models import ObjectType from extras.choices import * from extras.models import EventRule, Webhook from netbox.api.fields import ChoiceField, ContentTypeField +from netbox.api.gfk_fields import GFKSerializerField from netbox.api.serializers import NetBoxModelSerializer from users.api.serializers_.mixins import OwnerMixin -from utilities.api import get_serializer_for_model -from .scripts import ScriptSerializer __all__ = ( 'EventRuleSerializer', @@ -30,7 +25,7 @@ class EventRuleSerializer(OwnerMixin, NetBoxModelSerializer): action_object_type = ContentTypeField( queryset=ObjectType.objects.with_feature('event_rules'), ) - action_object = serializers.SerializerMethodField(read_only=True) + action_object = GFKSerializerField(read_only=True) class Meta: model = EventRule @@ -41,17 +36,6 @@ class Meta: ] 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 - return ScriptSerializer(script, nested=True, context=context).data - else: - serializer = get_serializer_for_model(instance.action_object_type.model_class()) - return serializer(instance.action_object, nested=True, context=context).data - # # Webhooks