How to Not allow the PUT method at all but allow PATCH in a DRF ViewSet?
Asked Answered
P

7

18

PUT and PATCH are both part of the same mixin (The UpdateModelMixin).

So if I extend it like so:

class UserViewSet(mixins.UpdateModelMixin, GenericViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer

Both PUT and PATCH are allowed. I want to not allow PUT at all for my app (since PATCH already does the work, and I want to limit object creation using just POST). One way is to create a permission:

class NoPut(permissions.BasePermission):
    """
    PUT not allowed.
    """
    message = 'You do not have permission to complete the action you are trying to perform.'

    def has_object_permission(self, request, view, obj):
        if view.action == "update":
            return False
        return True

And to give this permission to all my ViewSets which allow PATCH. Is this the best way to do it? Is there a more preferred way?

Edit: After looking at the answer provided by @wim, will this be a fine solution (everything kept the same except the mapping for put was removed):

from rest_framework.routers import SimpleRouter
class NoPutRouter(SimpleRouter):

    routes = [
        # List route.
        Route(
            url=r'^{prefix}{trailing_slash}$',
            mapping={
                'get': 'list',
                'post': 'create'
            },
            name='{basename}-list',
            initkwargs={'suffix': 'List'}
        ),
        # Dynamically generated list routes.
        # Generated using @list_route decorator
        # on methods of the viewset.
        DynamicListRoute(
            url=r'^{prefix}/{methodname}{trailing_slash}$',
            name='{basename}-{methodnamehyphen}',
            initkwargs={}
        ),
        # Detail route.
        Route(
            url=r'^{prefix}/{lookup}{trailing_slash}$',
            mapping={
                'get': 'retrieve',
                 # put removed
                'patch': 'partial_update',
                'delete': 'destroy'
            },
            name='{basename}-detail',
            initkwargs={'suffix': 'Instance'}
        ),
        # Dynamically generated detail routes.
        # Generated using @detail_route decorator on methods of the viewset.
        DynamicDetailRoute(
            url=r'^{prefix}/{lookup}/{methodname}{trailing_slash}$',
            name='{basename}-{methodnamehyphen}',
            initkwargs={}
        ),
    ]

or would I need to redefine other methods in SimpleRoute (e.g. __init()__, get_routes(), _get_dynamic_routes(), get_method_map() etc.) in order for it to work correctly?

Prognostic answered 20/4, 2017 at 20:43 Comment(0)
V
7

Instead of using mixins.UpdateModelMixin just define your own mixin that would perform patch only:

class UpdateModelMixin(object):
    """
    Update a model instance.
    """
    def partial_update(self, request, *args, **kwargs):
        partial = True
        instance = self.get_object()
        serializer = self.get_serializer(instance, data=request.data, partial=partial)
        serializer.is_valid(raise_exception=True)
        self.perform_update(serializer)

        if getattr(instance, '_prefetched_objects_cache', None):
            # If 'prefetch_related' has been applied to a queryset, we need to
            # forcibly invalidate the prefetch cache on the instance.
            instance._prefetched_objects_cache = {}

        return Response(serializer.data)

    def perform_update(self, serializer):
        serializer.save()
Viafore answered 21/4, 2017 at 5:45 Comment(0)
C
18

If you want to use builtin mixins.UpdateModelMixin, limit to PATCH and disable swagger from showing PUT you can use http_method_names

class UserViewSet(mixins.UpdateModelMixin, GenericViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer
    http_method_names = ["patch"]
Chaperon answered 19/2, 2021 at 8:14 Comment(2)
http_method_names = ["patch"] this worked, thank you!Skiagraph
Note also that it's working with generics.UpdateAPIView and enabling only patch is also adjusted when using drf-spectacular.Deteriorate
V
7

Instead of using mixins.UpdateModelMixin just define your own mixin that would perform patch only:

class UpdateModelMixin(object):
    """
    Update a model instance.
    """
    def partial_update(self, request, *args, **kwargs):
        partial = True
        instance = self.get_object()
        serializer = self.get_serializer(instance, data=request.data, partial=partial)
        serializer.is_valid(raise_exception=True)
        self.perform_update(serializer)

        if getattr(instance, '_prefetched_objects_cache', None):
            # If 'prefetch_related' has been applied to a queryset, we need to
            # forcibly invalidate the prefetch cache on the instance.
            instance._prefetched_objects_cache = {}

        return Response(serializer.data)

    def perform_update(self, serializer):
        serializer.save()
Viafore answered 21/4, 2017 at 5:45 Comment(0)
R
7

A simple and straight forward approach:

class UserViewSet(viewsets.ModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer
    http_method_names = ['get', 'post', 'patch'] # <---------

Like this the PUT method will not be allowed.

Roose answered 27/10, 2021 at 2:3 Comment(1)
This should be the accepted answer. It is so straight forwardNormi
A
2

Solution similar to @EbramShehata's but for drf-spectacular (OpenAPI 3). This will disallow full updates (PUT) and also exclude that from the generated OpenAPI 3 schema.

class SomeViewSet(
    mixins.UpdateModelMixin,
    ...
):
    @extend_schema(exclude=True)
    def update(self, request: Request, *args: Any, **kwargs: Any) -> Response:
        """Disallow full update (PUT) and allow partial update (PATCH)."""
        if kwargs.get("partial", False):  # Use .get() instead of .pop()
            return super().update(request, args, kwargs)

        raise MethodNotAllowed(request.method)
Angelitaangell answered 20/10, 2021 at 14:36 Comment(1)
Note that this solution with http_method_names = ['patch'] will automatically remove the PUT operation from the openapi generated by drf-spectacular without need to implement the update method.Deteriorate
E
1

I think a superior solution would be to use a custom router and disable the route for PUT. Then use your custom router for the viewsets.

class SimpleRouter(BaseRouter):
    routes = [
        # List route.
        Route(
            url=r'^{prefix}{trailing_slash}$',
            mapping={
                'get': 'list',
                'post': 'create'
            },
            name='{basename}-list',
            initkwargs={'suffix': 'List'}
        ),
        # Dynamically generated list routes.
        # Generated using @list_route decorator
        # on methods of the viewset.
        DynamicListRoute(
            url=r'^{prefix}/{methodname}{trailing_slash}$',
            name='{basename}-{methodnamehyphen}',
            initkwargs={}
        ),
        # Detail route.
        Route(
            url=r'^{prefix}/{lookup}{trailing_slash}$',
            mapping={
                'get': 'retrieve',
                'put': 'update',
                'patch': 'partial_update',
                'delete': 'destroy'
            },
            name='{basename}-detail',
            initkwargs={'suffix': 'Instance'}
        ),
        # Dynamically generated detail routes.
        # Generated using @detail_route decorator on methods of the viewset.
        DynamicDetailRoute(
            url=r'^{prefix}/{lookup}/{methodname}{trailing_slash}$',
            name='{basename}-{methodnamehyphen}',
            initkwargs={}
        ),
    ]

^ The router implementation looks something like that. So you just need to inherit the SimpleRouter, or perhaps the DefaultRouter, and defines the routes class attribute how you want it. You can remove the mapping for 'put' in the Route(mapping={...}) completely, or you can define your own action to handle it and return the appropriate 400-something resonse.

Eponymous answered 20/4, 2017 at 21:4 Comment(1)
Thanks. Okay so I edited my answer to include the new custom router. Can you verify that it is an okay custom router, or will I need to redefine other methods in SimpleRouter in order for it to work correctly?Prognostic
H
1

Similar to @linovia's answer but using standard mixin:

from rest_framework.exceptions import MethodNotAllowed

class UpdateModelMixin(mixins.UpdateModelMixin, viewsets.GenericViewSet):
    """
    update:
        Update Model
    """

    def update(self, *args, **kwargs):
        raise MethodNotAllowed("POST", detail="Use PATCH")

    def partial_update(self, request, *args, **kwargs):
        # Override Partial Update Code if desired
        return super().update(*args, **kwargs, partial=True)
Hopple answered 26/3, 2020 at 21:38 Comment(2)
This worked, but how to disable swagger from showing the PUT endpoint?Heaviness
@Heaviness For drf-spectacular you can use @extend_schema_view(update=extend_schema(exclude=True))Angelitaangell
P
1

Here's the solution I'm using:

class SomeViewSet(
    mixins.UpdateModelMixin,
    ...
):
    @swagger_auto_schema(auto_schema=None)
    def update(self, request, *args, **kwargs):
        """Disabled full update functionality"""
        partial = kwargs.get('partial', False)  # This must be .get() not .pop()
        if not partial:
            raise exceptions.MethodNotAllowed(request.method)

        return super(SomeViewSet, self).update(request, *args, **kwargs)

This will also disable it in drf-yasg UIs.

Piracy answered 1/5, 2021 at 17:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.