mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-16 04:02:52 -06:00
Closes #9608: Move from drf-yasg to spectacular
Co-authored-by: arthanson <worldnomad@gmail.com> Co-authored-by: jeremystretch <jstretch@netboxlabs.com>
This commit is contained in:
parent
1be626e5ee
commit
ecd0c56554
@ -66,9 +66,9 @@ django-timezone-field
|
|||||||
# https://github.com/encode/django-rest-framework
|
# https://github.com/encode/django-rest-framework
|
||||||
djangorestframework
|
djangorestframework
|
||||||
|
|
||||||
# Swagger/OpenAPI schema generation for REST APIs
|
# Sane and flexible OpenAPI 3 schema generation for Django REST framework.
|
||||||
# https://github.com/axnsan12/drf-yasg
|
# https://github.com/tfranzel/drf-spectacular
|
||||||
drf-yasg[validation]
|
drf-spectacular
|
||||||
|
|
||||||
# RSS feed parser
|
# RSS feed parser
|
||||||
# https://github.com/kurtmckee/feedparser
|
# https://github.com/kurtmckee/feedparser
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from drf_spectacular.utils import extend_schema_field, extend_schema_serializer
|
||||||
|
from drf_spectacular.types import OpenApiTypes
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from circuits.models import *
|
from circuits.models import *
|
||||||
@ -29,6 +31,9 @@ class NestedProviderNetworkSerializer(WritableNestedSerializer):
|
|||||||
# Providers
|
# Providers
|
||||||
#
|
#
|
||||||
|
|
||||||
|
@extend_schema_serializer(
|
||||||
|
exclude_fields=('circuit_count',),
|
||||||
|
)
|
||||||
class NestedProviderSerializer(WritableNestedSerializer):
|
class NestedProviderSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
|
||||||
circuit_count = serializers.IntegerField(read_only=True)
|
circuit_count = serializers.IntegerField(read_only=True)
|
||||||
@ -54,6 +59,9 @@ class NestedProviderAccountSerializer(WritableNestedSerializer):
|
|||||||
# Circuits
|
# Circuits
|
||||||
#
|
#
|
||||||
|
|
||||||
|
@extend_schema_serializer(
|
||||||
|
exclude_fields=('circuit_count',),
|
||||||
|
)
|
||||||
class NestedCircuitTypeSerializer(WritableNestedSerializer):
|
class NestedCircuitTypeSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
|
||||||
circuit_count = serializers.IntegerField(read_only=True)
|
circuit_count = serializers.IntegerField(read_only=True)
|
||||||
|
@ -92,8 +92,8 @@ class CircuitTypeSerializer(NetBoxModelSerializer):
|
|||||||
|
|
||||||
class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
|
class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
|
||||||
site = NestedSiteSerializer()
|
site = NestedSiteSerializer(allow_null=True)
|
||||||
provider_network = NestedProviderNetworkSerializer()
|
provider_network = NestedProviderNetworkSerializer(allow_null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CircuitTermination
|
model = CircuitTermination
|
||||||
@ -110,8 +110,8 @@ class CircuitSerializer(NetBoxModelSerializer):
|
|||||||
status = ChoiceField(choices=CircuitStatusChoices, required=False)
|
status = ChoiceField(choices=CircuitStatusChoices, required=False)
|
||||||
type = NestedCircuitTypeSerializer()
|
type = NestedCircuitTypeSerializer()
|
||||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||||
termination_a = CircuitCircuitTerminationSerializer(read_only=True)
|
termination_a = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True)
|
||||||
termination_z = CircuitCircuitTerminationSerializer(read_only=True)
|
termination_z = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Circuit
|
model = Circuit
|
||||||
|
224
netbox/core/api/schema.py
Normal file
224
netbox/core/api/schema.py
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
import re
|
||||||
|
import typing
|
||||||
|
|
||||||
|
from drf_spectacular.extensions import (
|
||||||
|
OpenApiSerializerFieldExtension,
|
||||||
|
OpenApiViewExtension,
|
||||||
|
)
|
||||||
|
from drf_spectacular.openapi import AutoSchema
|
||||||
|
from drf_spectacular.plumbing import (
|
||||||
|
ComponentRegistry,
|
||||||
|
ResolvedComponent,
|
||||||
|
build_basic_type,
|
||||||
|
build_media_type_object,
|
||||||
|
build_object_type,
|
||||||
|
is_serializer,
|
||||||
|
)
|
||||||
|
from drf_spectacular.types import OpenApiTypes
|
||||||
|
from drf_spectacular.utils import extend_schema
|
||||||
|
from rest_framework.relations import ManyRelatedField
|
||||||
|
|
||||||
|
from netbox.api.fields import ChoiceField, SerializedPKRelatedField
|
||||||
|
from netbox.api.serializers import WritableNestedSerializer
|
||||||
|
|
||||||
|
# see netbox.api.routers.NetBoxRouter
|
||||||
|
BULK_ACTIONS = ("bulk_destroy", "bulk_partial_update", "bulk_update")
|
||||||
|
WRITABLE_ACTIONS = ("PATCH", "POST", "PUT")
|
||||||
|
|
||||||
|
|
||||||
|
class FixTimeZoneSerializerField(OpenApiSerializerFieldExtension):
|
||||||
|
target_class = 'timezone_field.rest_framework.TimeZoneSerializerField'
|
||||||
|
|
||||||
|
def map_serializer_field(self, auto_schema, direction):
|
||||||
|
return build_basic_type(OpenApiTypes.STR)
|
||||||
|
|
||||||
|
|
||||||
|
class ChoiceFieldFix(OpenApiSerializerFieldExtension):
|
||||||
|
target_class = 'netbox.api.fields.ChoiceField'
|
||||||
|
|
||||||
|
def map_serializer_field(self, auto_schema, direction):
|
||||||
|
if direction == 'request':
|
||||||
|
return build_basic_type(OpenApiTypes.STR)
|
||||||
|
|
||||||
|
elif direction == "response":
|
||||||
|
return build_object_type(
|
||||||
|
properties={
|
||||||
|
"value": build_basic_type(OpenApiTypes.STR),
|
||||||
|
"label": build_basic_type(OpenApiTypes.STR),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class NetBoxAutoSchema(AutoSchema):
|
||||||
|
"""
|
||||||
|
Overrides to drf_spectacular.openapi.AutoSchema to fix following issues:
|
||||||
|
1. bulk serializers cause operation_id conflicts with non-bulk ones
|
||||||
|
2. bulk operations should specify a list
|
||||||
|
3. bulk operations don't have filter params
|
||||||
|
4. bulk operations don't have pagination
|
||||||
|
5. bulk delete should specify input
|
||||||
|
"""
|
||||||
|
|
||||||
|
writable_serializers = {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_bulk_action(self):
|
||||||
|
if hasattr(self.view, "action") and self.view.action in BULK_ACTIONS:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_operation_id(self):
|
||||||
|
"""
|
||||||
|
bulk serializers cause operation_id conflicts with non-bulk ones
|
||||||
|
bulk operations cause id conflicts in spectacular resulting in numerous:
|
||||||
|
Warning: operationId "xxx" has collisions [xxx]. "resolving with numeral suffixes"
|
||||||
|
code is modified from drf_spectacular.openapi.AutoSchema.get_operation_id
|
||||||
|
"""
|
||||||
|
if self.is_bulk_action:
|
||||||
|
tokenized_path = self._tokenize_path()
|
||||||
|
# replace dashes as they can be problematic later in code generation
|
||||||
|
tokenized_path = [t.replace('-', '_') for t in tokenized_path]
|
||||||
|
|
||||||
|
if self.method == 'GET' and self._is_list_view():
|
||||||
|
# this shouldn't happen, but keeping it here to follow base code
|
||||||
|
action = 'list'
|
||||||
|
else:
|
||||||
|
# action = self.method_mapping[self.method.lower()]
|
||||||
|
# use bulk name so partial_update -> bulk_partial_update
|
||||||
|
action = self.view.action.lower()
|
||||||
|
|
||||||
|
if not tokenized_path:
|
||||||
|
tokenized_path.append('root')
|
||||||
|
|
||||||
|
if re.search(r'<drf_format_suffix\w*:\w+>', self.path_regex):
|
||||||
|
tokenized_path.append('formatted')
|
||||||
|
|
||||||
|
return '_'.join(tokenized_path + [action])
|
||||||
|
|
||||||
|
# if not bulk - just return normal id
|
||||||
|
return super().get_operation_id()
|
||||||
|
|
||||||
|
def get_request_serializer(self) -> typing.Any:
|
||||||
|
# bulk operations should specify a list
|
||||||
|
serializer = super().get_request_serializer()
|
||||||
|
|
||||||
|
if self.is_bulk_action:
|
||||||
|
return type(serializer)(many=True)
|
||||||
|
|
||||||
|
# handle mapping for Writable serializers - adapted from dansheps original code
|
||||||
|
# for drf-yasg
|
||||||
|
if serializer is not None and self.method in WRITABLE_ACTIONS:
|
||||||
|
writable_class = self.get_writable_class(serializer)
|
||||||
|
if writable_class is not None:
|
||||||
|
if hasattr(serializer, "child"):
|
||||||
|
child_serializer = self.get_writable_class(serializer.child)
|
||||||
|
serializer = writable_class(context=serializer.context, child=child_serializer)
|
||||||
|
else:
|
||||||
|
serializer = writable_class(context=serializer.context)
|
||||||
|
|
||||||
|
return serializer
|
||||||
|
|
||||||
|
def get_response_serializers(self) -> typing.Any:
|
||||||
|
# bulk operations should specify a list
|
||||||
|
response_serializers = super().get_response_serializers()
|
||||||
|
|
||||||
|
if self.is_bulk_action:
|
||||||
|
return type(response_serializers)(many=True)
|
||||||
|
|
||||||
|
return response_serializers
|
||||||
|
|
||||||
|
def get_serializer_ref_name(self, serializer):
|
||||||
|
# from drf-yasg.utils
|
||||||
|
"""Get serializer's ref_name (or None for ModelSerializer if it is named 'NestedSerializer')
|
||||||
|
:param serializer: Serializer instance
|
||||||
|
:return: Serializer's ``ref_name`` or ``None`` for inline serializer
|
||||||
|
:rtype: str or None
|
||||||
|
"""
|
||||||
|
serializer_meta = getattr(serializer, 'Meta', None)
|
||||||
|
serializer_name = type(serializer).__name__
|
||||||
|
if hasattr(serializer_meta, 'ref_name'):
|
||||||
|
ref_name = serializer_meta.ref_name
|
||||||
|
elif serializer_name == 'NestedSerializer' and isinstance(serializer, serializers.ModelSerializer):
|
||||||
|
ref_name = None
|
||||||
|
else:
|
||||||
|
ref_name = serializer_name
|
||||||
|
if ref_name.endswith('Serializer'):
|
||||||
|
ref_name = ref_name[: -len('Serializer')]
|
||||||
|
return ref_name
|
||||||
|
|
||||||
|
def get_writable_class(self, serializer):
|
||||||
|
properties = {}
|
||||||
|
fields = {} if hasattr(serializer, 'child') else serializer.fields
|
||||||
|
|
||||||
|
for child_name, child in fields.items():
|
||||||
|
if isinstance(child, (ChoiceField, WritableNestedSerializer)):
|
||||||
|
properties[child_name] = None
|
||||||
|
elif isinstance(child, ManyRelatedField) and isinstance(child.child_relation, SerializedPKRelatedField):
|
||||||
|
properties[child_name] = None
|
||||||
|
|
||||||
|
if not properties:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if type(serializer) not in self.writable_serializers:
|
||||||
|
writable_name = 'Writable' + type(serializer).__name__
|
||||||
|
meta_class = getattr(type(serializer), 'Meta', None)
|
||||||
|
if meta_class:
|
||||||
|
ref_name = 'Writable' + self.get_serializer_ref_name(serializer)
|
||||||
|
writable_meta = type('Meta', (meta_class,), {'ref_name': ref_name})
|
||||||
|
properties['Meta'] = writable_meta
|
||||||
|
|
||||||
|
self.writable_serializers[type(serializer)] = type(writable_name, (type(serializer),), properties)
|
||||||
|
|
||||||
|
writable_class = self.writable_serializers[type(serializer)]
|
||||||
|
return writable_class
|
||||||
|
|
||||||
|
def get_filter_backends(self):
|
||||||
|
# bulk operations don't have filter params
|
||||||
|
if self.is_bulk_action:
|
||||||
|
return []
|
||||||
|
return super().get_filter_backends()
|
||||||
|
|
||||||
|
def _get_paginator(self):
|
||||||
|
# bulk operations don't have pagination
|
||||||
|
if self.is_bulk_action:
|
||||||
|
return None
|
||||||
|
return super()._get_paginator()
|
||||||
|
|
||||||
|
def _get_request_body(self, direction='request'):
|
||||||
|
# bulk delete should specify input
|
||||||
|
if (not self.is_bulk_action) or (self.method != 'DELETE'):
|
||||||
|
return super()._get_request_body(direction)
|
||||||
|
|
||||||
|
# rest from drf_spectacular.openapi.AutoSchema._get_request_body
|
||||||
|
# but remove the unsafe method check
|
||||||
|
|
||||||
|
request_serializer = self.get_request_serializer()
|
||||||
|
|
||||||
|
if isinstance(request_serializer, dict):
|
||||||
|
content = []
|
||||||
|
request_body_required = True
|
||||||
|
for media_type, serializer in request_serializer.items():
|
||||||
|
schema, partial_request_body_required = self._get_request_for_media_type(serializer, direction)
|
||||||
|
examples = self._get_examples(serializer, direction, media_type)
|
||||||
|
if schema is None:
|
||||||
|
continue
|
||||||
|
content.append((media_type, schema, examples))
|
||||||
|
request_body_required &= partial_request_body_required
|
||||||
|
else:
|
||||||
|
schema, request_body_required = self._get_request_for_media_type(request_serializer, direction)
|
||||||
|
if schema is None:
|
||||||
|
return None
|
||||||
|
content = [
|
||||||
|
(media_type, schema, self._get_examples(request_serializer, direction, media_type))
|
||||||
|
for media_type in self.map_parsers()
|
||||||
|
]
|
||||||
|
|
||||||
|
request_body = {
|
||||||
|
'content': {
|
||||||
|
media_type: build_media_type_object(schema, examples) for media_type, schema, examples in content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if request_body_required:
|
||||||
|
request_body['required'] = request_body_required
|
||||||
|
return request_body
|
@ -6,3 +6,4 @@ class CoreConfig(AppConfig):
|
|||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
from . import data_backends, search
|
from . import data_backends, search
|
||||||
|
from core.api import schema # noqa: E402
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from drf_spectacular.utils import extend_schema_serializer
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from dcim import models
|
from dcim import models
|
||||||
@ -53,6 +54,9 @@ __all__ = [
|
|||||||
# Regions/sites
|
# Regions/sites
|
||||||
#
|
#
|
||||||
|
|
||||||
|
@extend_schema_serializer(
|
||||||
|
exclude_fields=('site_count',),
|
||||||
|
)
|
||||||
class NestedRegionSerializer(WritableNestedSerializer):
|
class NestedRegionSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail')
|
||||||
site_count = serializers.IntegerField(read_only=True)
|
site_count = serializers.IntegerField(read_only=True)
|
||||||
@ -63,6 +67,9 @@ class NestedRegionSerializer(WritableNestedSerializer):
|
|||||||
fields = ['id', 'url', 'display', 'name', 'slug', 'site_count', '_depth']
|
fields = ['id', 'url', 'display', 'name', 'slug', 'site_count', '_depth']
|
||||||
|
|
||||||
|
|
||||||
|
@extend_schema_serializer(
|
||||||
|
exclude_fields=('site_count',),
|
||||||
|
)
|
||||||
class NestedSiteGroupSerializer(WritableNestedSerializer):
|
class NestedSiteGroupSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:sitegroup-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:sitegroup-detail')
|
||||||
site_count = serializers.IntegerField(read_only=True)
|
site_count = serializers.IntegerField(read_only=True)
|
||||||
@ -85,6 +92,9 @@ class NestedSiteSerializer(WritableNestedSerializer):
|
|||||||
# Racks
|
# Racks
|
||||||
#
|
#
|
||||||
|
|
||||||
|
@extend_schema_serializer(
|
||||||
|
exclude_fields=('rack_count',),
|
||||||
|
)
|
||||||
class NestedLocationSerializer(WritableNestedSerializer):
|
class NestedLocationSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:location-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:location-detail')
|
||||||
rack_count = serializers.IntegerField(read_only=True)
|
rack_count = serializers.IntegerField(read_only=True)
|
||||||
@ -95,6 +105,9 @@ class NestedLocationSerializer(WritableNestedSerializer):
|
|||||||
fields = ['id', 'url', 'display', 'name', 'slug', 'rack_count', '_depth']
|
fields = ['id', 'url', 'display', 'name', 'slug', 'rack_count', '_depth']
|
||||||
|
|
||||||
|
|
||||||
|
@extend_schema_serializer(
|
||||||
|
exclude_fields=('rack_count',),
|
||||||
|
)
|
||||||
class NestedRackRoleSerializer(WritableNestedSerializer):
|
class NestedRackRoleSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
|
||||||
rack_count = serializers.IntegerField(read_only=True)
|
rack_count = serializers.IntegerField(read_only=True)
|
||||||
@ -104,6 +117,9 @@ class NestedRackRoleSerializer(WritableNestedSerializer):
|
|||||||
fields = ['id', 'url', 'display', 'name', 'slug', 'rack_count']
|
fields = ['id', 'url', 'display', 'name', 'slug', 'rack_count']
|
||||||
|
|
||||||
|
|
||||||
|
@extend_schema_serializer(
|
||||||
|
exclude_fields=('device_count',),
|
||||||
|
)
|
||||||
class NestedRackSerializer(WritableNestedSerializer):
|
class NestedRackSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail')
|
||||||
device_count = serializers.IntegerField(read_only=True)
|
device_count = serializers.IntegerField(read_only=True)
|
||||||
@ -129,6 +145,9 @@ class NestedRackReservationSerializer(WritableNestedSerializer):
|
|||||||
# Device/module types
|
# Device/module types
|
||||||
#
|
#
|
||||||
|
|
||||||
|
@extend_schema_serializer(
|
||||||
|
exclude_fields=('devicetype_count',),
|
||||||
|
)
|
||||||
class NestedManufacturerSerializer(WritableNestedSerializer):
|
class NestedManufacturerSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
|
||||||
devicetype_count = serializers.IntegerField(read_only=True)
|
devicetype_count = serializers.IntegerField(read_only=True)
|
||||||
@ -138,6 +157,9 @@ class NestedManufacturerSerializer(WritableNestedSerializer):
|
|||||||
fields = ['id', 'url', 'display', 'name', 'slug', 'devicetype_count']
|
fields = ['id', 'url', 'display', 'name', 'slug', 'devicetype_count']
|
||||||
|
|
||||||
|
|
||||||
|
@extend_schema_serializer(
|
||||||
|
exclude_fields=('device_count',),
|
||||||
|
)
|
||||||
class NestedDeviceTypeSerializer(WritableNestedSerializer):
|
class NestedDeviceTypeSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
|
||||||
manufacturer = NestedManufacturerSerializer(read_only=True)
|
manufacturer = NestedManufacturerSerializer(read_only=True)
|
||||||
@ -247,6 +269,9 @@ class NestedInventoryItemTemplateSerializer(WritableNestedSerializer):
|
|||||||
# Devices
|
# Devices
|
||||||
#
|
#
|
||||||
|
|
||||||
|
@extend_schema_serializer(
|
||||||
|
exclude_fields=('device_count', 'virtualmachine_count'),
|
||||||
|
)
|
||||||
class NestedDeviceRoleSerializer(WritableNestedSerializer):
|
class NestedDeviceRoleSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
|
||||||
device_count = serializers.IntegerField(read_only=True)
|
device_count = serializers.IntegerField(read_only=True)
|
||||||
@ -257,6 +282,9 @@ class NestedDeviceRoleSerializer(WritableNestedSerializer):
|
|||||||
fields = ['id', 'url', 'display', 'name', 'slug', 'device_count', 'virtualmachine_count']
|
fields = ['id', 'url', 'display', 'name', 'slug', 'device_count', 'virtualmachine_count']
|
||||||
|
|
||||||
|
|
||||||
|
@extend_schema_serializer(
|
||||||
|
exclude_fields=('device_count', 'virtualmachine_count'),
|
||||||
|
)
|
||||||
class NestedPlatformSerializer(WritableNestedSerializer):
|
class NestedPlatformSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
|
||||||
device_count = serializers.IntegerField(read_only=True)
|
device_count = serializers.IntegerField(read_only=True)
|
||||||
@ -386,7 +414,7 @@ class NestedFrontPortSerializer(WritableNestedSerializer):
|
|||||||
|
|
||||||
class NestedModuleBaySerializer(WritableNestedSerializer):
|
class NestedModuleBaySerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail')
|
||||||
module = NestedModuleSerializer(read_only=True)
|
module = NestedModuleSerializer(required=False, read_only=True, allow_null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.ModuleBay
|
model = models.ModuleBay
|
||||||
@ -412,6 +440,9 @@ class NestedInventoryItemSerializer(WritableNestedSerializer):
|
|||||||
fields = ['id', 'url', 'display', 'device', 'name', '_depth']
|
fields = ['id', 'url', 'display', 'device', 'name', '_depth']
|
||||||
|
|
||||||
|
|
||||||
|
@extend_schema_serializer(
|
||||||
|
exclude_fields=('inventoryitem_count',),
|
||||||
|
)
|
||||||
class NestedInventoryItemRoleSerializer(WritableNestedSerializer):
|
class NestedInventoryItemRoleSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemrole-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemrole-detail')
|
||||||
inventoryitem_count = serializers.IntegerField(read_only=True)
|
inventoryitem_count = serializers.IntegerField(read_only=True)
|
||||||
@ -437,6 +468,9 @@ class NestedCableSerializer(BaseModelSerializer):
|
|||||||
# Virtual chassis
|
# Virtual chassis
|
||||||
#
|
#
|
||||||
|
|
||||||
|
@extend_schema_serializer(
|
||||||
|
exclude_fields=('member_count',),
|
||||||
|
)
|
||||||
class NestedVirtualChassisSerializer(WritableNestedSerializer):
|
class NestedVirtualChassisSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
|
||||||
master = NestedDeviceSerializer()
|
master = NestedDeviceSerializer()
|
||||||
@ -451,6 +485,9 @@ class NestedVirtualChassisSerializer(WritableNestedSerializer):
|
|||||||
# Power panels/feeds
|
# Power panels/feeds
|
||||||
#
|
#
|
||||||
|
|
||||||
|
@extend_schema_serializer(
|
||||||
|
exclude_fields=('powerfeed_count',),
|
||||||
|
)
|
||||||
class NestedPowerPanelSerializer(WritableNestedSerializer):
|
class NestedPowerPanelSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail')
|
||||||
powerfeed_count = serializers.IntegerField(read_only=True)
|
powerfeed_count = serializers.IntegerField(read_only=True)
|
||||||
|
@ -2,7 +2,8 @@ import decimal
|
|||||||
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from drf_yasg.utils import swagger_serializer_method
|
from drf_spectacular.utils import extend_schema_field
|
||||||
|
from drf_spectacular.types import OpenApiTypes
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from timezone_field.rest_framework import TimeZoneSerializerField
|
from timezone_field.rest_framework import TimeZoneSerializerField
|
||||||
|
|
||||||
@ -33,12 +34,13 @@ from .nested_serializers import *
|
|||||||
|
|
||||||
|
|
||||||
class CabledObjectSerializer(serializers.ModelSerializer):
|
class CabledObjectSerializer(serializers.ModelSerializer):
|
||||||
cable = NestedCableSerializer(read_only=True)
|
cable = NestedCableSerializer(read_only=True, allow_null=True)
|
||||||
cable_end = serializers.CharField(read_only=True)
|
cable_end = serializers.CharField(read_only=True)
|
||||||
link_peers_type = serializers.SerializerMethodField(read_only=True)
|
link_peers_type = serializers.SerializerMethodField(read_only=True)
|
||||||
link_peers = serializers.SerializerMethodField(read_only=True)
|
link_peers = serializers.SerializerMethodField(read_only=True)
|
||||||
_occupied = serializers.SerializerMethodField(read_only=True)
|
_occupied = serializers.SerializerMethodField(read_only=True)
|
||||||
|
|
||||||
|
@extend_schema_field(OpenApiTypes.STR)
|
||||||
def get_link_peers_type(self, obj):
|
def get_link_peers_type(self, obj):
|
||||||
"""
|
"""
|
||||||
Return the type of the peer link terminations, or None.
|
Return the type of the peer link terminations, or None.
|
||||||
@ -51,7 +53,7 @@ class CabledObjectSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.ListField)
|
@extend_schema_field(serializers.ListField)
|
||||||
def get_link_peers(self, obj):
|
def get_link_peers(self, obj):
|
||||||
"""
|
"""
|
||||||
Return the appropriate serializer for the link termination model.
|
Return the appropriate serializer for the link termination model.
|
||||||
@ -64,7 +66,7 @@ class CabledObjectSerializer(serializers.ModelSerializer):
|
|||||||
context = {'request': self.context['request']}
|
context = {'request': self.context['request']}
|
||||||
return serializer(obj.link_peers, context=context, many=True).data
|
return serializer(obj.link_peers, context=context, many=True).data
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.BooleanField)
|
@extend_schema_field(serializers.BooleanField)
|
||||||
def get__occupied(self, obj):
|
def get__occupied(self, obj):
|
||||||
return obj._occupied
|
return obj._occupied
|
||||||
|
|
||||||
@ -77,11 +79,12 @@ class ConnectedEndpointsSerializer(serializers.ModelSerializer):
|
|||||||
connected_endpoints = serializers.SerializerMethodField(read_only=True)
|
connected_endpoints = serializers.SerializerMethodField(read_only=True)
|
||||||
connected_endpoints_reachable = serializers.SerializerMethodField(read_only=True)
|
connected_endpoints_reachable = serializers.SerializerMethodField(read_only=True)
|
||||||
|
|
||||||
|
@extend_schema_field(OpenApiTypes.STR)
|
||||||
def get_connected_endpoints_type(self, obj):
|
def get_connected_endpoints_type(self, obj):
|
||||||
if endpoints := obj.connected_endpoints:
|
if endpoints := obj.connected_endpoints:
|
||||||
return f'{endpoints[0]._meta.app_label}.{endpoints[0]._meta.model_name}'
|
return f'{endpoints[0]._meta.app_label}.{endpoints[0]._meta.model_name}'
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.ListField)
|
@extend_schema_field(serializers.ListField)
|
||||||
def get_connected_endpoints(self, obj):
|
def get_connected_endpoints(self, obj):
|
||||||
"""
|
"""
|
||||||
Return the appropriate serializer for the type of connected object.
|
Return the appropriate serializer for the type of connected object.
|
||||||
@ -91,7 +94,7 @@ class ConnectedEndpointsSerializer(serializers.ModelSerializer):
|
|||||||
context = {'request': self.context['request']}
|
context = {'request': self.context['request']}
|
||||||
return serializer(endpoints, many=True, context=context).data
|
return serializer(endpoints, many=True, context=context).data
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.BooleanField)
|
@extend_schema_field(serializers.BooleanField)
|
||||||
def get_connected_endpoints_reachable(self, obj):
|
def get_connected_endpoints_reachable(self, obj):
|
||||||
return obj._path and obj._path.is_complete and obj._path.is_active
|
return obj._path and obj._path.is_complete and obj._path.is_active
|
||||||
|
|
||||||
@ -198,12 +201,12 @@ class RackSerializer(NetBoxModelSerializer):
|
|||||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||||
status = ChoiceField(choices=RackStatusChoices, required=False)
|
status = ChoiceField(choices=RackStatusChoices, required=False)
|
||||||
role = NestedRackRoleSerializer(required=False, allow_null=True)
|
role = NestedRackRoleSerializer(required=False, allow_null=True)
|
||||||
type = ChoiceField(choices=RackTypeChoices, allow_blank=True, required=False)
|
type = ChoiceField(choices=RackTypeChoices, allow_blank=True, required=False, allow_null=True)
|
||||||
facility_id = serializers.CharField(max_length=50, allow_blank=True, allow_null=True, label=_('Facility ID'),
|
facility_id = serializers.CharField(max_length=50, allow_blank=True, allow_null=True, label=_('Facility ID'),
|
||||||
default=None)
|
default=None)
|
||||||
width = ChoiceField(choices=RackWidthChoices, required=False)
|
width = ChoiceField(choices=RackWidthChoices, required=False)
|
||||||
outer_unit = ChoiceField(choices=RackDimensionUnitChoices, allow_blank=True, required=False)
|
outer_unit = ChoiceField(choices=RackDimensionUnitChoices, allow_blank=True, required=False, allow_null=True)
|
||||||
weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False)
|
weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True)
|
||||||
device_count = serializers.IntegerField(read_only=True)
|
device_count = serializers.IntegerField(read_only=True)
|
||||||
powerfeed_count = serializers.IntegerField(read_only=True)
|
powerfeed_count = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
@ -232,6 +235,7 @@ class RackUnitSerializer(serializers.Serializer):
|
|||||||
occupied = serializers.BooleanField(read_only=True)
|
occupied = serializers.BooleanField(read_only=True)
|
||||||
display = serializers.SerializerMethodField(read_only=True)
|
display = serializers.SerializerMethodField(read_only=True)
|
||||||
|
|
||||||
|
@extend_schema_field(OpenApiTypes.STR)
|
||||||
def get_display(self, obj):
|
def get_display(self, obj):
|
||||||
return obj['name']
|
return obj['name']
|
||||||
|
|
||||||
@ -318,9 +322,9 @@ class DeviceTypeSerializer(NetBoxModelSerializer):
|
|||||||
min_value=0,
|
min_value=0,
|
||||||
default=1.0
|
default=1.0
|
||||||
)
|
)
|
||||||
subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False)
|
subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False, allow_null=True)
|
||||||
airflow = ChoiceField(choices=DeviceAirflowChoices, allow_blank=True, required=False)
|
airflow = ChoiceField(choices=DeviceAirflowChoices, allow_blank=True, required=False, allow_null=True)
|
||||||
weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False)
|
weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True)
|
||||||
device_count = serializers.IntegerField(read_only=True)
|
device_count = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -335,7 +339,7 @@ class DeviceTypeSerializer(NetBoxModelSerializer):
|
|||||||
class ModuleTypeSerializer(NetBoxModelSerializer):
|
class ModuleTypeSerializer(NetBoxModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:moduletype-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:moduletype-detail')
|
||||||
manufacturer = NestedManufacturerSerializer()
|
manufacturer = NestedManufacturerSerializer()
|
||||||
weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False)
|
weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ModuleType
|
model = ModuleType
|
||||||
@ -416,7 +420,8 @@ class PowerPortTemplateSerializer(ValidatedModelSerializer):
|
|||||||
type = ChoiceField(
|
type = ChoiceField(
|
||||||
choices=PowerPortTypeChoices,
|
choices=PowerPortTypeChoices,
|
||||||
allow_blank=True,
|
allow_blank=True,
|
||||||
required=False
|
required=False,
|
||||||
|
allow_null=True
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -442,7 +447,8 @@ class PowerOutletTemplateSerializer(ValidatedModelSerializer):
|
|||||||
type = ChoiceField(
|
type = ChoiceField(
|
||||||
choices=PowerOutletTypeChoices,
|
choices=PowerOutletTypeChoices,
|
||||||
allow_blank=True,
|
allow_blank=True,
|
||||||
required=False
|
required=False,
|
||||||
|
allow_null=True
|
||||||
)
|
)
|
||||||
power_port = NestedPowerPortTemplateSerializer(
|
power_port = NestedPowerPortTemplateSerializer(
|
||||||
required=False,
|
required=False,
|
||||||
@ -451,7 +457,8 @@ class PowerOutletTemplateSerializer(ValidatedModelSerializer):
|
|||||||
feed_leg = ChoiceField(
|
feed_leg = ChoiceField(
|
||||||
choices=PowerOutletFeedLegChoices,
|
choices=PowerOutletFeedLegChoices,
|
||||||
allow_blank=True,
|
allow_blank=True,
|
||||||
required=False
|
required=False,
|
||||||
|
allow_null=True
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -482,12 +489,14 @@ class InterfaceTemplateSerializer(ValidatedModelSerializer):
|
|||||||
poe_mode = ChoiceField(
|
poe_mode = ChoiceField(
|
||||||
choices=InterfacePoEModeChoices,
|
choices=InterfacePoEModeChoices,
|
||||||
required=False,
|
required=False,
|
||||||
allow_blank=True
|
allow_blank=True,
|
||||||
|
allow_null=True
|
||||||
)
|
)
|
||||||
poe_type = ChoiceField(
|
poe_type = ChoiceField(
|
||||||
choices=InterfacePoETypeChoices,
|
choices=InterfacePoETypeChoices,
|
||||||
required=False,
|
required=False,
|
||||||
allow_blank=True
|
allow_blank=True,
|
||||||
|
allow_null=True
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -589,7 +598,7 @@ class InventoryItemTemplateSerializer(ValidatedModelSerializer):
|
|||||||
'description', 'component_type', 'component_id', 'component', 'created', 'last_updated', '_depth',
|
'description', 'component_type', 'component_id', 'component', 'created', 'last_updated', '_depth',
|
||||||
]
|
]
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||||
def get_component(self, obj):
|
def get_component(self, obj):
|
||||||
if obj.component is None:
|
if obj.component is None:
|
||||||
return None
|
return None
|
||||||
@ -640,7 +649,7 @@ class DeviceSerializer(NetBoxModelSerializer):
|
|||||||
site = NestedSiteSerializer()
|
site = NestedSiteSerializer()
|
||||||
location = NestedLocationSerializer(required=False, allow_null=True, default=None)
|
location = NestedLocationSerializer(required=False, allow_null=True, default=None)
|
||||||
rack = NestedRackSerializer(required=False, allow_null=True, default=None)
|
rack = NestedRackSerializer(required=False, allow_null=True, default=None)
|
||||||
face = ChoiceField(choices=DeviceFaceChoices, allow_blank=True, default='')
|
face = ChoiceField(choices=DeviceFaceChoices, allow_blank=True, default=lambda: '')
|
||||||
position = serializers.DecimalField(
|
position = serializers.DecimalField(
|
||||||
max_digits=4,
|
max_digits=4,
|
||||||
decimal_places=1,
|
decimal_places=1,
|
||||||
@ -669,7 +678,7 @@ class DeviceSerializer(NetBoxModelSerializer):
|
|||||||
'comments', 'config_template', 'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated',
|
'comments', 'config_template', 'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=NestedDeviceSerializer)
|
@extend_schema_field(NestedDeviceSerializer)
|
||||||
def get_parent_device(self, obj):
|
def get_parent_device(self, obj):
|
||||||
try:
|
try:
|
||||||
device_bay = obj.parent_bay
|
device_bay = obj.parent_bay
|
||||||
@ -682,7 +691,7 @@ class DeviceSerializer(NetBoxModelSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class DeviceWithConfigContextSerializer(DeviceSerializer):
|
class DeviceWithConfigContextSerializer(DeviceSerializer):
|
||||||
config_context = serializers.SerializerMethodField()
|
config_context = serializers.SerializerMethodField(read_only=True)
|
||||||
|
|
||||||
class Meta(DeviceSerializer.Meta):
|
class Meta(DeviceSerializer.Meta):
|
||||||
fields = [
|
fields = [
|
||||||
@ -692,7 +701,7 @@ class DeviceWithConfigContextSerializer(DeviceSerializer):
|
|||||||
'comments', 'local_context_data', 'tags', 'custom_fields', 'config_context', 'created', 'last_updated',
|
'comments', 'local_context_data', 'tags', 'custom_fields', 'config_context', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||||
def get_config_context(self, obj):
|
def get_config_context(self, obj):
|
||||||
return obj.get_config_context()
|
return obj.get_config_context()
|
||||||
|
|
||||||
@ -701,7 +710,7 @@ class VirtualDeviceContextSerializer(NetBoxModelSerializer):
|
|||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
tenant = NestedTenantSerializer(required=False, allow_null=True, default=None)
|
tenant = NestedTenantSerializer(required=False, allow_null=True, default=None)
|
||||||
primary_ip = NestedIPAddressSerializer(read_only=True)
|
primary_ip = NestedIPAddressSerializer(read_only=True, allow_null=True)
|
||||||
primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True)
|
primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True)
|
||||||
primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True)
|
primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True)
|
||||||
|
|
||||||
@ -806,7 +815,8 @@ class PowerOutletSerializer(NetBoxModelSerializer, CabledObjectSerializer, Conne
|
|||||||
type = ChoiceField(
|
type = ChoiceField(
|
||||||
choices=PowerOutletTypeChoices,
|
choices=PowerOutletTypeChoices,
|
||||||
allow_blank=True,
|
allow_blank=True,
|
||||||
required=False
|
required=False,
|
||||||
|
allow_null=True
|
||||||
)
|
)
|
||||||
power_port = NestedPowerPortSerializer(
|
power_port = NestedPowerPortSerializer(
|
||||||
required=False,
|
required=False,
|
||||||
@ -815,7 +825,8 @@ class PowerOutletSerializer(NetBoxModelSerializer, CabledObjectSerializer, Conne
|
|||||||
feed_leg = ChoiceField(
|
feed_leg = ChoiceField(
|
||||||
choices=PowerOutletFeedLegChoices,
|
choices=PowerOutletFeedLegChoices,
|
||||||
allow_blank=True,
|
allow_blank=True,
|
||||||
required=False
|
required=False,
|
||||||
|
allow_null=True
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -838,7 +849,8 @@ class PowerPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
|
|||||||
type = ChoiceField(
|
type = ChoiceField(
|
||||||
choices=PowerPortTypeChoices,
|
choices=PowerPortTypeChoices,
|
||||||
allow_blank=True,
|
allow_blank=True,
|
||||||
required=False
|
required=False,
|
||||||
|
allow_null=True
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -868,12 +880,12 @@ class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
|
|||||||
parent = NestedInterfaceSerializer(required=False, allow_null=True)
|
parent = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||||
bridge = NestedInterfaceSerializer(required=False, allow_null=True)
|
bridge = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||||
lag = NestedInterfaceSerializer(required=False, allow_null=True)
|
lag = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||||
mode = ChoiceField(choices=InterfaceModeChoices, required=False, allow_blank=True)
|
mode = ChoiceField(choices=InterfaceModeChoices, required=False, allow_blank=True, allow_null=True)
|
||||||
duplex = ChoiceField(choices=InterfaceDuplexChoices, required=False, allow_blank=True)
|
duplex = ChoiceField(choices=InterfaceDuplexChoices, required=False, allow_blank=True, allow_null=True)
|
||||||
rf_role = ChoiceField(choices=WirelessRoleChoices, required=False, allow_blank=True)
|
rf_role = ChoiceField(choices=WirelessRoleChoices, required=False, allow_blank=True, allow_null=True)
|
||||||
rf_channel = ChoiceField(choices=WirelessChannelChoices, required=False, allow_blank=True)
|
rf_channel = ChoiceField(choices=WirelessChannelChoices, required=False, allow_blank=True, allow_null=True)
|
||||||
poe_mode = ChoiceField(choices=InterfacePoEModeChoices, required=False, allow_blank=True)
|
poe_mode = ChoiceField(choices=InterfacePoEModeChoices, required=False, allow_blank=True, allow_null=True)
|
||||||
poe_type = ChoiceField(choices=InterfacePoETypeChoices, required=False, allow_blank=True)
|
poe_type = ChoiceField(choices=InterfacePoETypeChoices, required=False, allow_blank=True, allow_null=True)
|
||||||
untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
|
untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
|
||||||
tagged_vlans = SerializedPKRelatedField(
|
tagged_vlans = SerializedPKRelatedField(
|
||||||
queryset=VLAN.objects.all(),
|
queryset=VLAN.objects.all(),
|
||||||
@ -882,8 +894,8 @@ class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
|
|||||||
many=True
|
many=True
|
||||||
)
|
)
|
||||||
vrf = NestedVRFSerializer(required=False, allow_null=True)
|
vrf = NestedVRFSerializer(required=False, allow_null=True)
|
||||||
l2vpn_termination = NestedL2VPNTerminationSerializer(read_only=True)
|
l2vpn_termination = NestedL2VPNTerminationSerializer(read_only=True, allow_null=True)
|
||||||
wireless_link = NestedWirelessLinkSerializer(read_only=True)
|
wireless_link = NestedWirelessLinkSerializer(read_only=True, allow_null=True)
|
||||||
wireless_lans = SerializedPKRelatedField(
|
wireless_lans = SerializedPKRelatedField(
|
||||||
queryset=WirelessLAN.objects.all(),
|
queryset=WirelessLAN.objects.all(),
|
||||||
serializer=NestedWirelessLANSerializer,
|
serializer=NestedWirelessLANSerializer,
|
||||||
@ -892,6 +904,8 @@ class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
|
|||||||
)
|
)
|
||||||
count_ipaddresses = serializers.IntegerField(read_only=True)
|
count_ipaddresses = serializers.IntegerField(read_only=True)
|
||||||
count_fhrp_groups = serializers.IntegerField(read_only=True)
|
count_fhrp_groups = serializers.IntegerField(read_only=True)
|
||||||
|
mac_address = serializers.CharField(required=False, default=None)
|
||||||
|
wwn = serializers.CharField(required=False, default=None)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
@ -1015,7 +1029,7 @@ class InventoryItemSerializer(NetBoxModelSerializer):
|
|||||||
'custom_fields', 'created', 'last_updated', '_depth',
|
'custom_fields', 'created', 'last_updated', '_depth',
|
||||||
]
|
]
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||||
def get_component(self, obj):
|
def get_component(self, obj):
|
||||||
if obj.component is None:
|
if obj.component is None:
|
||||||
return None
|
return None
|
||||||
@ -1050,7 +1064,7 @@ class CableSerializer(NetBoxModelSerializer):
|
|||||||
b_terminations = GenericObjectSerializer(many=True, required=False)
|
b_terminations = GenericObjectSerializer(many=True, required=False)
|
||||||
status = ChoiceField(choices=LinkStatusChoices, required=False)
|
status = ChoiceField(choices=LinkStatusChoices, required=False)
|
||||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||||
length_unit = ChoiceField(choices=CableLengthUnitChoices, allow_blank=True, required=False)
|
length_unit = ChoiceField(choices=CableLengthUnitChoices, allow_blank=True, required=False, allow_null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Cable
|
model = Cable
|
||||||
@ -1086,7 +1100,7 @@ class CableTerminationSerializer(NetBoxModelSerializer):
|
|||||||
'id', 'url', 'display', 'cable', 'cable_end', 'termination_type', 'termination_id', 'termination'
|
'id', 'url', 'display', 'cable', 'cable_end', 'termination_type', 'termination_id', 'termination'
|
||||||
]
|
]
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||||
def get_termination(self, obj):
|
def get_termination(self, obj):
|
||||||
serializer = get_serializer_for_model(obj.termination, prefix=NESTED_SERIALIZER_PREFIX)
|
serializer = get_serializer_for_model(obj.termination, prefix=NESTED_SERIALIZER_PREFIX)
|
||||||
context = {'request': self.context['request']}
|
context = {'request': self.context['request']}
|
||||||
@ -1100,7 +1114,7 @@ class CablePathSerializer(serializers.ModelSerializer):
|
|||||||
model = CablePath
|
model = CablePath
|
||||||
fields = ['id', 'path', 'is_active', 'is_complete', 'is_split']
|
fields = ['id', 'path', 'is_active', 'is_complete', 'is_split']
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.ListField)
|
@extend_schema_field(serializers.ListField)
|
||||||
def get_path(self, obj):
|
def get_path(self, obj):
|
||||||
ret = []
|
ret = []
|
||||||
for nodes in obj.path_objects:
|
for nodes in obj.path_objects:
|
||||||
@ -1159,19 +1173,19 @@ class PowerFeedSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
|
|||||||
)
|
)
|
||||||
type = ChoiceField(
|
type = ChoiceField(
|
||||||
choices=PowerFeedTypeChoices,
|
choices=PowerFeedTypeChoices,
|
||||||
default=PowerFeedTypeChoices.TYPE_PRIMARY
|
default=lambda: PowerFeedTypeChoices.TYPE_PRIMARY,
|
||||||
)
|
)
|
||||||
status = ChoiceField(
|
status = ChoiceField(
|
||||||
choices=PowerFeedStatusChoices,
|
choices=PowerFeedStatusChoices,
|
||||||
default=PowerFeedStatusChoices.STATUS_ACTIVE
|
default=lambda: PowerFeedStatusChoices.STATUS_ACTIVE,
|
||||||
)
|
)
|
||||||
supply = ChoiceField(
|
supply = ChoiceField(
|
||||||
choices=PowerFeedSupplyChoices,
|
choices=PowerFeedSupplyChoices,
|
||||||
default=PowerFeedSupplyChoices.SUPPLY_AC
|
default=lambda: PowerFeedSupplyChoices.SUPPLY_AC,
|
||||||
)
|
)
|
||||||
phase = ChoiceField(
|
phase = ChoiceField(
|
||||||
choices=PowerFeedPhaseChoices,
|
choices=PowerFeedPhaseChoices,
|
||||||
default=PowerFeedPhaseChoices.PHASE_SINGLE
|
default=lambda: PowerFeedPhaseChoices.PHASE_SINGLE,
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
from django.http import Http404, HttpResponse
|
from django.http import Http404, HttpResponse
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from drf_yasg import openapi
|
from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiParameter
|
||||||
from drf_yasg.openapi import Parameter
|
from drf_spectacular.types import OpenApiTypes
|
||||||
from drf_yasg.utils import swagger_auto_schema
|
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.renderers import JSONRenderer
|
from rest_framework.renderers import JSONRenderer
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
@ -194,10 +193,6 @@ class RackViewSet(NetBoxModelViewSet):
|
|||||||
serializer_class = serializers.RackSerializer
|
serializer_class = serializers.RackSerializer
|
||||||
filterset_class = filtersets.RackFilterSet
|
filterset_class = filtersets.RackFilterSet
|
||||||
|
|
||||||
@swagger_auto_schema(
|
|
||||||
responses={200: serializers.RackUnitSerializer(many=True)},
|
|
||||||
query_serializer=serializers.RackElevationDetailFilterSerializer
|
|
||||||
)
|
|
||||||
@action(detail=True)
|
@action(detail=True)
|
||||||
def elevation(self, request, pk=None):
|
def elevation(self, request, pk=None):
|
||||||
"""
|
"""
|
||||||
@ -622,28 +617,26 @@ class ConnectedDeviceViewSet(ViewSet):
|
|||||||
* `peer_interface`: The name of the peer interface
|
* `peer_interface`: The name of the peer interface
|
||||||
"""
|
"""
|
||||||
permission_classes = [IsAuthenticatedOrLoginNotRequired]
|
permission_classes = [IsAuthenticatedOrLoginNotRequired]
|
||||||
_device_param = Parameter(
|
_device_param = OpenApiParameter(
|
||||||
name='peer_device',
|
name='peer_device',
|
||||||
in_='query',
|
location='query',
|
||||||
description='The name of the peer device',
|
description='The name of the peer device',
|
||||||
required=True,
|
required=True,
|
||||||
type=openapi.TYPE_STRING
|
type=OpenApiTypes.STR
|
||||||
)
|
)
|
||||||
_interface_param = Parameter(
|
_interface_param = OpenApiParameter(
|
||||||
name='peer_interface',
|
name='peer_interface',
|
||||||
in_='query',
|
location='query',
|
||||||
description='The name of the peer interface',
|
description='The name of the peer interface',
|
||||||
required=True,
|
required=True,
|
||||||
type=openapi.TYPE_STRING
|
type=OpenApiTypes.STR
|
||||||
)
|
)
|
||||||
|
serializer_class = serializers.DeviceSerializer
|
||||||
|
|
||||||
def get_view_name(self):
|
def get_view_name(self):
|
||||||
return "Connected Device Locator"
|
return "Connected Device Locator"
|
||||||
|
|
||||||
@swagger_auto_schema(
|
@extend_schema(responses={200: OpenApiTypes.OBJECT})
|
||||||
manual_parameters=[_device_param, _interface_param],
|
|
||||||
responses={'200': serializers.DeviceSerializer}
|
|
||||||
)
|
|
||||||
def list(self, request):
|
def list(self, request):
|
||||||
|
|
||||||
peer_device_name = request.query_params.get(self._device_param.name)
|
peer_device_name = request.query_params.get(self._device_param.name)
|
||||||
|
@ -1674,7 +1674,7 @@ class RearPortTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
class ModuleBayTest(APIViewTestCases.APIViewTestCase):
|
class ModuleBayTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = ModuleBay
|
model = ModuleBay
|
||||||
brief_fields = ['display', 'id', 'name', 'url']
|
brief_fields = ['display', 'id', 'module', 'name', 'url']
|
||||||
bulk_update_data = {
|
bulk_update_data = {
|
||||||
'description': 'New description',
|
'description': 'New description',
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from drf_spectacular.utils import extend_schema_field
|
||||||
|
from drf_spectacular.types import OpenApiTypes
|
||||||
from rest_framework.fields import Field
|
from rest_framework.fields import Field
|
||||||
from rest_framework.serializers import ValidationError
|
from rest_framework.serializers import ValidationError
|
||||||
|
|
||||||
@ -36,6 +38,7 @@ class CustomFieldDefaultValues:
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
@extend_schema_field(OpenApiTypes.OBJECT)
|
||||||
class CustomFieldsDataField(Field):
|
class CustomFieldsDataField(Field):
|
||||||
|
|
||||||
def _get_custom_fields(self):
|
def _get_custom_fields(self):
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from drf_yasg.utils import swagger_serializer_method
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from core.api.serializers import JobSerializer
|
from core.api.serializers import JobSerializer
|
||||||
@ -11,6 +10,8 @@ from dcim.api.nested_serializers import (
|
|||||||
NestedRegionSerializer, NestedSiteSerializer, NestedSiteGroupSerializer,
|
NestedRegionSerializer, NestedSiteSerializer, NestedSiteGroupSerializer,
|
||||||
)
|
)
|
||||||
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
|
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
|
||||||
|
from drf_spectacular.utils import extend_schema_field
|
||||||
|
from drf_spectacular.types import OpenApiTypes
|
||||||
from extras.choices import *
|
from extras.choices import *
|
||||||
from extras.models import *
|
from extras.models import *
|
||||||
from extras.utils import FeatureQuery
|
from extras.utils import FeatureQuery
|
||||||
@ -103,6 +104,7 @@ class CustomFieldSerializer(ValidatedModelSerializer):
|
|||||||
'last_updated',
|
'last_updated',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@extend_schema_field(OpenApiTypes.STR)
|
||||||
def get_data_type(self, obj):
|
def get_data_type(self, obj):
|
||||||
types = CustomFieldTypeChoices
|
types = CustomFieldTypeChoices
|
||||||
if obj.type == types.TYPE_INTEGER:
|
if obj.type == types.TYPE_INTEGER:
|
||||||
@ -230,7 +232,7 @@ class ImageAttachmentSerializer(ValidatedModelSerializer):
|
|||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||||
def get_parent(self, obj):
|
def get_parent(self, obj):
|
||||||
serializer = get_serializer_for_model(obj.parent, prefix=NESTED_SERIALIZER_PREFIX)
|
serializer = get_serializer_for_model(obj.parent, prefix=NESTED_SERIALIZER_PREFIX)
|
||||||
return serializer(obj.parent, context={'request': self.context['request']}).data
|
return serializer(obj.parent, context={'request': self.context['request']}).data
|
||||||
@ -280,7 +282,7 @@ class JournalEntrySerializer(NetBoxModelSerializer):
|
|||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||||
def get_assigned_object(self, instance):
|
def get_assigned_object(self, instance):
|
||||||
serializer = get_serializer_for_model(instance.assigned_object_type.model_class(), prefix=NESTED_SERIALIZER_PREFIX)
|
serializer = get_serializer_for_model(instance.assigned_object_type.model_class(), prefix=NESTED_SERIALIZER_PREFIX)
|
||||||
context = {'request': self.context['request']}
|
context = {'request': self.context['request']}
|
||||||
@ -453,7 +455,7 @@ class ScriptSerializer(serializers.Serializer):
|
|||||||
vars = serializers.SerializerMethodField(read_only=True)
|
vars = serializers.SerializerMethodField(read_only=True)
|
||||||
result = NestedJobSerializer()
|
result = NestedJobSerializer()
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||||
def get_vars(self, instance):
|
def get_vars(self, instance):
|
||||||
return {
|
return {
|
||||||
k: v.__class__.__name__ for k, v in instance._get_vars().items()
|
k: v.__class__.__name__ for k, v in instance._get_vars().items()
|
||||||
@ -514,7 +516,7 @@ class ObjectChangeSerializer(BaseModelSerializer):
|
|||||||
'changed_object_id', 'changed_object', 'prechange_data', 'postchange_data',
|
'changed_object_id', 'changed_object', 'prechange_data', 'postchange_data',
|
||||||
]
|
]
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||||
def get_changed_object(self, obj):
|
def get_changed_object(self, obj):
|
||||||
"""
|
"""
|
||||||
Serialize a nested representation of the changed object.
|
Serialize a nested representation of the changed object.
|
||||||
|
@ -168,7 +168,7 @@ class ConfigTemplateViewSet(SyncedDataMixin, ConfigTemplateRenderMixin, NetBoxMo
|
|||||||
class ReportViewSet(ViewSet):
|
class ReportViewSet(ViewSet):
|
||||||
permission_classes = [IsAuthenticatedOrLoginNotRequired]
|
permission_classes = [IsAuthenticatedOrLoginNotRequired]
|
||||||
_ignore_model_permissions = True
|
_ignore_model_permissions = True
|
||||||
exclude_from_schema = True
|
schema = None
|
||||||
lookup_value_regex = '[^/]+' # Allow dots
|
lookup_value_regex = '[^/]+' # Allow dots
|
||||||
|
|
||||||
def _get_report(self, pk):
|
def _get_report(self, pk):
|
||||||
@ -270,7 +270,7 @@ class ReportViewSet(ViewSet):
|
|||||||
class ScriptViewSet(ViewSet):
|
class ScriptViewSet(ViewSet):
|
||||||
permission_classes = [IsAuthenticatedOrLoginNotRequired]
|
permission_classes = [IsAuthenticatedOrLoginNotRequired]
|
||||||
_ignore_model_permissions = True
|
_ignore_model_permissions = True
|
||||||
exclude_from_schema = True
|
schema = None
|
||||||
lookup_value_regex = '[^/]+' # Allow dots
|
lookup_value_regex = '[^/]+' # Allow dots
|
||||||
|
|
||||||
def _get_script(self, pk):
|
def _get_script(self, pk):
|
||||||
|
@ -5,6 +5,7 @@ from django.conf import settings
|
|||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.urls.exceptions import NoReverseMatch
|
from django.urls.exceptions import NoReverseMatch
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
|
from drf_spectacular.utils import extend_schema
|
||||||
from rest_framework import permissions
|
from rest_framework import permissions
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.reverse import reverse
|
from rest_framework.reverse import reverse
|
||||||
@ -22,14 +23,14 @@ class InstalledPluginsAdminView(View):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@extend_schema(exclude=True)
|
||||||
class InstalledPluginsAPIView(APIView):
|
class InstalledPluginsAPIView(APIView):
|
||||||
"""
|
"""
|
||||||
API view for listing all installed plugins
|
API view for listing all installed plugins
|
||||||
"""
|
"""
|
||||||
permission_classes = [permissions.IsAdminUser]
|
permission_classes = [permissions.IsAdminUser]
|
||||||
_ignore_model_permissions = True
|
_ignore_model_permissions = True
|
||||||
exclude_from_schema = True
|
schema = None
|
||||||
swagger_schema = None
|
|
||||||
|
|
||||||
def get_view_name(self):
|
def get_view_name(self):
|
||||||
return "Installed Plugins"
|
return "Installed Plugins"
|
||||||
@ -49,10 +50,10 @@ class InstalledPluginsAPIView(APIView):
|
|||||||
return Response([self._get_plugin_data(apps.get_app_config(plugin)) for plugin in settings.PLUGINS])
|
return Response([self._get_plugin_data(apps.get_app_config(plugin)) for plugin in settings.PLUGINS])
|
||||||
|
|
||||||
|
|
||||||
|
@extend_schema(exclude=True)
|
||||||
class PluginsAPIRootView(APIView):
|
class PluginsAPIRootView(APIView):
|
||||||
_ignore_model_permissions = True
|
_ignore_model_permissions = True
|
||||||
exclude_from_schema = True
|
schema = None
|
||||||
swagger_schema = None
|
|
||||||
|
|
||||||
def get_view_name(self):
|
def get_view_name(self):
|
||||||
return "Plugins"
|
return "Plugins"
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from drf_spectacular.utils import extend_schema_serializer
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from ipam import models
|
from ipam import models
|
||||||
@ -54,6 +55,9 @@ class NestedASNSerializer(WritableNestedSerializer):
|
|||||||
# VRFs
|
# VRFs
|
||||||
#
|
#
|
||||||
|
|
||||||
|
@extend_schema_serializer(
|
||||||
|
exclude_fields=('prefix_count',),
|
||||||
|
)
|
||||||
class NestedVRFSerializer(WritableNestedSerializer):
|
class NestedVRFSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vrf-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vrf-detail')
|
||||||
prefix_count = serializers.IntegerField(read_only=True)
|
prefix_count = serializers.IntegerField(read_only=True)
|
||||||
@ -79,6 +83,9 @@ class NestedRouteTargetSerializer(WritableNestedSerializer):
|
|||||||
# RIRs/aggregates
|
# RIRs/aggregates
|
||||||
#
|
#
|
||||||
|
|
||||||
|
@extend_schema_serializer(
|
||||||
|
exclude_fields=('aggregate_count',),
|
||||||
|
)
|
||||||
class NestedRIRSerializer(WritableNestedSerializer):
|
class NestedRIRSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail')
|
||||||
aggregate_count = serializers.IntegerField(read_only=True)
|
aggregate_count = serializers.IntegerField(read_only=True)
|
||||||
@ -121,6 +128,9 @@ class NestedFHRPGroupAssignmentSerializer(WritableNestedSerializer):
|
|||||||
# VLANs
|
# VLANs
|
||||||
#
|
#
|
||||||
|
|
||||||
|
@extend_schema_serializer(
|
||||||
|
exclude_fields=('prefix_count', 'vlan_count'),
|
||||||
|
)
|
||||||
class NestedRoleSerializer(WritableNestedSerializer):
|
class NestedRoleSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:role-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:role-detail')
|
||||||
prefix_count = serializers.IntegerField(read_only=True)
|
prefix_count = serializers.IntegerField(read_only=True)
|
||||||
@ -131,6 +141,9 @@ class NestedRoleSerializer(WritableNestedSerializer):
|
|||||||
fields = ['id', 'url', 'display', 'name', 'slug', 'prefix_count', 'vlan_count']
|
fields = ['id', 'url', 'display', 'name', 'slug', 'prefix_count', 'vlan_count']
|
||||||
|
|
||||||
|
|
||||||
|
@extend_schema_serializer(
|
||||||
|
exclude_fields=('vlan_count',),
|
||||||
|
)
|
||||||
class NestedVLANGroupSerializer(WritableNestedSerializer):
|
class NestedVLANGroupSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail')
|
||||||
vlan_count = serializers.IntegerField(read_only=True)
|
vlan_count = serializers.IntegerField(read_only=True)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from drf_yasg.utils import swagger_serializer_method
|
from drf_spectacular.utils import extend_schema_field
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from dcim.api.nested_serializers import NestedDeviceSerializer, NestedSiteSerializer
|
from dcim.api.nested_serializers import NestedDeviceSerializer, NestedSiteSerializer
|
||||||
@ -136,6 +136,7 @@ class AggregateSerializer(NetBoxModelSerializer):
|
|||||||
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
|
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
|
||||||
rir = NestedRIRSerializer()
|
rir = NestedRIRSerializer()
|
||||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||||
|
prefix = serializers.CharField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Aggregate
|
model = Aggregate
|
||||||
@ -177,7 +178,7 @@ class FHRPGroupAssignmentSerializer(NetBoxModelSerializer):
|
|||||||
'last_updated',
|
'last_updated',
|
||||||
]
|
]
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||||
def get_interface(self, obj):
|
def get_interface(self, obj):
|
||||||
if obj.interface is None:
|
if obj.interface is None:
|
||||||
return None
|
return None
|
||||||
@ -225,7 +226,7 @@ class VLANGroupSerializer(NetBoxModelSerializer):
|
|||||||
]
|
]
|
||||||
validators = []
|
validators = []
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||||
def get_scope(self, obj):
|
def get_scope(self, obj):
|
||||||
if obj.scope_id is None:
|
if obj.scope_id is None:
|
||||||
return None
|
return None
|
||||||
@ -242,7 +243,7 @@ class VLANSerializer(NetBoxModelSerializer):
|
|||||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||||
status = ChoiceField(choices=VLANStatusChoices, required=False)
|
status = ChoiceField(choices=VLANStatusChoices, required=False)
|
||||||
role = NestedRoleSerializer(required=False, allow_null=True)
|
role = NestedRoleSerializer(required=False, allow_null=True)
|
||||||
l2vpn_termination = NestedL2VPNTerminationSerializer(read_only=True)
|
l2vpn_termination = NestedL2VPNTerminationSerializer(read_only=True, allow_null=True)
|
||||||
prefix_count = serializers.IntegerField(read_only=True)
|
prefix_count = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -302,6 +303,7 @@ class PrefixSerializer(NetBoxModelSerializer):
|
|||||||
role = NestedRoleSerializer(required=False, allow_null=True)
|
role = NestedRoleSerializer(required=False, allow_null=True)
|
||||||
children = serializers.IntegerField(read_only=True)
|
children = serializers.IntegerField(read_only=True)
|
||||||
_depth = serializers.IntegerField(read_only=True)
|
_depth = serializers.IntegerField(read_only=True)
|
||||||
|
prefix = serializers.CharField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Prefix
|
model = Prefix
|
||||||
@ -371,13 +373,13 @@ class IPRangeSerializer(NetBoxModelSerializer):
|
|||||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||||
status = ChoiceField(choices=IPRangeStatusChoices, required=False)
|
status = ChoiceField(choices=IPRangeStatusChoices, required=False)
|
||||||
role = NestedRoleSerializer(required=False, allow_null=True)
|
role = NestedRoleSerializer(required=False, allow_null=True)
|
||||||
children = serializers.IntegerField(read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IPRange
|
model = IPRange
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'family', 'start_address', 'end_address', 'size', 'vrf', 'tenant', 'status', 'role',
|
'id', 'url', 'display', 'family', 'start_address', 'end_address', 'size', 'vrf', 'tenant', 'status', 'role',
|
||||||
'mark_utilized', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'children',
|
'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||||
|
'mark_utilized', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
read_only_fields = ['family']
|
read_only_fields = ['family']
|
||||||
|
|
||||||
@ -392,7 +394,7 @@ class IPAddressSerializer(NetBoxModelSerializer):
|
|||||||
vrf = NestedVRFSerializer(required=False, allow_null=True)
|
vrf = NestedVRFSerializer(required=False, allow_null=True)
|
||||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||||
status = ChoiceField(choices=IPAddressStatusChoices, required=False)
|
status = ChoiceField(choices=IPAddressStatusChoices, required=False)
|
||||||
role = ChoiceField(choices=IPAddressRoleChoices, allow_blank=True, required=False)
|
role = ChoiceField(choices=IPAddressRoleChoices, allow_blank=True, required=False, allow_null=True)
|
||||||
assigned_object_type = ContentTypeField(
|
assigned_object_type = ContentTypeField(
|
||||||
queryset=ContentType.objects.filter(IPADDRESS_ASSIGNMENT_MODELS),
|
queryset=ContentType.objects.filter(IPADDRESS_ASSIGNMENT_MODELS),
|
||||||
required=False,
|
required=False,
|
||||||
@ -410,7 +412,7 @@ class IPAddressSerializer(NetBoxModelSerializer):
|
|||||||
'tags', 'custom_fields', 'created', 'last_updated',
|
'tags', 'custom_fields', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||||
def get_assigned_object(self, obj):
|
def get_assigned_object(self, obj):
|
||||||
if obj.assigned_object is None:
|
if obj.assigned_object is None:
|
||||||
return None
|
return None
|
||||||
@ -519,7 +521,7 @@ class L2VPNTerminationSerializer(NetBoxModelSerializer):
|
|||||||
'assigned_object', 'tags', 'custom_fields', 'created', 'last_updated'
|
'assigned_object', 'tags', 'custom_fields', 'created', 'last_updated'
|
||||||
]
|
]
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||||
def get_assigned_object(self, instance):
|
def get_assigned_object(self, instance):
|
||||||
serializer = get_serializer_for_model(instance.assigned_object, prefix=NESTED_SERIALIZER_PREFIX)
|
serializer = get_serializer_for_model(instance.assigned_object, prefix=NESTED_SERIALIZER_PREFIX)
|
||||||
context = {'request': self.context['request']}
|
context = {'request': self.context['request']}
|
||||||
|
@ -2,7 +2,7 @@ from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
|
|||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django_pglocks import advisory_lock
|
from django_pglocks import advisory_lock
|
||||||
from drf_yasg.utils import swagger_auto_schema
|
from drf_spectacular.utils import extend_schema, extend_schema_view
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.routers import APIRootView
|
from rest_framework.routers import APIRootView
|
||||||
@ -210,7 +210,7 @@ def get_results_limit(request):
|
|||||||
class AvailableASNsView(ObjectValidationMixin, APIView):
|
class AvailableASNsView(ObjectValidationMixin, APIView):
|
||||||
queryset = ASN.objects.all()
|
queryset = ASN.objects.all()
|
||||||
|
|
||||||
@swagger_auto_schema(responses={200: serializers.AvailableASNSerializer(many=True)})
|
@extend_schema(methods=["get"], responses={200: serializers.AvailableASNSerializer(many=True)})
|
||||||
def get(self, request, pk):
|
def get(self, request, pk):
|
||||||
asnrange = get_object_or_404(ASNRange.objects.restrict(request.user), pk=pk)
|
asnrange = get_object_or_404(ASNRange.objects.restrict(request.user), pk=pk)
|
||||||
limit = get_results_limit(request)
|
limit = get_results_limit(request)
|
||||||
@ -224,10 +224,7 @@ class AvailableASNsView(ObjectValidationMixin, APIView):
|
|||||||
|
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
@swagger_auto_schema(
|
@extend_schema(methods=["post"], responses={201: serializers.ASNSerializer(many=True)})
|
||||||
request_body=serializers.AvailableASNSerializer,
|
|
||||||
responses={201: serializers.ASNSerializer(many=True)}
|
|
||||||
)
|
|
||||||
@advisory_lock(ADVISORY_LOCK_KEYS['available-asns'])
|
@advisory_lock(ADVISORY_LOCK_KEYS['available-asns'])
|
||||||
def post(self, request, pk):
|
def post(self, request, pk):
|
||||||
self.queryset = self.queryset.restrict(request.user, 'add')
|
self.queryset = self.queryset.restrict(request.user, 'add')
|
||||||
@ -274,11 +271,17 @@ class AvailableASNsView(ObjectValidationMixin, APIView):
|
|||||||
|
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
def get_serializer_class(self):
|
||||||
|
if self.request.method == "GET":
|
||||||
|
return serializers.AvailableASNSerializer
|
||||||
|
|
||||||
|
return serializers.ASNSerializer
|
||||||
|
|
||||||
|
|
||||||
class AvailablePrefixesView(ObjectValidationMixin, APIView):
|
class AvailablePrefixesView(ObjectValidationMixin, APIView):
|
||||||
queryset = Prefix.objects.all()
|
queryset = Prefix.objects.all()
|
||||||
|
|
||||||
@swagger_auto_schema(responses={200: serializers.AvailablePrefixSerializer(many=True)})
|
@extend_schema(methods=["get"], responses={200: serializers.AvailablePrefixSerializer(many=True)})
|
||||||
def get(self, request, pk):
|
def get(self, request, pk):
|
||||||
prefix = get_object_or_404(Prefix.objects.restrict(request.user), pk=pk)
|
prefix = get_object_or_404(Prefix.objects.restrict(request.user), pk=pk)
|
||||||
available_prefixes = prefix.get_available_prefixes()
|
available_prefixes = prefix.get_available_prefixes()
|
||||||
@ -290,10 +293,7 @@ class AvailablePrefixesView(ObjectValidationMixin, APIView):
|
|||||||
|
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
@swagger_auto_schema(
|
@extend_schema(methods=["post"], responses={201: serializers.PrefixSerializer(many=True)})
|
||||||
request_body=serializers.PrefixLengthSerializer,
|
|
||||||
responses={201: serializers.PrefixSerializer(many=True)}
|
|
||||||
)
|
|
||||||
@advisory_lock(ADVISORY_LOCK_KEYS['available-prefixes'])
|
@advisory_lock(ADVISORY_LOCK_KEYS['available-prefixes'])
|
||||||
def post(self, request, pk):
|
def post(self, request, pk):
|
||||||
self.queryset = self.queryset.restrict(request.user, 'add')
|
self.queryset = self.queryset.restrict(request.user, 'add')
|
||||||
@ -356,6 +356,12 @@ class AvailablePrefixesView(ObjectValidationMixin, APIView):
|
|||||||
|
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
def get_serializer_class(self):
|
||||||
|
if self.request.method == "GET":
|
||||||
|
return serializers.AvailablePrefixSerializer
|
||||||
|
|
||||||
|
return serializers.PrefixLengthSerializer
|
||||||
|
|
||||||
|
|
||||||
class AvailableIPAddressesView(ObjectValidationMixin, APIView):
|
class AvailableIPAddressesView(ObjectValidationMixin, APIView):
|
||||||
queryset = IPAddress.objects.all()
|
queryset = IPAddress.objects.all()
|
||||||
@ -363,7 +369,7 @@ class AvailableIPAddressesView(ObjectValidationMixin, APIView):
|
|||||||
def get_parent(self, request, pk):
|
def get_parent(self, request, pk):
|
||||||
raise NotImplemented()
|
raise NotImplemented()
|
||||||
|
|
||||||
@swagger_auto_schema(responses={200: serializers.AvailableIPSerializer(many=True)})
|
@extend_schema(methods=["get"], responses={200: serializers.AvailableIPSerializer(many=True)})
|
||||||
def get(self, request, pk):
|
def get(self, request, pk):
|
||||||
parent = self.get_parent(request, pk)
|
parent = self.get_parent(request, pk)
|
||||||
limit = get_results_limit(request)
|
limit = get_results_limit(request)
|
||||||
@ -382,10 +388,7 @@ class AvailableIPAddressesView(ObjectValidationMixin, APIView):
|
|||||||
|
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
@swagger_auto_schema(
|
@extend_schema(methods=["post"], responses={201: serializers.IPAddressSerializer(many=True)})
|
||||||
request_body=serializers.AvailableIPSerializer,
|
|
||||||
responses={201: serializers.IPAddressSerializer(many=True)}
|
|
||||||
)
|
|
||||||
@advisory_lock(ADVISORY_LOCK_KEYS['available-ips'])
|
@advisory_lock(ADVISORY_LOCK_KEYS['available-ips'])
|
||||||
def post(self, request, pk):
|
def post(self, request, pk):
|
||||||
self.queryset = self.queryset.restrict(request.user, 'add')
|
self.queryset = self.queryset.restrict(request.user, 'add')
|
||||||
@ -430,6 +433,12 @@ class AvailableIPAddressesView(ObjectValidationMixin, APIView):
|
|||||||
|
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
def get_serializer_class(self):
|
||||||
|
if self.request.method == "GET":
|
||||||
|
return serializers.AvailableIPSerializer
|
||||||
|
|
||||||
|
return serializers.IPAddressSerializer
|
||||||
|
|
||||||
|
|
||||||
class PrefixAvailableIPAddressesView(AvailableIPAddressesView):
|
class PrefixAvailableIPAddressesView(AvailableIPAddressesView):
|
||||||
|
|
||||||
@ -446,7 +455,7 @@ class IPRangeAvailableIPAddressesView(AvailableIPAddressesView):
|
|||||||
class AvailableVLANsView(ObjectValidationMixin, APIView):
|
class AvailableVLANsView(ObjectValidationMixin, APIView):
|
||||||
queryset = VLAN.objects.all()
|
queryset = VLAN.objects.all()
|
||||||
|
|
||||||
@swagger_auto_schema(responses={200: serializers.AvailableVLANSerializer(many=True)})
|
@extend_schema(methods=["get"], responses={200: serializers.AvailableVLANSerializer(many=True)})
|
||||||
def get(self, request, pk):
|
def get(self, request, pk):
|
||||||
vlangroup = get_object_or_404(VLANGroup.objects.restrict(request.user), pk=pk)
|
vlangroup = get_object_or_404(VLANGroup.objects.restrict(request.user), pk=pk)
|
||||||
limit = get_results_limit(request)
|
limit = get_results_limit(request)
|
||||||
@ -459,10 +468,7 @@ class AvailableVLANsView(ObjectValidationMixin, APIView):
|
|||||||
|
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
@swagger_auto_schema(
|
@extend_schema(methods=["post"], responses={201: serializers.VLANSerializer(many=True)})
|
||||||
request_body=serializers.CreateAvailableVLANSerializer,
|
|
||||||
responses={201: serializers.VLANSerializer(many=True)}
|
|
||||||
)
|
|
||||||
@advisory_lock(ADVISORY_LOCK_KEYS['available-vlans'])
|
@advisory_lock(ADVISORY_LOCK_KEYS['available-vlans'])
|
||||||
def post(self, request, pk):
|
def post(self, request, pk):
|
||||||
self.queryset = self.queryset.restrict(request.user, 'add')
|
self.queryset = self.queryset.restrict(request.user, 'add')
|
||||||
@ -514,3 +520,9 @@ class AvailableVLANsView(ObjectValidationMixin, APIView):
|
|||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
def get_serializer_class(self):
|
||||||
|
if self.request.method == "GET":
|
||||||
|
return serializers.AvailableVLANSerializer
|
||||||
|
|
||||||
|
return serializers.VLANSerializer
|
||||||
|
@ -16,8 +16,6 @@ from virtualization.models import VirtualMachine, VMInterface
|
|||||||
from .choices import *
|
from .choices import *
|
||||||
from .models import *
|
from .models import *
|
||||||
|
|
||||||
from rest_framework import serializers
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'AggregateFilterSet',
|
'AggregateFilterSet',
|
||||||
'ASNFilterSet',
|
'ASNFilterSet',
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
from drf_spectacular.utils import extend_schema_field
|
||||||
|
from drf_spectacular.types import OpenApiTypes
|
||||||
from netaddr import IPNetwork
|
from netaddr import IPNetwork
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
@ -12,6 +14,7 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@extend_schema_field(OpenApiTypes.STR)
|
||||||
class ChoiceField(serializers.Field):
|
class ChoiceField(serializers.Field):
|
||||||
"""
|
"""
|
||||||
Represent a ChoiceField as {'value': <DB value>, 'label': <string>}. Accepts a single value on write.
|
Represent a ChoiceField as {'value': <DB value>, 'label': <string>}. Accepts a single value on write.
|
||||||
@ -86,6 +89,7 @@ class ChoiceField(serializers.Field):
|
|||||||
return self._choices
|
return self._choices
|
||||||
|
|
||||||
|
|
||||||
|
@extend_schema_field(OpenApiTypes.STR)
|
||||||
class ContentTypeField(RelatedField):
|
class ContentTypeField(RelatedField):
|
||||||
"""
|
"""
|
||||||
Represent a ContentType as '<app_label>.<model>'
|
Represent a ContentType as '<app_label>.<model>'
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
from django.db.models import ManyToManyField
|
from django.db.models import ManyToManyField
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
from drf_spectacular.utils import extend_schema_field
|
||||||
|
from drf_spectacular.types import OpenApiTypes
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'BaseModelSerializer',
|
'BaseModelSerializer',
|
||||||
@ -10,6 +12,7 @@ __all__ = (
|
|||||||
class BaseModelSerializer(serializers.ModelSerializer):
|
class BaseModelSerializer(serializers.ModelSerializer):
|
||||||
display = serializers.SerializerMethodField(read_only=True)
|
display = serializers.SerializerMethodField(read_only=True)
|
||||||
|
|
||||||
|
@extend_schema_field(OpenApiTypes.STR)
|
||||||
def get_display(self, obj):
|
def get_display(self, obj):
|
||||||
return str(obj)
|
return str(obj)
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from drf_yasg.utils import swagger_serializer_method
|
from drf_spectacular.utils import extend_schema_field
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from netbox.api.fields import ContentTypeField
|
from netbox.api.fields import ContentTypeField
|
||||||
@ -38,7 +38,7 @@ class GenericObjectSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||||
def get_object(self, obj):
|
def get_object(self, obj):
|
||||||
serializer = get_serializer_for_model(obj, prefix=NESTED_SERIALIZER_PREFIX)
|
serializer = get_serializer_for_model(obj, prefix=NESTED_SERIALIZER_PREFIX)
|
||||||
# context = {'request': self.context['request']}
|
# context = {'request': self.context['request']}
|
||||||
|
@ -4,6 +4,8 @@ from django import __version__ as DJANGO_VERSION
|
|||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django_rq.queues import get_connection
|
from django_rq.queues import get_connection
|
||||||
|
from drf_spectacular.utils import extend_schema
|
||||||
|
from drf_spectacular.types import OpenApiTypes
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.reverse import reverse
|
from rest_framework.reverse import reverse
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
@ -17,12 +19,12 @@ class APIRootView(APIView):
|
|||||||
This is the root of NetBox's REST API. API endpoints are arranged by app and model name; e.g. `/api/dcim/sites/`.
|
This is the root of NetBox's REST API. API endpoints are arranged by app and model name; e.g. `/api/dcim/sites/`.
|
||||||
"""
|
"""
|
||||||
_ignore_model_permissions = True
|
_ignore_model_permissions = True
|
||||||
exclude_from_schema = True
|
# schema = None
|
||||||
swagger_schema = None
|
|
||||||
|
|
||||||
def get_view_name(self):
|
def get_view_name(self):
|
||||||
return "API Root"
|
return "API Root"
|
||||||
|
|
||||||
|
@extend_schema(exclude=True)
|
||||||
def get(self, request, format=None):
|
def get(self, request, format=None):
|
||||||
|
|
||||||
return Response({
|
return Response({
|
||||||
@ -46,6 +48,7 @@ class StatusView(APIView):
|
|||||||
"""
|
"""
|
||||||
permission_classes = [IsAuthenticatedOrLoginNotRequired]
|
permission_classes = [IsAuthenticatedOrLoginNotRequired]
|
||||||
|
|
||||||
|
@extend_schema(responses={200: OpenApiTypes.OBJECT})
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
# Gather the version numbers from all installed Django apps
|
# Gather the version numbers from all installed Django apps
|
||||||
installed_apps = {}
|
installed_apps = {}
|
||||||
|
@ -344,7 +344,7 @@ INSTALLED_APPS = [
|
|||||||
'virtualization',
|
'virtualization',
|
||||||
'wireless',
|
'wireless',
|
||||||
'django_rq', # Must come after extras to allow overriding management commands
|
'django_rq', # Must come after extras to allow overriding management commands
|
||||||
'drf_yasg',
|
'drf_spectacular',
|
||||||
]
|
]
|
||||||
|
|
||||||
# Middleware
|
# Middleware
|
||||||
@ -561,6 +561,7 @@ REST_FRAMEWORK = {
|
|||||||
'rest_framework.renderers.JSONRenderer',
|
'rest_framework.renderers.JSONRenderer',
|
||||||
'netbox.api.renderers.FormlessBrowsableAPIRenderer',
|
'netbox.api.renderers.FormlessBrowsableAPIRenderer',
|
||||||
),
|
),
|
||||||
|
'DEFAULT_SCHEMA_CLASS': 'core.api.schema.NetBoxAutoSchema',
|
||||||
'DEFAULT_VERSION': REST_FRAMEWORK_VERSION,
|
'DEFAULT_VERSION': REST_FRAMEWORK_VERSION,
|
||||||
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.AcceptHeaderVersioning',
|
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.AcceptHeaderVersioning',
|
||||||
'SCHEMA_COERCE_METHOD_NAMES': {
|
'SCHEMA_COERCE_METHOD_NAMES': {
|
||||||
@ -573,6 +574,17 @@ REST_FRAMEWORK = {
|
|||||||
'VIEW_NAME_FUNCTION': 'utilities.api.get_view_name',
|
'VIEW_NAME_FUNCTION': 'utilities.api.get_view_name',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#
|
||||||
|
# DRF Spectacular
|
||||||
|
#
|
||||||
|
|
||||||
|
SPECTACULAR_SETTINGS = {
|
||||||
|
"TITLE": "NetBox API",
|
||||||
|
"DESCRIPTION": "API to access NetBox",
|
||||||
|
"LICENSE": {"name": "Apache v2 License"},
|
||||||
|
"VERSION": VERSION,
|
||||||
|
'COMPONENT_SPLIT_REQUEST': True,
|
||||||
|
}
|
||||||
|
|
||||||
#
|
#
|
||||||
# Graphene
|
# Graphene
|
||||||
@ -585,49 +597,6 @@ GRAPHENE = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# drf_yasg (OpenAPI/Swagger)
|
|
||||||
#
|
|
||||||
|
|
||||||
SWAGGER_SETTINGS = {
|
|
||||||
'DEFAULT_AUTO_SCHEMA_CLASS': 'utilities.custom_inspectors.NetBoxSwaggerAutoSchema',
|
|
||||||
'DEFAULT_FIELD_INSPECTORS': [
|
|
||||||
'utilities.custom_inspectors.CustomFieldsDataFieldInspector',
|
|
||||||
'utilities.custom_inspectors.NullableBooleanFieldInspector',
|
|
||||||
'utilities.custom_inspectors.ChoiceFieldInspector',
|
|
||||||
'utilities.custom_inspectors.SerializedPKRelatedFieldInspector',
|
|
||||||
'drf_yasg.inspectors.CamelCaseJSONFilter',
|
|
||||||
'drf_yasg.inspectors.ReferencingSerializerInspector',
|
|
||||||
'drf_yasg.inspectors.RelatedFieldInspector',
|
|
||||||
'drf_yasg.inspectors.ChoiceFieldInspector',
|
|
||||||
'drf_yasg.inspectors.FileFieldInspector',
|
|
||||||
'drf_yasg.inspectors.DictFieldInspector',
|
|
||||||
'drf_yasg.inspectors.JSONFieldInspector',
|
|
||||||
'drf_yasg.inspectors.SerializerMethodFieldInspector',
|
|
||||||
'drf_yasg.inspectors.SimpleFieldInspector',
|
|
||||||
'drf_yasg.inspectors.StringDefaultFieldInspector',
|
|
||||||
],
|
|
||||||
'DEFAULT_FILTER_INSPECTORS': [
|
|
||||||
'drf_yasg.inspectors.CoreAPICompatInspector',
|
|
||||||
],
|
|
||||||
'DEFAULT_INFO': 'netbox.urls.openapi_info',
|
|
||||||
'DEFAULT_MODEL_DEPTH': 1,
|
|
||||||
'DEFAULT_PAGINATOR_INSPECTORS': [
|
|
||||||
'utilities.custom_inspectors.NullablePaginatorInspector',
|
|
||||||
'drf_yasg.inspectors.DjangoRestResponsePagination',
|
|
||||||
'drf_yasg.inspectors.CoreAPICompatInspector',
|
|
||||||
],
|
|
||||||
'SECURITY_DEFINITIONS': {
|
|
||||||
'Bearer': {
|
|
||||||
'type': 'apiKey',
|
|
||||||
'name': 'Authorization',
|
|
||||||
'in': 'header',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'VALIDATOR_URL': None,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Django RQ (Webhooks backend)
|
# Django RQ (Webhooks backend)
|
||||||
#
|
#
|
||||||
|
@ -3,8 +3,7 @@ from django.conf.urls import include
|
|||||||
from django.urls import path, re_path
|
from django.urls import path, re_path
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from django.views.static import serve
|
from django.views.static import serve
|
||||||
from drf_yasg import openapi
|
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
|
||||||
from drf_yasg.views import get_schema_view
|
|
||||||
|
|
||||||
from extras.plugins.urls import plugin_admin_patterns, plugin_patterns, plugin_api_patterns
|
from extras.plugins.urls import plugin_admin_patterns, plugin_patterns, plugin_api_patterns
|
||||||
from netbox.api.views import APIRootView, StatusView
|
from netbox.api.views import APIRootView, StatusView
|
||||||
@ -14,20 +13,6 @@ from netbox.views import HomeView, StaticMediaFailureView, SearchView, htmx
|
|||||||
from users.views import LoginView, LogoutView
|
from users.views import LoginView, LogoutView
|
||||||
from .admin import admin_site
|
from .admin import admin_site
|
||||||
|
|
||||||
openapi_info = openapi.Info(
|
|
||||||
title="NetBox API",
|
|
||||||
default_version='v3',
|
|
||||||
description="API to access NetBox",
|
|
||||||
terms_of_service="https://github.com/netbox-community/netbox",
|
|
||||||
license=openapi.License(name="Apache v2 License"),
|
|
||||||
)
|
|
||||||
|
|
||||||
schema_view = get_schema_view(
|
|
||||||
openapi_info,
|
|
||||||
validators=['flex', 'ssv'],
|
|
||||||
public=True,
|
|
||||||
permission_classes=()
|
|
||||||
)
|
|
||||||
|
|
||||||
_patterns = [
|
_patterns = [
|
||||||
|
|
||||||
@ -66,9 +51,10 @@ _patterns = [
|
|||||||
path('api/virtualization/', include('virtualization.api.urls')),
|
path('api/virtualization/', include('virtualization.api.urls')),
|
||||||
path('api/wireless/', include('wireless.api.urls')),
|
path('api/wireless/', include('wireless.api.urls')),
|
||||||
path('api/status/', StatusView.as_view(), name='api-status'),
|
path('api/status/', StatusView.as_view(), name='api-status'),
|
||||||
path('api/docs/', schema_view.with_ui('swagger', cache_timeout=86400), name='api_docs'),
|
|
||||||
path('api/redoc/', schema_view.with_ui('redoc', cache_timeout=86400), name='api_redocs'),
|
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
|
||||||
re_path(r'^api/swagger(?P<format>.json|.yaml)$', schema_view.without_ui(cache_timeout=86400), name='schema_swagger'),
|
path('api/schema/swagger-ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='api_docs'),
|
||||||
|
path('api/schema/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='api_redocs'),
|
||||||
|
|
||||||
# GraphQL
|
# GraphQL
|
||||||
path('graphql/', csrf_exempt(GraphQLView.as_view(graphiql=True, schema=schema)), name='graphql'),
|
path('graphql/', csrf_exempt(GraphQLView.as_view(graphiql=True, schema=schema)), name='graphql'),
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from drf_spectacular.utils import extend_schema_serializer
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from netbox.api.serializers import WritableNestedSerializer
|
from netbox.api.serializers import WritableNestedSerializer
|
||||||
@ -17,6 +18,9 @@ __all__ = [
|
|||||||
# Tenants
|
# Tenants
|
||||||
#
|
#
|
||||||
|
|
||||||
|
@extend_schema_serializer(
|
||||||
|
exclude_fields=('tenant_count',),
|
||||||
|
)
|
||||||
class NestedTenantGroupSerializer(WritableNestedSerializer):
|
class NestedTenantGroupSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenantgroup-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenantgroup-detail')
|
||||||
tenant_count = serializers.IntegerField(read_only=True)
|
tenant_count = serializers.IntegerField(read_only=True)
|
||||||
@ -39,6 +43,9 @@ class NestedTenantSerializer(WritableNestedSerializer):
|
|||||||
# Contacts
|
# Contacts
|
||||||
#
|
#
|
||||||
|
|
||||||
|
@extend_schema_serializer(
|
||||||
|
exclude_fields=('contact_count',),
|
||||||
|
)
|
||||||
class NestedContactGroupSerializer(WritableNestedSerializer):
|
class NestedContactGroupSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactgroup-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactgroup-detail')
|
||||||
contact_count = serializers.IntegerField(read_only=True)
|
contact_count = serializers.IntegerField(read_only=True)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from django.contrib.auth.models import ContentType
|
from django.contrib.auth.models import ContentType
|
||||||
from drf_yasg.utils import swagger_serializer_method
|
from drf_spectacular.types import OpenApiTypes
|
||||||
|
from drf_spectacular.utils import extend_schema_field
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from netbox.api.fields import ChoiceField, ContentTypeField
|
from netbox.api.fields import ChoiceField, ContentTypeField
|
||||||
@ -98,7 +99,7 @@ class ContactAssignmentSerializer(NetBoxModelSerializer):
|
|||||||
object = serializers.SerializerMethodField(read_only=True)
|
object = serializers.SerializerMethodField(read_only=True)
|
||||||
contact = NestedContactSerializer()
|
contact = NestedContactSerializer()
|
||||||
role = NestedContactRoleSerializer(required=False, allow_null=True)
|
role = NestedContactRoleSerializer(required=False, allow_null=True)
|
||||||
priority = ChoiceField(choices=ContactPriorityChoices, allow_blank=True, required=False, default='')
|
priority = ChoiceField(choices=ContactPriorityChoices, allow_blank=True, required=False, default=lambda: '')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ContactAssignment
|
model = ContactAssignment
|
||||||
@ -107,7 +108,7 @@ class ContactAssignmentSerializer(NetBoxModelSerializer):
|
|||||||
'last_updated',
|
'last_updated',
|
||||||
]
|
]
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
@extend_schema_field(OpenApiTypes.OBJECT)
|
||||||
def get_object(self, instance):
|
def get_object(self, instance):
|
||||||
serializer = get_serializer_for_model(instance.content_type.model_class(), prefix=NESTED_SERIALIZER_PREFIX)
|
serializer = get_serializer_for_model(instance.content_type.model_class(), prefix=NESTED_SERIALIZER_PREFIX)
|
||||||
context = {'request': self.context['request']}
|
context = {'request': self.context['request']}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from django.contrib.auth.models import Group, User
|
from django.contrib.auth.models import Group, User
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from drf_yasg.utils import swagger_serializer_method
|
from drf_spectacular.utils import extend_schema_field
|
||||||
|
from drf_spectacular.types import OpenApiTypes
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from netbox.api.fields import ContentTypeField
|
from netbox.api.fields import ContentTypeField
|
||||||
@ -30,6 +31,7 @@ class NestedUserSerializer(WritableNestedSerializer):
|
|||||||
model = User
|
model = User
|
||||||
fields = ['id', 'url', 'display', 'username']
|
fields = ['id', 'url', 'display', 'username']
|
||||||
|
|
||||||
|
@extend_schema_field(OpenApiTypes.STR)
|
||||||
def get_display(self, obj):
|
def get_display(self, obj):
|
||||||
if full_name := obj.get_full_name():
|
if full_name := obj.get_full_name():
|
||||||
return f"{obj.username} ({full_name})"
|
return f"{obj.username} ({full_name})"
|
||||||
@ -57,10 +59,10 @@ class NestedObjectPermissionSerializer(WritableNestedSerializer):
|
|||||||
model = ObjectPermission
|
model = ObjectPermission
|
||||||
fields = ['id', 'url', 'display', 'name', 'enabled', 'object_types', 'groups', 'users', 'actions']
|
fields = ['id', 'url', 'display', 'name', 'enabled', 'object_types', 'groups', 'users', 'actions']
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.ListField)
|
@extend_schema_field(serializers.ListField)
|
||||||
def get_groups(self, obj):
|
def get_groups(self, obj):
|
||||||
return [g.name for g in obj.groups.all()]
|
return [g.name for g in obj.groups.all()]
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.ListField)
|
@extend_schema_field(serializers.ListField)
|
||||||
def get_users(self, obj):
|
def get_users(self, obj):
|
||||||
return [u.username for u in obj.users.all()]
|
return [u.username for u in obj.users.all()]
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import Group, User
|
from django.contrib.auth.models import Group, User
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from drf_spectacular.utils import extend_schema_field
|
||||||
|
from drf_spectacular.types import OpenApiTypes
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from netbox.api.fields import ContentTypeField, IPNetworkSerializer, SerializedPKRelatedField
|
from netbox.api.fields import ContentTypeField, IPNetworkSerializer, SerializedPKRelatedField
|
||||||
@ -47,6 +49,7 @@ class UserSerializer(ValidatedModelSerializer):
|
|||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
@extend_schema_field(OpenApiTypes.STR)
|
||||||
def get_display(self, obj):
|
def get_display(self, obj):
|
||||||
if full_name := obj.get_full_name():
|
if full_name := obj.get_full_name():
|
||||||
return f"{obj.username} ({full_name})"
|
return f"{obj.username} ({full_name})"
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
from django.contrib.auth import authenticate
|
from django.contrib.auth import authenticate
|
||||||
from django.contrib.auth.models import Group, User
|
from django.contrib.auth.models import Group, User
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
|
from drf_spectacular.utils import extend_schema
|
||||||
|
from drf_spectacular.types import OpenApiTypes
|
||||||
from rest_framework.exceptions import AuthenticationFailed
|
from rest_framework.exceptions import AuthenticationFailed
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
@ -55,9 +57,6 @@ class TokenViewSet(NetBoxModelViewSet):
|
|||||||
Limit the non-superusers to their own Tokens.
|
Limit the non-superusers to their own Tokens.
|
||||||
"""
|
"""
|
||||||
queryset = super().get_queryset()
|
queryset = super().get_queryset()
|
||||||
# Workaround for schema generation (drf_yasg)
|
|
||||||
if getattr(self, 'swagger_fake_view', False):
|
|
||||||
return queryset.none()
|
|
||||||
if not self.request.user.is_authenticated:
|
if not self.request.user.is_authenticated:
|
||||||
return queryset.none()
|
return queryset.none()
|
||||||
if self.request.user.is_superuser:
|
if self.request.user.is_superuser:
|
||||||
@ -71,6 +70,7 @@ class TokenProvisionView(APIView):
|
|||||||
"""
|
"""
|
||||||
permission_classes = []
|
permission_classes = []
|
||||||
|
|
||||||
|
# @extend_schema(methods=["post"], responses={201: serializers.TokenSerializer})
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
serializer = serializers.TokenProvisionSerializer(data=request.data)
|
serializer = serializers.TokenProvisionSerializer(data=request.data)
|
||||||
serializer.is_valid()
|
serializer.is_valid()
|
||||||
@ -93,6 +93,9 @@ class TokenProvisionView(APIView):
|
|||||||
|
|
||||||
return Response(data, status=HTTP_201_CREATED)
|
return Response(data, status=HTTP_201_CREATED)
|
||||||
|
|
||||||
|
def get_serializer_class(self):
|
||||||
|
return serializers.TokenSerializer
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# ObjectPermissions
|
# ObjectPermissions
|
||||||
@ -117,6 +120,7 @@ class UserConfigViewSet(ViewSet):
|
|||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return UserConfig.objects.filter(user=self.request.user)
|
return UserConfig.objects.filter(user=self.request.user)
|
||||||
|
|
||||||
|
@extend_schema(responses={200: OpenApiTypes.OBJECT})
|
||||||
def list(self, request):
|
def list(self, request):
|
||||||
"""
|
"""
|
||||||
Return the UserConfig for the currently authenticated User.
|
Return the UserConfig for the currently authenticated User.
|
||||||
@ -125,6 +129,7 @@ class UserConfigViewSet(ViewSet):
|
|||||||
|
|
||||||
return Response(userconfig.data)
|
return Response(userconfig.data)
|
||||||
|
|
||||||
|
@extend_schema(methods=["patch"], responses={201: OpenApiTypes.OBJECT})
|
||||||
def patch(self, request):
|
def patch(self, request):
|
||||||
"""
|
"""
|
||||||
Update the UserConfig for the currently authenticated User.
|
Update the UserConfig for the currently authenticated User.
|
||||||
|
@ -1,142 +0,0 @@
|
|||||||
from drf_yasg import openapi
|
|
||||||
from drf_yasg.inspectors import FieldInspector, NotHandled, PaginatorInspector, SwaggerAutoSchema
|
|
||||||
from drf_yasg.utils import get_serializer_ref_name
|
|
||||||
from rest_framework.fields import ChoiceField
|
|
||||||
from rest_framework.relations import ManyRelatedField
|
|
||||||
|
|
||||||
from extras.api.customfields import CustomFieldsDataField
|
|
||||||
from netbox.api.fields import ChoiceField, SerializedPKRelatedField
|
|
||||||
from netbox.api.serializers import WritableNestedSerializer
|
|
||||||
|
|
||||||
|
|
||||||
class NetBoxSwaggerAutoSchema(SwaggerAutoSchema):
|
|
||||||
writable_serializers = {}
|
|
||||||
|
|
||||||
def get_operation_id(self, operation_keys=None):
|
|
||||||
operation_keys = operation_keys or self.operation_keys
|
|
||||||
operation_id = self.overrides.get('operation_id', '')
|
|
||||||
if not operation_id:
|
|
||||||
# Overwrite the action for bulk update/bulk delete views to ensure they get an operation ID that's
|
|
||||||
# unique from their single-object counterparts (see #3436)
|
|
||||||
if operation_keys[-1] in ('delete', 'partial_update', 'update') and not getattr(self.view, 'detail', None):
|
|
||||||
operation_keys[-1] = f'bulk_{operation_keys[-1]}'
|
|
||||||
operation_id = '_'.join(operation_keys)
|
|
||||||
|
|
||||||
return operation_id
|
|
||||||
|
|
||||||
def get_request_serializer(self):
|
|
||||||
serializer = super().get_request_serializer()
|
|
||||||
|
|
||||||
if serializer is not None and not isinstance(serializer, openapi.Schema) and self.method in self.implicit_body_methods:
|
|
||||||
if writable_class := self.get_writable_class(serializer):
|
|
||||||
if hasattr(serializer, 'child'):
|
|
||||||
child_serializer = self.get_writable_class(serializer.child)
|
|
||||||
serializer = writable_class(context=serializer.context, child=child_serializer)
|
|
||||||
else:
|
|
||||||
serializer = writable_class(context=serializer.context)
|
|
||||||
return serializer
|
|
||||||
|
|
||||||
def get_writable_class(self, serializer):
|
|
||||||
properties = {}
|
|
||||||
fields = {} if hasattr(serializer, 'child') else serializer.fields
|
|
||||||
for child_name, child in fields.items():
|
|
||||||
if isinstance(child, (ChoiceField, WritableNestedSerializer)):
|
|
||||||
properties[child_name] = None
|
|
||||||
elif isinstance(child, ManyRelatedField) and isinstance(child.child_relation, SerializedPKRelatedField):
|
|
||||||
properties[child_name] = None
|
|
||||||
|
|
||||||
if properties:
|
|
||||||
if type(serializer) not in self.writable_serializers:
|
|
||||||
writable_name = 'Writable' + type(serializer).__name__
|
|
||||||
meta_class = getattr(type(serializer), 'Meta', None)
|
|
||||||
if meta_class:
|
|
||||||
ref_name = 'Writable' + get_serializer_ref_name(serializer)
|
|
||||||
writable_meta = type('Meta', (meta_class,), {'ref_name': ref_name})
|
|
||||||
properties['Meta'] = writable_meta
|
|
||||||
|
|
||||||
self.writable_serializers[type(serializer)] = type(writable_name, (type(serializer),), properties)
|
|
||||||
|
|
||||||
writable_class = self.writable_serializers[type(serializer)]
|
|
||||||
return writable_class
|
|
||||||
|
|
||||||
|
|
||||||
class SerializedPKRelatedFieldInspector(FieldInspector):
|
|
||||||
def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs):
|
|
||||||
SwaggerType, ChildSwaggerType = self._get_partial_types(field, swagger_object_type, use_references, **kwargs)
|
|
||||||
if isinstance(field, SerializedPKRelatedField):
|
|
||||||
return self.probe_field_inspectors(field.serializer(), ChildSwaggerType, use_references)
|
|
||||||
|
|
||||||
return NotHandled
|
|
||||||
|
|
||||||
|
|
||||||
class ChoiceFieldInspector(FieldInspector):
|
|
||||||
def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs):
|
|
||||||
# this returns a callable which extracts title, description and other stuff
|
|
||||||
# https://drf-yasg.readthedocs.io/en/stable/_modules/drf_yasg/inspectors/base.html#FieldInspector._get_partial_types
|
|
||||||
SwaggerType, _ = self._get_partial_types(field, swagger_object_type, use_references, **kwargs)
|
|
||||||
|
|
||||||
if isinstance(field, ChoiceField):
|
|
||||||
choices = field._choices
|
|
||||||
choice_value = list(choices.keys())
|
|
||||||
choice_label = list(choices.values())
|
|
||||||
value_schema = openapi.Schema(type=openapi.TYPE_STRING, enum=choice_value)
|
|
||||||
|
|
||||||
if set([None] + choice_value) == {None, True, False}:
|
|
||||||
# DeviceType.subdevice_role and Device.face need to be differentiated since they each have
|
|
||||||
# subtly different values in their choice keys.
|
|
||||||
# - subdevice_role and connection_status are booleans, although subdevice_role includes None
|
|
||||||
# - face is an integer set {0, 1} which is easily confused with {False, True}
|
|
||||||
schema_type = openapi.TYPE_STRING
|
|
||||||
if all(type(x) == bool for x in [c for c in choice_value if c is not None]):
|
|
||||||
schema_type = openapi.TYPE_BOOLEAN
|
|
||||||
value_schema = openapi.Schema(type=schema_type, enum=choice_value)
|
|
||||||
value_schema['x-nullable'] = True
|
|
||||||
|
|
||||||
if all(type(x) == int for x in [c for c in choice_value if c is not None]):
|
|
||||||
# Change value_schema for IPAddressFamilyChoices, RackWidthChoices
|
|
||||||
value_schema = openapi.Schema(type=openapi.TYPE_INTEGER, enum=choice_value)
|
|
||||||
|
|
||||||
schema = SwaggerType(type=openapi.TYPE_OBJECT, required=["label", "value"], properties={
|
|
||||||
"label": openapi.Schema(type=openapi.TYPE_STRING, enum=choice_label),
|
|
||||||
"value": value_schema
|
|
||||||
})
|
|
||||||
|
|
||||||
return schema
|
|
||||||
|
|
||||||
return NotHandled
|
|
||||||
|
|
||||||
|
|
||||||
class NullableBooleanFieldInspector(FieldInspector):
|
|
||||||
def process_result(self, result, method_name, obj, **kwargs):
|
|
||||||
|
|
||||||
if isinstance(result, openapi.Schema) and isinstance(obj, ChoiceField) and result.type == 'boolean':
|
|
||||||
keys = obj.choices.keys()
|
|
||||||
if set(keys) == {None, True, False}:
|
|
||||||
result['x-nullable'] = True
|
|
||||||
result.type = 'boolean'
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
class CustomFieldsDataFieldInspector(FieldInspector):
|
|
||||||
|
|
||||||
def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs):
|
|
||||||
SwaggerType, ChildSwaggerType = self._get_partial_types(field, swagger_object_type, use_references, **kwargs)
|
|
||||||
|
|
||||||
if isinstance(field, CustomFieldsDataField) and swagger_object_type == openapi.Schema:
|
|
||||||
return SwaggerType(type=openapi.TYPE_OBJECT)
|
|
||||||
|
|
||||||
return NotHandled
|
|
||||||
|
|
||||||
|
|
||||||
class NullablePaginatorInspector(PaginatorInspector):
|
|
||||||
def process_result(self, result, method_name, obj, **kwargs):
|
|
||||||
if method_name == 'get_paginated_response' and isinstance(result, openapi.Schema):
|
|
||||||
next = result.properties['next']
|
|
||||||
if isinstance(next, openapi.Schema):
|
|
||||||
next['x-nullable'] = True
|
|
||||||
previous = result.properties['previous']
|
|
||||||
if isinstance(previous, openapi.Schema):
|
|
||||||
previous['x-nullable'] = True
|
|
||||||
|
|
||||||
return result
|
|
@ -3,6 +3,8 @@ from django import forms
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django_filters.constants import EMPTY_VALUES
|
from django_filters.constants import EMPTY_VALUES
|
||||||
|
from drf_spectacular.utils import extend_schema_field
|
||||||
|
from drf_spectacular.types import OpenApiTypes
|
||||||
|
|
||||||
|
|
||||||
def multivalue_field_factory(field_class):
|
def multivalue_field_factory(field_class):
|
||||||
@ -37,26 +39,32 @@ def multivalue_field_factory(field_class):
|
|||||||
# Filters
|
# Filters
|
||||||
#
|
#
|
||||||
|
|
||||||
|
@extend_schema_field(OpenApiTypes.STR)
|
||||||
class MultiValueCharFilter(django_filters.MultipleChoiceFilter):
|
class MultiValueCharFilter(django_filters.MultipleChoiceFilter):
|
||||||
field_class = multivalue_field_factory(forms.CharField)
|
field_class = multivalue_field_factory(forms.CharField)
|
||||||
|
|
||||||
|
|
||||||
|
@extend_schema_field(OpenApiTypes.DATE)
|
||||||
class MultiValueDateFilter(django_filters.MultipleChoiceFilter):
|
class MultiValueDateFilter(django_filters.MultipleChoiceFilter):
|
||||||
field_class = multivalue_field_factory(forms.DateField)
|
field_class = multivalue_field_factory(forms.DateField)
|
||||||
|
|
||||||
|
|
||||||
|
@extend_schema_field(OpenApiTypes.DATETIME)
|
||||||
class MultiValueDateTimeFilter(django_filters.MultipleChoiceFilter):
|
class MultiValueDateTimeFilter(django_filters.MultipleChoiceFilter):
|
||||||
field_class = multivalue_field_factory(forms.DateTimeField)
|
field_class = multivalue_field_factory(forms.DateTimeField)
|
||||||
|
|
||||||
|
|
||||||
|
@extend_schema_field(OpenApiTypes.INT32)
|
||||||
class MultiValueNumberFilter(django_filters.MultipleChoiceFilter):
|
class MultiValueNumberFilter(django_filters.MultipleChoiceFilter):
|
||||||
field_class = multivalue_field_factory(forms.IntegerField)
|
field_class = multivalue_field_factory(forms.IntegerField)
|
||||||
|
|
||||||
|
|
||||||
|
@extend_schema_field(OpenApiTypes.DECIMAL)
|
||||||
class MultiValueDecimalFilter(django_filters.MultipleChoiceFilter):
|
class MultiValueDecimalFilter(django_filters.MultipleChoiceFilter):
|
||||||
field_class = multivalue_field_factory(forms.DecimalField)
|
field_class = multivalue_field_factory(forms.DecimalField)
|
||||||
|
|
||||||
|
|
||||||
|
@extend_schema_field(OpenApiTypes.TIME)
|
||||||
class MultiValueTimeFilter(django_filters.MultipleChoiceFilter):
|
class MultiValueTimeFilter(django_filters.MultipleChoiceFilter):
|
||||||
field_class = multivalue_field_factory(forms.TimeField)
|
field_class = multivalue_field_factory(forms.TimeField)
|
||||||
|
|
||||||
@ -65,6 +73,7 @@ class MACAddressFilter(django_filters.CharFilter):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@extend_schema_field(OpenApiTypes.STR)
|
||||||
class MultiValueMACAddressFilter(django_filters.MultipleChoiceFilter):
|
class MultiValueMACAddressFilter(django_filters.MultipleChoiceFilter):
|
||||||
field_class = multivalue_field_factory(forms.CharField)
|
field_class = multivalue_field_factory(forms.CharField)
|
||||||
|
|
||||||
@ -75,6 +84,7 @@ class MultiValueMACAddressFilter(django_filters.MultipleChoiceFilter):
|
|||||||
return qs.none()
|
return qs.none()
|
||||||
|
|
||||||
|
|
||||||
|
@extend_schema_field(OpenApiTypes.STR)
|
||||||
class MultiValueWWNFilter(django_filters.MultipleChoiceFilter):
|
class MultiValueWWNFilter(django_filters.MultipleChoiceFilter):
|
||||||
field_class = multivalue_field_factory(forms.CharField)
|
field_class = multivalue_field_factory(forms.CharField)
|
||||||
|
|
||||||
|
@ -249,9 +249,9 @@ class APIDocsTestCase(TestCase):
|
|||||||
def test_api_docs(self):
|
def test_api_docs(self):
|
||||||
|
|
||||||
url = reverse('api_docs')
|
url = reverse('api_docs')
|
||||||
params = {
|
response = self.client.get(url)
|
||||||
"format": "openapi",
|
self.assertEqual(response.status_code, 200)
|
||||||
}
|
|
||||||
|
url = reverse('schema')
|
||||||
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
|
response = self.client.get(url)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from drf_spectacular.utils import extend_schema_serializer
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from netbox.api.serializers import WritableNestedSerializer
|
from netbox.api.serializers import WritableNestedSerializer
|
||||||
@ -16,6 +17,9 @@ __all__ = [
|
|||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
|
@extend_schema_serializer(
|
||||||
|
exclude_fields=('cluster_count',),
|
||||||
|
)
|
||||||
class NestedClusterTypeSerializer(WritableNestedSerializer):
|
class NestedClusterTypeSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustertype-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustertype-detail')
|
||||||
cluster_count = serializers.IntegerField(read_only=True)
|
cluster_count = serializers.IntegerField(read_only=True)
|
||||||
@ -25,6 +29,9 @@ class NestedClusterTypeSerializer(WritableNestedSerializer):
|
|||||||
fields = ['id', 'url', 'display', 'name', 'slug', 'cluster_count']
|
fields = ['id', 'url', 'display', 'name', 'slug', 'cluster_count']
|
||||||
|
|
||||||
|
|
||||||
|
@extend_schema_serializer(
|
||||||
|
exclude_fields=('cluster_count',),
|
||||||
|
)
|
||||||
class NestedClusterGroupSerializer(WritableNestedSerializer):
|
class NestedClusterGroupSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustergroup-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustergroup-detail')
|
||||||
cluster_count = serializers.IntegerField(read_only=True)
|
cluster_count = serializers.IntegerField(read_only=True)
|
||||||
@ -34,6 +41,9 @@ class NestedClusterGroupSerializer(WritableNestedSerializer):
|
|||||||
fields = ['id', 'url', 'display', 'name', 'slug', 'cluster_count']
|
fields = ['id', 'url', 'display', 'name', 'slug', 'cluster_count']
|
||||||
|
|
||||||
|
|
||||||
|
@extend_schema_serializer(
|
||||||
|
exclude_fields=('virtualmachine_count',),
|
||||||
|
)
|
||||||
class NestedClusterSerializer(WritableNestedSerializer):
|
class NestedClusterSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail')
|
||||||
virtualmachine_count = serializers.IntegerField(read_only=True)
|
virtualmachine_count = serializers.IntegerField(read_only=True)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from drf_yasg.utils import swagger_serializer_method
|
from drf_spectacular.utils import extend_schema_field
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from dcim.api.nested_serializers import (
|
from dcim.api.nested_serializers import (
|
||||||
@ -100,7 +100,7 @@ class VirtualMachineWithConfigContextSerializer(VirtualMachineSerializer):
|
|||||||
'local_context_data', 'tags', 'custom_fields', 'config_context', 'created', 'last_updated',
|
'local_context_data', 'tags', 'custom_fields', 'config_context', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||||
def get_config_context(self, obj):
|
def get_config_context(self, obj):
|
||||||
return obj.get_config_context()
|
return obj.get_config_context()
|
||||||
|
|
||||||
@ -114,7 +114,7 @@ class VMInterfaceSerializer(NetBoxModelSerializer):
|
|||||||
virtual_machine = NestedVirtualMachineSerializer()
|
virtual_machine = NestedVirtualMachineSerializer()
|
||||||
parent = NestedVMInterfaceSerializer(required=False, allow_null=True)
|
parent = NestedVMInterfaceSerializer(required=False, allow_null=True)
|
||||||
bridge = NestedVMInterfaceSerializer(required=False, allow_null=True)
|
bridge = NestedVMInterfaceSerializer(required=False, allow_null=True)
|
||||||
mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False)
|
mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False, allow_null=True)
|
||||||
untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
|
untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
|
||||||
tagged_vlans = SerializedPKRelatedField(
|
tagged_vlans = SerializedPKRelatedField(
|
||||||
queryset=VLAN.objects.all(),
|
queryset=VLAN.objects.all(),
|
||||||
@ -123,9 +123,10 @@ class VMInterfaceSerializer(NetBoxModelSerializer):
|
|||||||
many=True
|
many=True
|
||||||
)
|
)
|
||||||
vrf = NestedVRFSerializer(required=False, allow_null=True)
|
vrf = NestedVRFSerializer(required=False, allow_null=True)
|
||||||
l2vpn_termination = NestedL2VPNTerminationSerializer(read_only=True)
|
l2vpn_termination = NestedL2VPNTerminationSerializer(read_only=True, allow_null=True)
|
||||||
count_ipaddresses = serializers.IntegerField(read_only=True)
|
count_ipaddresses = serializers.IntegerField(read_only=True)
|
||||||
count_fhrp_groups = serializers.IntegerField(read_only=True)
|
count_fhrp_groups = serializers.IntegerField(read_only=True)
|
||||||
|
mac_address = serializers.CharField(required=False, default=None)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VMInterface
|
model = VMInterface
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from drf_spectacular.utils import extend_schema_serializer
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from netbox.api.serializers import WritableNestedSerializer
|
from netbox.api.serializers import WritableNestedSerializer
|
||||||
@ -10,6 +11,9 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@extend_schema_serializer(
|
||||||
|
exclude_fields=('wirelesslan_count',),
|
||||||
|
)
|
||||||
class NestedWirelessLANGroupSerializer(WritableNestedSerializer):
|
class NestedWirelessLANGroupSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslangroup-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslangroup-detail')
|
||||||
wirelesslan_count = serializers.IntegerField(read_only=True)
|
wirelesslan_count = serializers.IntegerField(read_only=True)
|
||||||
|
@ -15,7 +15,7 @@ django-tables2==2.5.3
|
|||||||
django-taggit==3.1.0
|
django-taggit==3.1.0
|
||||||
django-timezone-field==5.0
|
django-timezone-field==5.0
|
||||||
djangorestframework==3.14.0
|
djangorestframework==3.14.0
|
||||||
drf-yasg[validation]==1.21.5
|
drf-spectacular==0.25.1
|
||||||
feedparser==6.0.10
|
feedparser==6.0.10
|
||||||
graphene-django==3.0.0
|
graphene-django==3.0.0
|
||||||
gunicorn==20.1.0
|
gunicorn==20.1.0
|
||||||
|
Loading…
Reference in New Issue
Block a user