Просмотр исходного кода

Closes: #17936 - GFK serializer field (#20706)

* Establish GFKSerializerField and replace get_* methods in circuits.py

* Set read_only=True

* Apply GFKSerializerField to all matching SerializerMethodFields

* Use GFKSerializerField for ObjectChangeSerializer.changed_object and EventRuleSerializer.action_object
bctiemann 3 месяцев назад
Родитель
Сommit
bcffc383bf

+ 52 - 49
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",
@@ -211972,8 +211972,8 @@
                         "nullable": true
                     },
                     "termination": {
-                        "nullable": true,
-                        "readOnly": true
+                        "readOnly": true,
+                        "nullable": true
                     },
                     "port_speed": {
                         "type": "integer",
@@ -212176,8 +212176,8 @@
                         "format": "int64"
                     },
                     "member": {
-                        "nullable": true,
-                        "readOnly": true
+                        "readOnly": true,
+                        "nullable": true
                     },
                     "priority": {
                         "type": "object",
@@ -212561,8 +212561,8 @@
                         "nullable": true
                     },
                     "termination": {
-                        "nullable": true,
-                        "readOnly": true
+                        "readOnly": true,
+                        "nullable": true
                     },
                     "port_speed": {
                         "type": "integer",
@@ -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"
@@ -219029,9 +219028,8 @@
                         "nullable": true
                     },
                     "action_object": {
-                        "type": "object",
-                        "additionalProperties": {},
-                        "readOnly": true
+                        "readOnly": true,
+                        "nullable": true
                     },
                     "description": {
                         "type": "string",
@@ -219536,8 +219534,8 @@
                         "format": "int64"
                     },
                     "interface": {
-                        "nullable": true,
-                        "readOnly": true
+                        "readOnly": true,
+                        "nullable": true
                     },
                     "priority": {
                         "type": "integer",
@@ -221339,8 +221337,8 @@
                         "nullable": true
                     },
                     "assigned_object": {
-                        "nullable": true,
-                        "readOnly": true
+                        "readOnly": true,
+                        "nullable": true
                     },
                     "nat_inside": {
                         "allOf": [
@@ -222501,8 +222499,8 @@
                         "format": "int64"
                     },
                     "parent": {
-                        "nullable": true,
-                        "readOnly": true
+                        "readOnly": true,
+                        "nullable": true
                     },
                     "name": {
                         "type": "string",
@@ -225660,8 +225658,8 @@
                         "nullable": true
                     },
                     "component": {
-                        "nullable": true,
-                        "readOnly": true
+                        "readOnly": true,
+                        "nullable": true
                     },
                     "tags": {
                         "type": "array",
@@ -226044,8 +226042,8 @@
                         "nullable": true
                     },
                     "component": {
-                        "nullable": true,
-                        "readOnly": true
+                        "readOnly": true,
+                        "nullable": true
                     },
                     "created": {
                         "type": "string",
@@ -226327,8 +226325,8 @@
                         "format": "int64"
                     },
                     "assigned_object": {
-                        "nullable": true,
-                        "readOnly": true
+                        "readOnly": true,
+                        "nullable": true
                     },
                     "created": {
                         "type": "string",
@@ -226777,8 +226775,8 @@
                         "format": "int64"
                     },
                     "assigned_object": {
-                        "nullable": true,
-                        "readOnly": true
+                        "readOnly": true,
+                        "nullable": true
                     },
                     "tags": {
                         "type": "array",
@@ -227155,8 +227153,8 @@
                         "nullable": true
                     },
                     "assigned_object": {
-                        "nullable": true,
-                        "readOnly": true
+                        "readOnly": true,
+                        "nullable": true
                     },
                     "description": {
                         "type": "string",
@@ -229542,8 +229540,8 @@
                         "format": "int64"
                     },
                     "object": {
-                        "nullable": true,
-                        "readOnly": true
+                        "readOnly": true,
+                        "nullable": true
                     },
                     "user": {
                         "$ref": "#/components/schemas/BriefUser"
@@ -229796,7 +229794,11 @@
                         "format": "int64"
                     },
                     "changed_object": {
-                        "nullable": true,
+                        "readOnly": true,
+                        "nullable": true
+                    },
+                    "object_repr": {
+                        "type": "string",
                         "readOnly": true
                     },
                     "message": {
@@ -229821,6 +229823,7 @@
                     "display_url",
                     "id",
                     "message",
+                    "object_repr",
                     "postchange_data",
                     "prechange_data",
                     "request_id",
@@ -248892,8 +248895,8 @@
                         "nullable": true
                     },
                     "scope": {
-                        "nullable": true,
-                        "readOnly": true
+                        "readOnly": true,
+                        "nullable": true
                     },
                     "tenant": {
                         "allOf": [
@@ -252745,8 +252748,8 @@
                         "format": "int64"
                     },
                     "parent": {
-                        "nullable": true,
-                        "readOnly": true
+                        "readOnly": true,
+                        "nullable": true
                     },
                     "name": {
                         "type": "string",
@@ -253669,8 +253672,8 @@
                         "format": "int64"
                     },
                     "object": {
-                        "nullable": true,
-                        "readOnly": true
+                        "readOnly": true,
+                        "nullable": true
                     },
                     "user": {
                         "$ref": "#/components/schemas/BriefUser"
@@ -255193,8 +255196,8 @@
                         "nullable": true
                     },
                     "termination": {
-                        "nullable": true,
-                        "readOnly": true
+                        "readOnly": true,
+                        "nullable": true
                     },
                     "outside_ip": {
                         "allOf": [
@@ -255671,8 +255674,8 @@
                         "nullable": true
                     },
                     "scope": {
-                        "nullable": true,
-                        "readOnly": true
+                        "readOnly": true,
+                        "nullable": true
                     },
                     "vid_ranges": {
                         "type": "array",
@@ -258528,8 +258531,8 @@
                         "nullable": true
                     },
                     "scope": {
-                        "nullable": true,
-                        "readOnly": true
+                        "readOnly": true,
+                        "nullable": true
                     },
                     "tenant": {
                         "allOf": [

+ 4 - 29
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 circuits.models import (
 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.gfk_fields 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(read_only=True)
 
     class Meta:
         model = CircuitTermination
@@ -64,14 +63,6 @@ class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
             '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(read_only=True)
 
     class Meta:
         model = CircuitTermination
@@ -146,20 +137,12 @@ class CircuitTerminationSerializer(NetBoxModelSerializer, CabledObjectSerializer
         ]
         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(read_only=True)
 
     class Meta:
         model = CircuitGroupAssignment
@@ -169,14 +152,6 @@ class CircuitGroupAssignmentSerializer(CircuitGroupAssignmentSerializer_):
         ]
         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):
 

+ 7 - 22
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 ObjectChangeSerializer(BaseModelSerializer):
         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

+ 2 - 9
netbox/dcim/api/serializers_/cables.py

@@ -5,6 +5,7 @@ from rest_framework import serializers
 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 CableTerminationSerializer(NetBoxModelSerializer):
         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)

+ 2 - 11
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_.vlans import VLANSerializer, VLANTranslationPolicySer
 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 InventoryItemSerializer(NetBoxModelSerializer):
             '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

+ 2 - 10
netbox/dcim/api/serializers_/devices.py

@@ -11,9 +11,9 @@ from dcim.models import Device, DeviceBay, MACAddress, Module, VirtualDeviceCont
 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 MACAddressSerializer(PrimaryModelSerializer):
             '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

+ 2 - 11
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 @@ from dcim.models import (
     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 InventoryItemTemplateSerializer(ComponentTemplateSerializer):
             '_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

+ 2 - 9
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 @@ class ImageAttachmentSerializer(ValidatedModelSerializer):
         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

+ 2 - 11
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 BookmarkSerializer(ValidatedModelSerializer):
             '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

+ 2 - 18
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 EventRuleSerializer(OwnerMixin, NetBoxModelSerializer):
         ]
         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

+ 2 - 9
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 @@ class JournalEntrySerializer(NetBoxModelSerializer):
                 )
 
         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

+ 3 - 18
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 NotificationSerializer(ValidatedModelSerializer):
         ]
         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 SubscriptionSerializer(ValidatedModelSerializer):
             '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

+ 2 - 13
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 FHRPGroupAssignmentSerializer(NetBoxModelSerializer):
             '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

+ 3 - 20
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.choices import *
 from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS
 from ipam.models import Aggregate, IPAddress, IPRange, Prefix
 from netbox.api.fields import ChoiceField, ContentTypeField
+from netbox.api.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 PrefixSerializer(PrimaryModelSerializer):
         ]
         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 IPAddressSerializer(PrimaryModelSerializer):
         ]
         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):
     """

+ 2 - 12
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 ServiceSerializer(PrimaryModelSerializer):
             '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

+ 2 - 11
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.choices import *
 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 VLANGroupSerializer(OrganizationalModelSerializer):
         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)

+ 18 - 0
netbox/netbox/api/gfk_fields.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

+ 2 - 10
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 ContactAssignmentSerializer(NetBoxModelSerializer):
             '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

+ 2 - 11
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 ClusterSerializer(PrimaryModelSerializer):
             '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

+ 2 - 10
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 L2VPNTerminationSerializer(NetBoxModelSerializer):
             '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

+ 2 - 14
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 TunnelTerminationSerializer(NetBoxModelSerializer):
             '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

+ 2 - 11
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 WirelessLANSerializer(PrimaryModelSerializer):
             '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