SilentDev
SilentDev

Reputation: 22777

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

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?

Upvotes: 18

Views: 11854

Answers (7)

Elias Prado
Elias Prado

Reputation: 1817

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.

Upvotes: 8

johnthagen
johnthagen

Reputation: 9189

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)

Upvotes: 2

Ebram Shehata
Ebram Shehata

Reputation: 621

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.

Upvotes: 1

Mike Br
Mike Br

Reputation: 918

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"]

Upvotes: 19

gtalarico
gtalarico

Reputation: 4699

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)

Upvotes: 1

Linovia
Linovia

Reputation: 20996

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()

Upvotes: 7

wim
wim

Reputation: 363374

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.

Upvotes: 1

Related Questions