mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-28 11:36:44 +00:00
Add request body to schema for bulk delete operations, deconflict list (#9420)
* Add request body to schema for bulk delete operations, deconflict list vs single delete operation ids * API version bump * Fix variable name conflict * Switch from post-processing hook to AutoSchema extension * Loosen typing on filter dict, correct expected code in tests * Filter by view class instead of path --------- Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
This commit is contained in:
parent
851ef71864
commit
33cc86a603
@ -373,6 +373,20 @@ class NotFoundView(APIView):
|
||||
return self.not_found(request)
|
||||
|
||||
|
||||
class BulkRequestSerializer(serializers.Serializer):
|
||||
"""Parameters for selecting items for bulk operations."""
|
||||
|
||||
items = serializers.ListField(
|
||||
label='A list of primary key values',
|
||||
child=serializers.IntegerField(),
|
||||
required=False,
|
||||
)
|
||||
|
||||
filters = serializers.DictField(
|
||||
label='A dictionary of filter values', required=False
|
||||
)
|
||||
|
||||
|
||||
class BulkOperationMixin:
|
||||
"""Mixin class for handling bulk data operations.
|
||||
|
||||
@ -532,6 +546,7 @@ class BulkDeleteMixin(BulkOperationMixin):
|
||||
"""
|
||||
return queryset
|
||||
|
||||
@extend_schema(request=BulkRequestSerializer)
|
||||
def delete(self, request, *args, **kwargs):
|
||||
"""Perform a DELETE operation against this list endpoint.
|
||||
|
||||
@ -553,7 +568,7 @@ class BulkDeleteMixin(BulkOperationMixin):
|
||||
for item in queryset:
|
||||
item.delete()
|
||||
|
||||
return Response({'success': f'Deleted {n_deleted} items'}, status=204)
|
||||
return Response({'success': f'Deleted {n_deleted} items'}, status=200)
|
||||
|
||||
|
||||
class ListCreateDestroyAPIView(BulkDeleteMixin, ListCreateAPI):
|
||||
|
@ -1,13 +1,17 @@
|
||||
"""InvenTree API version information."""
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 329
|
||||
INVENTREE_API_VERSION = 330
|
||||
|
||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||
|
||||
|
||||
INVENTREE_API_TEXT = """
|
||||
|
||||
v330 - 2025-03-31 : https://github.com/inventree/InvenTree/pull/9420
|
||||
- Deconflict operation id between single and bulk destroy operations
|
||||
- Add request body definition for bulk destroy operations
|
||||
|
||||
v329 - 2025-03-30 : https://github.com/inventree/InvenTree/pull/9399
|
||||
- Convert url path regex-specified PKs to int
|
||||
|
||||
|
@ -1,10 +1,64 @@
|
||||
"""Schema processing functions for cleaning up generated schema."""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from drf_spectacular.openapi import AutoSchema
|
||||
from drf_spectacular.plumbing import ComponentRegistry
|
||||
from drf_spectacular.utils import _SchemaType
|
||||
|
||||
|
||||
class ExtendedAutoSchema(AutoSchema):
|
||||
"""Extend drf-spectacular to allow customizing the schema to match the actual API behavior."""
|
||||
|
||||
def is_bulk_delete(self) -> bool:
|
||||
"""Check the class of the current view for the BulkDeleteMixin."""
|
||||
return 'BulkDeleteMixin' in [c.__name__ for c in type(self.view).__mro__]
|
||||
|
||||
def get_operation_id(self) -> str:
|
||||
"""Custom path handling overrides, falling back to default behavior."""
|
||||
result_id = super().get_operation_id()
|
||||
|
||||
# rename bulk deletes to deconflict with single delete operation_id
|
||||
if self.method == 'DELETE' and self.is_bulk_delete():
|
||||
action = self.method_mapping[self.method.lower()]
|
||||
result_id = result_id.replace(action, 'bulk_' + action)
|
||||
|
||||
return result_id
|
||||
|
||||
def get_operation(
|
||||
self,
|
||||
path: str,
|
||||
path_regex: str,
|
||||
path_prefix: str,
|
||||
method: str,
|
||||
registry: ComponentRegistry,
|
||||
) -> Optional[_SchemaType]:
|
||||
"""Custom operation handling, falling back to default behavior."""
|
||||
operation = super().get_operation(
|
||||
path, path_regex, path_prefix, method, registry
|
||||
)
|
||||
if operation is None:
|
||||
return None
|
||||
|
||||
# drf-spectacular doesn't support a body on DELETE endpoints because the semantics are not well-defined and
|
||||
# OpenAPI recommends against it. This allows us to generate a schema that follows existing behavior.
|
||||
if self.method == 'DELETE' and self.is_bulk_delete():
|
||||
original_method = self.method
|
||||
self.method = 'PUT'
|
||||
request_body = self._get_request_body()
|
||||
request_body['required'] = True
|
||||
operation['requestBody'] = request_body
|
||||
self.method = original_method
|
||||
|
||||
return operation
|
||||
|
||||
|
||||
def postprocess_required_nullable(result, generator, request, public):
|
||||
"""Un-require nullable fields.
|
||||
|
||||
Read-only values are all marked as required by spectacular, but InvenTree doesn't always include them in the response. This removes them from the required list to allow responses lacking read-only nullable fields to validate against the schema.
|
||||
Read-only values are all marked as required by spectacular, but InvenTree doesn't always include them in the
|
||||
response. This removes them from the required list to allow responses lacking read-only nullable fields to validate
|
||||
against the schema.
|
||||
"""
|
||||
# Process schema section
|
||||
schemas = result.get('components', {}).get('schemas', {})
|
||||
|
@ -536,7 +536,7 @@ REST_FRAMEWORK = {
|
||||
'rest_framework.permissions.DjangoModelPermissions',
|
||||
'InvenTree.permissions.RolePermission',
|
||||
],
|
||||
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
|
||||
'DEFAULT_SCHEMA_CLASS': 'InvenTree.schema.ExtendedAutoSchema',
|
||||
'DEFAULT_METADATA_CLASS': 'InvenTree.metadata.InvenTreeMetadata',
|
||||
'DEFAULT_RENDERER_CLASSES': ['rest_framework.renderers.JSONRenderer'],
|
||||
'TOKEN_MODEL': 'users.models.ApiToken',
|
||||
|
@ -1112,7 +1112,7 @@ class NotificationTest(InvenTreeAPITestCase):
|
||||
|
||||
# Now, let's bulk delete all 'unread' notifications via the API,
|
||||
# but only associated with the logged in user
|
||||
response = self.delete(url, {'filters': {'read': False}}, expected_code=204)
|
||||
response = self.delete(url, {'filters': {'read': False}}, expected_code=200)
|
||||
|
||||
# Only 7 notifications should have been deleted,
|
||||
# as the notifications associated with other users must remain untouched
|
||||
|
@ -756,7 +756,7 @@ class PurchaseOrderLineItemTest(OrderTest):
|
||||
url = reverse('api-po-line-list')
|
||||
|
||||
# Try to delete a set of line items via their IDs
|
||||
self.delete(url, {'items': [1, 2]}, expected_code=204)
|
||||
self.delete(url, {'items': [1, 2]}, expected_code=200)
|
||||
|
||||
# We should have 2 less PurchaseOrderLineItems after deletign them
|
||||
self.assertEqual(models.PurchaseOrderLineItem.objects.count(), n - 2)
|
||||
|
@ -2015,7 +2015,7 @@ class StockTestResultTest(StockAPITestCase):
|
||||
|
||||
# Try again, but with the correct filters this time
|
||||
response = self.delete(
|
||||
url, {'items': tests, 'filters': {'stock_item': 1}}, expected_code=204
|
||||
url, {'items': tests, 'filters': {'stock_item': 1}}, expected_code=200
|
||||
)
|
||||
|
||||
self.assertEqual(StockItemTestResult.objects.count(), n)
|
||||
|
Loading…
x
Reference in New Issue
Block a user