Преглед изворни кода

fix: make id field required in bulk patch/put open api schema

Josh Niec пре 1 месец
родитељ
комит
396a9a6ebe
3 измењених фајлова са 83 додато и 4 уклоњено
  1. 19 3
      netbox/core/api/schema.py
  2. 49 1
      netbox/netbox/api/serializers/bulk.py
  3. 15 0
      netbox/netbox/api/viewsets/mixins.py

+ 19 - 3
netbox/core/api/schema.py

@@ -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:

+ 49 - 1
netbox/netbox/api/serializers/bulk.py

@@ -1,11 +1,59 @@
+from functools import lru_cache
+
+from rest_framework import serializers
 from rest_framework import serializers
 
 from .features import ChangeLogMessageSerializer
 
 __all__ = (
-    'BulkOperationSerializer',
+    'BulkOperationSerializer', 
+    'BulkPartialUpdateSchemaMixin',
+    'get_bulk_update_serializer_class'
 )
 
 
+class BulkPartialUpdateSchemaMixin:
+    def get_fields(self):
+        fields = super().get_fields()
+        fields['id'] = serializers.IntegerField(required=True)
+
+        for name, field in fields.items():
+            if name != 'id':
+                field.required = False
+
+        return fields
+
+
+@lru_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')
+
+    class Meta(meta):
+        fields = ('id', *[f for f in meta.fields if f != 'id'])
+
+    bases = (
+        (BulkPartialUpdateSchemaMixin, serializer_class)
+        if partial
+        else (serializer_class,)
+    )
+
+    attrs = {
+        'id': serializers.IntegerField(required=True),
+        'Meta': Meta,
+        '__module__': serializer_class.__module__,
+    }
+
+    prefix = 'PatchedBulk' if partial else 'Bulk'
+    return type(f'{prefix}{serializer_class.__name__}', bases, attrs)
+
+
 class BulkOperationSerializer(ChangeLogMessageSerializer):
     id = serializers.IntegerField()

+ 15 - 0
netbox/netbox/api/viewsets/mixins.py

@@ -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)