Django Rest Framework: POST to Viewset with URL parameter

Viewed 3149

Typically, in a DRF Viewset you might do something like this:

class FooViewSet(viewsets.ViewSet):
    """
    Foo-related viewsets.
    """
    permission_classes = [IsAuthenticated,]

    def list(self, request):
        """
        A list of foo objects.
        """
        context = {'request': self.request}
        queryset = Foo.objects.all()
        serializer = FooSerializer(queryset, many=True, context=context)
        return Response(serializer.data)

    def retrieve(self, request, pk=None):
        """
        Get one publicly available Foo item.
        """
        context = {'request': self.request}
        queryset = Foo.objects.all()
        store_object = get_object_or_404(queryset, pk=pk)
        serializer = FooSerializer(store_object, context=context)
        return Response(serializer.data)

This works fine, and respectively correlates to:

GET /foo and GET /foo/<pk>. However, the last endpoint I need is POST /foo/<pk>. The problem here is that providing a create method to the views typically will be routed to POST /foo. Is there anything neat and elegant I can do from the ViewSet itself? Or is the only option basically to route POST /foo/<pk> to a specific one-off view?

1 Answers

So, I would say in your urls.py for the REST endpoints you need:

urlpatterns = [
    path(
        '/foo',
        viewsets.FooViewSet.as_view({'post': 'create'}),
        name='Create Foo',
    ),
    path(
        '/foo',
        viewsets.FooViewSet.as_view({'get': 'list'}),
        name='List Foo',
    )
    path(
        '/foo/<pk>',
        viewsets.FooViewSet.as_view({'get': 'retrieve'}),
        name='Retrieve Foo',
    )
]

It is convention to only patch (partial update) or put (update) to a /foo/<pk> endpoint.

Therefore, post to the base /foo endpoint, with a JSON deserialized representation of the object you want to create (minus the pk, let the DB create the pk on the fly).

If you want to update foo, then simply add:

urlpatterns = [
    ...
    path(
        '/foo/<pk>',
        viewsets.FooViewSet.as_view({'put': 'update'}),
        name='Update Foo',
    ),
    path(
        '/foo/<pk>',
        viewsets.FooViewSet.as_view({'patch': 'partial_update'}),
        name='Partially Update Foo',
    )
]

This would correspond to your FooViewSet as the following, as the most basic example as:

class FooViewSet(viewsets.ViewSet):

    def list(self, request):
        pass

    def create(self, request):
        pass

    def retrieve(self, request, pk=None):
        pass

    def update(self, request, pk=None):
        pass

    def partial_update(self, request, pk=None):
        pass

    def destroy(self, request, pk=None):
        pass

Replacing each of the needed endpoints with the code you desire. Leaving in pass will disallow the method as well, which is handy if, for example, you wish to disable the destroy action on your API.

Addendum: It's worth adding you can also do the following:

urlpatterns = [
    path(
        '/foo',
        viewsets.FooViewSet.as_view({'post': 'custom_post_action'}),
        name='Create Foo',
    ),

With the corresponding in your FooViewSet():

class FooViewSet(viewsets.ViewSet):
    ...

    @action(methods=['post'], detail=False, permission_classes=[SomePermissionClass], url_path='?', url_name='?')
    def custom_post_action(self, request):
        pass

If you wish, put I feel it may be bad practise and not truly representative of the "RESTful" principles, but who gives a CRUD if it's what you need?

Related