diff --git a/netbox/core/api/schema.py b/netbox/core/api/schema.py new file mode 100644 index 000000000..be1dd894b --- /dev/null +++ b/netbox/core/api/schema.py @@ -0,0 +1,99 @@ +import logging +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_object_type, + is_serializer, +) +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import extend_schema + +BULK_ACTIONS = ["bulk_destroy", "bulk_partial_update", "bulk_update"] + + +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 spectaculars AutoSchema to fix following issues: + 1. bulk serializers cause operation_id conflicts with non-bulk ones + 2. bulk operations don't have filter params + """ + + @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(self, path, path_regex, path_prefix, method, registry: ComponentRegistry): + operation = super().get_operation(path, path_regex, path_prefix, method, registry) + return operation + + def get_operation_id(self): + """ + Fix: 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'', 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_filter_backends(self): + # Fix: bulk operations don't have filter params + if self.is_bulk_action: + return [] + return super().get_filter_backends() diff --git a/netbox/core/apps.py b/netbox/core/apps.py index c4886eb41..ffcf0b4ea 100644 --- a/netbox/core/apps.py +++ b/netbox/core/apps.py @@ -6,3 +6,4 @@ class CoreConfig(AppConfig): def ready(self): from . import data_backends, search + from core.api import schema # noqa: E402 diff --git a/netbox/dcim/apps.py b/netbox/dcim/apps.py index d9b45ae3f..bfb09e601 100644 --- a/netbox/dcim/apps.py +++ b/netbox/dcim/apps.py @@ -10,7 +10,6 @@ class DCIMConfig(AppConfig): def ready(self): from . import signals, search from .models import CableTermination - from . import schema # noqa: E402 # Register denormalized fields denormalized.register(CableTermination, '_device', { diff --git a/netbox/dcim/schema.py b/netbox/dcim/schema.py deleted file mode 100644 index 36ef1f8a6..000000000 --- a/netbox/dcim/schema.py +++ /dev/null @@ -1,30 +0,0 @@ -from drf_spectacular.extensions import ( - OpenApiSerializerFieldExtension, - OpenApiViewExtension, -) -from drf_spectacular.plumbing import build_basic_type, build_object_type -from drf_spectacular.types import OpenApiTypes -from drf_spectacular.utils import extend_schema - - -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), - } - ) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 4562b4906..23b77a0b9 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -558,7 +558,7 @@ REST_FRAMEWORK = { 'rest_framework.renderers.JSONRenderer', 'netbox.api.renderers.FormlessBrowsableAPIRenderer', ), - 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', + 'DEFAULT_SCHEMA_CLASS': 'core.api.schema.NetBoxAutoSchema', 'DEFAULT_VERSION': REST_FRAMEWORK_VERSION, 'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.AcceptHeaderVersioning', 'SCHEMA_COERCE_METHOD_NAMES': {