Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to do bulk instance deletion in Django Rest Framework?

In DRF's DefaultRouter url router, it requires a {lookup} parameter to route DELETE requests to the destroy method of a ModelViewSet (so, you'd make your request to delete an object instance to the endpoint {prefix}/{lookup}/).

This is fine for deleting a single instance, but I'd like to extend that functionality to deleting multiple instances on a single request. Let's say the lookup parameter is called uuid and the model is called Product. Here's an extended version of destroy:

 def destroy(self, request, uuid=None):
    """
    Overridden method allows either url parameter of single UUID 
    (to delete a single instance), or multiple query parameters `uuids`
    to delete multiple instances.
    """
    if not uuid:
        uuids = request.query_params.get('uuids', None)
        if not uuids:
            return Response(status=status.HTTP_404_NOT_FOUND)
        if len(uuids) != Product.objects.filter(uuid__in=uuids).count():
            return Response(status=status.HTTP_404_NOT_FOUND)
        Product.objects.filter(uuid__in=uuids).delete()
    else:
        instance = self.get_object(uuid)
        if not instance:
            return Response(status=status.HTTP_404_NOT_FOUND)
        instance.delete()
     return Response(status=status.HTTP_204_NO_CONTENT)

So this version takes a DELETE request and multiple uuids[] query parameters in the url. Now I just need to route it in urls.py:

from rest_framework.routers import DefaultRouter, Route

class BulkDeleteRouter(DefaultRouter):
    """
    a custom URL router for the Product API that correctly routes
    DELETE requests with multiple query parameters.
    """
    def __init__(self, *args, **kwargs):
        super(BulkDeleteRouter, self).__init__(*args, **kwargs)
        self.routes += [
            Route(
                url=r'^{prefix}{trailing_slash}$',
                mapping={'delete': 'destroy'},
                name='{basename}-delete',
                initkwargs={'suffix': 'Delete'}
            ),
        ]

bulk_delete_router = BulkDeleteRouter()
bulk_delete_router.register(r'product', ProductViewSet, base_name='product')

This, unfortunately, has killed my url router. It won't resolve GET to the appropriate methods in the viewset, and I don't understand why - isn't my BulkDeleteRouter supposed to extend this functionality from the DefaultRouter? What did I do wrong?

like image 544
Escher Avatar asked Jul 31 '17 13:07

Escher


2 Answers

Adding an additional 'delete': 'destroy' to the 'List route' route will perfectly do the job.

class CustomRouter(DefaultRouter):
    """
    a custom URL router for the Product API that correctly routes
    DELETE requests with multiple query parameters.
    """
    routes = [
        # List route.
        Route(
            url=r'^{prefix}{trailing_slash}$',
            mapping={
                'get': 'list',
                'post': 'create',
                'delete': 'destroy', # The magic
            },
            name='{basename}-list',
            detail=False,
            initkwargs={'suffix': 'List'}
        ),
        # Dynamically generated list routes. Generated using
        # @action(detail=False) decorator on methods of the viewset.
        DynamicRoute(
            url=r'^{prefix}/{url_path}{trailing_slash}$',
            name='{basename}-{url_name}',
            detail=False,
            initkwargs={}
        ),
        # Detail route.
        Route(
            url=r'^{prefix}/{lookup}{trailing_slash}$',
            mapping={
                'get': 'retrieve',
                'put': 'update',
                'patch': 'partial_update',
                'delete': 'destroy'
            },
            name='{basename}-detail',
            detail=True,
            initkwargs={'suffix': 'Instance'}
        ),
        # Dynamically generated detail routes. Generated using
        # @action(detail=True) decorator on methods of the viewset.
        DynamicRoute(
            url=r'^{prefix}/{lookup}/{url_path}{trailing_slash}$',
            name='{basename}-{url_name}',
            detail=True,
            initkwargs={}
        ),
    ]

Then use the router like this:

custom_router = CustomRouter()
custom_router.register(r'your-endpoint', YourViewSet)

urlpatterns = [
    url(r'^', include(custom_router.urls)),
]

The viewset:

from rest_framework import viewsets, status
from rest_framework.response import Response
from django.db.models import QuerySet
class MachineSegmentAnnotationViewSet(viewsets.ModelViewSet):
    def destroy(self, request, *args, **kwargs):
        qs: QuerySet = self.get_queryset(*args, **kwargs)
        qs.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)

Hope this helps.

like image 111
Tobias Ernst Avatar answered Oct 16 '22 20:10

Tobias Ernst


Forgot to add the router urls to the urlpatterns. I must be blind.

urlpatterns += [
    url(r'^API/', include(bulk_delete_router.urls, namespace='api')),
]
like image 25
Escher Avatar answered Oct 16 '22 21:10

Escher