151

ViewSets have automatic methods to list, retrieve, create, update, delete, ...

I would like to disable some of those, and the solution I came up with is probably not a good one, since OPTIONS still states those as allowed.

Any idea on how to do this the right way?

class SampleViewSet(viewsets.ModelViewSet):
    queryset = api_models.Sample.objects.all()
    serializer_class = api_serializers.SampleSerializer

    def list(self, request):
        return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED)
    def create(self, request):
        return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED)
db0
  • 3,279
  • 3
  • 21
  • 25

9 Answers9

291

The definition of ModelViewSet is:

class ModelViewSet(mixins.CreateModelMixin, 
                   mixins.RetrieveModelMixin, 
                   mixins.UpdateModelMixin,
                   mixins.DestroyModelMixin,
                   mixins.ListModelMixin,
                   GenericViewSet)

So rather than extending ModelViewSet, why not just use whatever you need? So for example:

from rest_framework import viewsets, mixins

class SampleViewSet(mixins.RetrieveModelMixin,
                    mixins.UpdateModelMixin,
                    mixins.DestroyModelMixin,
                    viewsets.GenericViewSet):
    ...

With this approach, the router should only generate routes for the included methods.

Reference:

ModelViewSet

SunnySydeUp
  • 6,000
  • 4
  • 26
  • 32
  • @SunnySydeUp Just trying this now and it seems the router does generate the route for a list view, but it 404s because the ViewSet doesn't know how to handle the request. Is this what you expected? – Steve Jalim Jul 31 '14 at 21:47
  • @stevejalim If you don't use the ListModelMixin, it shouldn't create the urlpattern for it. This is why it 404s I believe. – SunnySydeUp Jul 31 '14 at 22:31
  • Looking at it, it actually 500s as a NoReverseMatch, which is less ideal than a 404 – Steve Jalim Jul 31 '14 at 22:51
  • I just tested this and I did get a 404. Are you going to the URL through the browser or are you calling `reverse`? How did you set up your viewset? – SunnySydeUp Jul 31 '14 at 22:56
  • 3
    By only using the mixins you need you can disable GET, POST, PUT, DELETE Methods but I did not able to find out how to disable the PATCH method specially if you are using routers. – Muneeb Ahmad Mar 31 '15 at 14:56
  • 4
    @MuneebAhmad The PATCH method is enabled from the `UpdateModelMixin`. If you want to use update but not the patch, I can currently think of two ways. You can either override the allowed methods in the view and remove "patch" or you can override the `partial_update` method and call `http_method_not_allowed(request, *args, **kwargs)`. I haven't tested this so I'm not sure if it works – SunnySydeUp Mar 31 '15 at 23:39
  • 1
    @JulioMarins I've added a reference. I'm not sure if this is what you wanted though. – SunnySydeUp Mar 04 '16 at 06:24
  • [@Akshar Raaj's solution](https://stackoverflow.com/questions/23639113/disable-a-method-in-a-viewset-django-rest-framework#answer-31450643) looks much better for me – Qback Sep 11 '18 at 15:52
  • This does not seem to work as of DRF 3.9.0. The dispatch tries to call the non-existent method (like `self.create` for a POST request) and then it raises an AttributeError. The appropriate response would be to raise a 405. – Dustin Wyatt Nov 14 '18 at 16:37
  • 1
    If someone wants to make read-only viewset then they can use `class SampleViewSet(viewsets.ReadOnlyModelViewSet)`. – Bikash kharel Apr 03 '19 at 08:58
173

You could keep using viewsets.ModelViewSet and define http_method_names on your ViewSet.

Example

class SampleViewSet(viewsets.ModelViewSet):
    queryset = api_models.Sample.objects.all()
    serializer_class = api_serializers.SampleSerializer
    http_method_names = ['get', 'post', 'head']

Once you add http_method_names, you will not be able to do put and patch anymore.

If you want put but don't want patch, you can keep http_method_names = ['get', 'post', 'head', 'put']

Internally, DRF Views extend from Django CBV. Django CBV has an attribute called http_method_names. So you can use http_method_names with DRF views too.

[Shameless Plug]: If this answer was helpful, you will like my series of posts on DRF at https://www.agiliq.com/blog/2019/04/drf-polls/.

Akshar Raaj
  • 11,107
  • 5
  • 45
  • 41
  • 25
    The problem with this way is no way to disable either list or retrieve. Have to disable both or neither – Fuad Aug 21 '15 at 14:42
  • 2
    This did not work for me, after including get and head I was still able to do a post – RunLoop Dec 10 '15 at 08:25
  • This works for me on django 1.9. Great solution. Is there a risk users can do a GET request another way? – Ycon Nov 10 '16 at 00:43
  • FANTASTIC solution. Works on `python3` and `Django 1.10` just fine. – Urda Dec 26 '16 at 19:50
  • 2
    I prefer this approach because I could not change the inheritance of mixins to include PATCH, but not PUT because they are both an implementation of `mixins.UpdateModelMixin` – ThatsAMorais Jan 31 '18 at 18:42
  • This is my ideal solution because it updates the HTTP methods in the auto-generated API documentation page :) – greenie-beans Nov 23 '20 at 03:45
