Merge pull request #22275 from jniec-js/main
Closes: #22245: Fix OpenAPI request schemas for bulk update endpoints
This commit is contained in:
commit
c1d69ebae6
|
|
@ -138,14 +138,30 @@ class NetBoxAutoSchema(AutoSchema):
|
|||
return super().get_operation_id()
|
||||
|
||||
def get_request_serializer(self) -> typing.Any:
|
||||
# bulk operations should specify a list
|
||||
serializer = super().get_request_serializer()
|
||||
|
||||
# Bulk update/partial-update has a special request shape: a list of
|
||||
# writable objects plus a required `id` field. The normal writable
|
||||
# serializer omits `id` because it is read-only, so don't use the generic
|
||||
# bulk handling for these actions.
|
||||
action = getattr(self.view, 'action', None)
|
||||
if action in ('bulk_update', 'bulk_partial_update'):
|
||||
get_bulk_update_request_serializer = getattr(
|
||||
self.view,
|
||||
'get_bulk_update_request_serializer',
|
||||
None,
|
||||
)
|
||||
if get_bulk_update_request_serializer is not None:
|
||||
return get_bulk_update_request_serializer(
|
||||
partial=(action == 'bulk_partial_update' or self.method == 'PATCH')
|
||||
)
|
||||
|
||||
# Bulk creates/deletes should specify a list.
|
||||
if self.is_bulk_action:
|
||||
return type(serializer)(many=True)
|
||||
|
||||
# handle mapping for Writable serializers - adapted from dansheps original code
|
||||
# for drf-yasg
|
||||
# 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:
|
||||
|
|
|
|||
|
|
@ -1,11 +1,79 @@
|
|||
import copy
|
||||
import functools
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from .features import ChangeLogMessageSerializer
|
||||
|
||||
__all__ = (
|
||||
'BulkOperationSerializer',
|
||||
'BulkPartialUpdateSchemaMixin',
|
||||
'BulkUpdateSchemaMixin',
|
||||
'get_bulk_update_serializer_class'
|
||||
)
|
||||
|
||||
|
||||
class BulkOperationSerializer(ChangeLogMessageSerializer):
|
||||
id = serializers.IntegerField()
|
||||
|
||||
|
||||
class BulkUpdateSchemaMixin:
|
||||
def get_fields(self):
|
||||
fields = super().get_fields()
|
||||
# Reuse the runtime bulk-operation ID field so the schema stays in sync
|
||||
# with the validator that consumes `id` before model serialization.
|
||||
_id = copy.deepcopy(BulkOperationSerializer().fields['id'])
|
||||
_id.required = True
|
||||
fields['id'] = _id
|
||||
|
||||
return fields
|
||||
|
||||
|
||||
class BulkPartialUpdateSchemaMixin(BulkUpdateSchemaMixin):
|
||||
def get_fields(self):
|
||||
fields = super().get_fields()
|
||||
|
||||
for name, field in fields.items():
|
||||
if name != 'id':
|
||||
field.required = False
|
||||
|
||||
return fields
|
||||
|
||||
|
||||
@functools.cache
|
||||
def get_bulk_update_serializer_class(serializer_class, *, partial=False):
|
||||
"""
|
||||
Return a schema-only serializer for bulk PUT/PATCH requests.
|
||||
|
||||
Bulk update requests to a list endpoint require each object to include
|
||||
the target object's numeric ID, even though `id` is read-only on the
|
||||
normal model serializer. The runtime code consumes `id` before invoking
|
||||
the model serializer for each object.
|
||||
"""
|
||||
|
||||
meta = getattr(serializer_class, 'Meta')
|
||||
|
||||
if meta.fields == '__all__':
|
||||
fields = '__all__'
|
||||
else:
|
||||
fields = ('id', *[f for f in meta.fields if f != 'id'])
|
||||
|
||||
class Meta(meta):
|
||||
pass
|
||||
|
||||
# intentional; this is different than setting fields = fields within class Meta above
|
||||
Meta.fields = fields
|
||||
|
||||
bases = (
|
||||
(BulkPartialUpdateSchemaMixin, serializer_class)
|
||||
if partial
|
||||
else (BulkUpdateSchemaMixin, serializer_class)
|
||||
)
|
||||
|
||||
attrs = {
|
||||
'Meta': Meta,
|
||||
'__module__': serializer_class.__module__,
|
||||
}
|
||||
|
||||
prefix = 'PatchedBulk' if partial else 'Bulk'
|
||||
return type(f'{prefix}{serializer_class.__name__}', bases, attrs)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ from rest_framework.response import Response
|
|||
from core.models import ObjectType
|
||||
from extras.models import ExportTemplate
|
||||
from netbox.api.serializers import BulkOperationSerializer
|
||||
from netbox.api.serializers.bulk import get_bulk_update_serializer_class
|
||||
|
||||
__all__ = (
|
||||
'BulkDestroyModelMixin',
|
||||
|
|
@ -133,6 +134,20 @@ class BulkUpdateModelMixin:
|
|||
|
||||
return updated_pks
|
||||
|
||||
def get_bulk_update_serializer_class(self, *, partial=False):
|
||||
return get_bulk_update_serializer_class(
|
||||
self.get_serializer_class(),
|
||||
partial=partial,
|
||||
)
|
||||
|
||||
def get_bulk_update_request_serializer(self, *, partial=False):
|
||||
serializer_class = self.get_bulk_update_serializer_class(partial=partial)
|
||||
|
||||
# Important: do NOT pass partial=True here. The partial schema class already
|
||||
# makes non-id fields optional, and passing partial=True would also make id
|
||||
# appear optional in OpenAPI.
|
||||
return serializer_class(many=True)
|
||||
|
||||
def bulk_partial_update(self, request, *args, **kwargs):
|
||||
kwargs['partial'] = True
|
||||
return self.bulk_update(request, *args, **kwargs)
|
||||
|
|
|
|||
Loading…
Reference in New Issue