mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-22 20:12:00 -06:00
Merge pull request #5163 from netbox-community/3436-api-bulk-delete
#3436: Support for bulk deletion via REST API
This commit is contained in:
commit
961a491ea4
@ -529,3 +529,17 @@ Note that `DELETE` requests do not return any data: If successful, the API will
|
||||
|
||||
!!! note
|
||||
You can run `curl` with the verbose (`-v`) flag to inspect the HTTP response codes.
|
||||
|
||||
### Deleting Multiple Objects
|
||||
|
||||
NetBox supports the simultaneous deletion of multiple objects of the same type by issuing a `DELETE` request to the model's list endpoint with a list of dictionaries specifying the numeric ID of each object to be deleted. For example, to delete sites with IDs 10, 11, and 12, issue the following request:
|
||||
|
||||
```no-highlight
|
||||
curl -s -X DELETE \
|
||||
-H "Authorization: Token $TOKEN" \
|
||||
http://netbox/api/dcim/sites/ \
|
||||
--data '[{"id": 10}, {"id": 11}, {"id": 12}]'
|
||||
```
|
||||
|
||||
!!! note
|
||||
The bulk deletion of objects is an all-or-none operation, meaning that if NetBox fails to delete any of the specified objects (e.g. due a dependency by a related object), the entire operation will be aborted and none of the objects will be deleted.
|
||||
|
@ -4,11 +4,22 @@ from rest_framework import authentication, exceptions
|
||||
from rest_framework.pagination import LimitOffsetPagination
|
||||
from rest_framework.permissions import DjangoObjectPermissions, SAFE_METHODS
|
||||
from rest_framework.renderers import BrowsableAPIRenderer
|
||||
from rest_framework.schemas import coreapi
|
||||
from rest_framework.utils import formatting
|
||||
|
||||
from users.models import Token
|
||||
|
||||
|
||||
def is_custom_action(action):
|
||||
return action not in {
|
||||
'retrieve', 'list', 'create', 'update', 'partial_update', 'destroy', 'bulk_destroy'
|
||||
}
|
||||
|
||||
|
||||
# Monkey-patch DRF to treat bulk_destroy() as a non-custom action (see #3436)
|
||||
coreapi.is_custom_action = is_custom_action
|
||||
|
||||
|
||||
#
|
||||
# Renderers
|
||||
#
|
||||
|
@ -472,6 +472,13 @@ REST_FRAMEWORK = {
|
||||
'DEFAULT_VERSION': REST_FRAMEWORK_VERSION,
|
||||
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.AcceptHeaderVersioning',
|
||||
'PAGE_SIZE': PAGINATE_COUNT,
|
||||
'SCHEMA_COERCE_METHOD_NAMES': {
|
||||
# Default mappings
|
||||
'retrieve': 'read',
|
||||
'destroy': 'delete',
|
||||
# Custom operations
|
||||
'bulk_destroy': 'bulk_delete',
|
||||
},
|
||||
'VIEW_NAME_FUNCTION': 'netbox.api.get_view_name',
|
||||
}
|
||||
|
||||
|
@ -8,13 +8,13 @@ from django.core.exceptions import FieldError, MultipleObjectsReturned, ObjectDo
|
||||
from django.db import transaction
|
||||
from django.db.models import ManyToManyField, ProtectedError
|
||||
from django.urls import reverse
|
||||
from rest_framework import serializers
|
||||
from rest_framework import mixins, serializers, status
|
||||
from rest_framework.exceptions import APIException, ValidationError
|
||||
from rest_framework.permissions import BasePermission
|
||||
from rest_framework.relations import PrimaryKeyRelatedField, RelatedField
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from rest_framework.viewsets import ModelViewSet as _ModelViewSet
|
||||
from rest_framework.viewsets import GenericViewSet
|
||||
|
||||
from .utils import dict_to_filter_params, dynamic_import
|
||||
|
||||
@ -291,11 +291,53 @@ class WritableNestedSerializer(serializers.ModelSerializer):
|
||||
)
|
||||
|
||||
|
||||
class BulkDeleteSerializer(serializers.Serializer):
|
||||
id = serializers.IntegerField()
|
||||
|
||||
|
||||
#
|
||||
# Mixins
|
||||
#
|
||||
|
||||
class BulkDestroyModelMixin:
|
||||
"""
|
||||
Support bulk deletion of objects using the list endpoint for a model. Accepts a DELETE action with a list of one
|
||||
or more JSON objects, each specifying the numeric ID of an object to be deleted. For example:
|
||||
|
||||
DELETE /api/dcim/sites/
|
||||
[
|
||||
{"id": 123},
|
||||
{"id": 456}
|
||||
]
|
||||
"""
|
||||
def bulk_destroy(self, request):
|
||||
serializer = BulkDeleteSerializer(data=request.data, many=True)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
pk_list = [o['id'] for o in serializer.data]
|
||||
qs = self.get_queryset().filter(pk__in=pk_list)
|
||||
|
||||
self.perform_bulk_destroy(qs)
|
||||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
def perform_bulk_destroy(self, objects):
|
||||
with transaction.atomic():
|
||||
for obj in objects:
|
||||
self.perform_destroy(obj)
|
||||
|
||||
|
||||
#
|
||||
# Viewsets
|
||||
#
|
||||
|
||||
class ModelViewSet(_ModelViewSet):
|
||||
class ModelViewSet(mixins.CreateModelMixin,
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.UpdateModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
mixins.ListModelMixin,
|
||||
BulkDestroyModelMixin,
|
||||
GenericViewSet):
|
||||
"""
|
||||
Accept either a single object or a list of objects to create.
|
||||
"""
|
||||
@ -408,6 +450,14 @@ class ModelViewSet(_ModelViewSet):
|
||||
|
||||
class OrderedDefaultRouter(DefaultRouter):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Extend the list view mappings to support the DELETE operation
|
||||
self.routes[0].mapping.update({
|
||||
'delete': 'bulk_destroy',
|
||||
})
|
||||
|
||||
def get_api_root_view(self, api_urls=None):
|
||||
"""
|
||||
Wrap DRF's DefaultRouter to return an alphabetized list of endpoints.
|
||||
|
@ -300,6 +300,29 @@ class APIViewTestCases:
|
||||
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
|
||||
self.assertFalse(self._get_queryset().filter(pk=instance.pk).exists())
|
||||
|
||||
def test_bulk_delete_objects(self):
|
||||
"""
|
||||
DELETE a set of objects in a single request.
|
||||
"""
|
||||
# Add object-level permission
|
||||
obj_perm = ObjectPermission(
|
||||
actions=['delete']
|
||||
)
|
||||
obj_perm.save()
|
||||
obj_perm.users.add(self.user)
|
||||
obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
|
||||
|
||||
# Target the three most recently created objects to avoid triggering recursive deletions
|
||||
# (e.g. with MPTT objects)
|
||||
id_list = self._get_queryset().order_by('-id').values_list('id', flat=True)[:3]
|
||||
self.assertEqual(len(id_list), 3, "Insufficient number of objects to test bulk deletion")
|
||||
data = [{"id": id} for id in id_list]
|
||||
|
||||
initial_count = self._get_queryset().count()
|
||||
response = self.client.delete(self._get_list_url(), data, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
|
||||
self.assertEqual(self._get_queryset().count(), initial_count - 3)
|
||||
|
||||
class APIViewTestCase(
|
||||
GetObjectViewTestCase,
|
||||
ListObjectsViewTestCase,
|
||||
|
Loading…
Reference in New Issue
Block a user