13

Although it's been a while for this post, I suddenly found out that actually there is a way to disable those functions, you can edit it in the views.py directly.

Source: https://www.django-rest-framework.org/api-guide/viewsets/#viewset-actions

from rest_framework import viewsets, status
from rest_framework.response import Response

class NameThisClassWhateverYouWantViewSet(viewsets.ModelViewSet):

    def create(self, request):
        response = {'message': 'Create function is not offered in this path.'}
        return Response(response, status=status.HTTP_403_FORBIDDEN)

    def update(self, request, pk=None):
        response = {'message': 'Update function is not offered in this path.'}
        return Response(response, status=status.HTTP_403_FORBIDDEN)

    def partial_update(self, request, pk=None):
        response = {'message': 'Update function is not offered in this path.'}
        return Response(response, status=status.HTTP_403_FORBIDDEN)

    def destroy(self, request, pk=None):
        response = {'message': 'Delete function is not offered in this path.'}
        return Response(response, status=status.HTTP_403_FORBIDDEN)
Diyako
  • 568
  • 1
  • 6
  • 22
W Kenny
  • 918
  • 9
  • 22
6

If you are trying to disable the PUT method from a DRF viewset, you can create a custom router:

from rest_framework.routers import DefaultRouter

class NoPutRouter(DefaultRouter):
    """
    Router class that disables the PUT method.
    """
    def get_method_map(self, viewset, method_map):

        bound_methods = super().get_method_map(viewset, method_map)

        if 'put' in bound_methods.keys():
            del bound_methods['put']

        return bound_methods

By disabling the method at the router, your api schema documentation will be correct.

storn
  • 71
  • 1
  • 3
  • As partial patch is not correctly implemented within DRF it would be wise to remove it globally in the way described here – oden Sep 18 '17 at 06:10
3

How to disable "DELETE" method for ViewSet in DRF

class YourViewSet(viewsets.ModelViewSet):
    def _allowed_methods(self):
        return [m for m in super(YourViewSet, self)._allowed_methods() if m not in ['DELETE']]

P.S. This is more reliable than explicitly specifying all the necessary methods, so there is less chance of forgetting some of important methods OPTIONS, HEAD, etc

P.P.S. by default DRF has http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']

pymen
  • 3,591
  • 34
  • 27
3

I liked @pymen answer's idea, but his implementation didn't work. This does:

class SampleViewSet(viewsets.ModelViewSet):
    http_method_names = [m for m in viewsets.ModelViewSet.http_method_names if m not in ['delete']]

This has the advantage of doing literally only exclusion and being simple. It looks sort of hacky though, but might be exactly what you need if it's only for that one ViewSet.

crunch
  • 129
  • 5
2

In Django Rest Framework 3.x.x you can simply enable every each method you want to be enabled for ModelViewSet, by passing a dictionary to as_view method. In this dictionary, the key must contain request type (GET, POST, DELETE, etc) and the value must contain corresponding method name (list, retrieve, update, etc). For example let say you want Sample model to be created or read but you don't want it to be modified. So it means you want list, retrieve and create method to be enable (and you want others to be disabled.)

All you need to do is to add paths to urlpatterns like these:

path('sample/', SampleViewSet.as_view({
    'get': 'list',
    'post': 'create'
})),
path('sample/<pk>/', SampleViewSet.as_view({  # for get sample by id.
    'get': 'retrieve'
}))

As you can see there's no delete and put request in above routing settings, so for example if you send a put request to the url, it response you with 405 Method Not Allowed:

{
    "detail": "Method \"PUT\" not allowed."
}
Hamidreza
  • 999
  • 7
  • 20
1

If you are planning to disable put/post/destroy methods, you can use

viewsets.ReadOnlyModelViewSet https://www.django-rest-framework.org/tutorial/6-viewsets-and-routers/#refactoring-to-use-viewsets

Aleem
  • 403
  • 5
  • 13
1

The most straightforward way to disable a method on a viewset, keep things consistent across your api, and return a useful error message is to simply raise a MethodNotAllowed exception for any methods you do not want to use. For a method like GET that is mapped to retrieve and list with list being disabled, you could customize the error message to indicate that GET only works with a lookup value on the URL.

from rest_framework.exceptions import MethodNotAllowed

class SampleViewSet(viewsets.ModelViewSet):
    queryset = api_models.Sample.objects.all()
    serializer_class = api_serializers.SampleSerializer

def list(self, request):
    raise MethodNotAllowed('GET', detail='Method "GET" not allowed without lookup')
def create(self, request):
    raise MethodNotAllowed(method='POST')

This will return a 405 status code and json data in the format DRF uses:

{'detail': 'Method "POST" not allowed.'}