Disable a method in a ViewSet, django-rest-framework
Question:
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)
Answers:
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:
Update:
In DRF 3.14.0, using one of the methods not implemented in the mixins gives a 405 - Method Not Allowed
:
Method Not Allowed: /status/
[06/Mar/2023 01:03:01] "POST /status/ HTTP/1.1" 405 41
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/.
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.
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']
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)
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."
}
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
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.'}
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.
You can write a little decorator:
def http_methods_disable(*methods):
def decorator(cls):
cls.http_method_names = [method for method in cls.http_method_names if method not in methods]
return cls
return decorator
It can then be used in different classes:
@http_methods_disable('patch', 'delete')
class SampleViewSet(viewsets.ModelViewSet):
...
@http_methods_disable('patch')
class AnyViewSet(viewsets.ModelViewSet):
...
This is what i prefer.
from rest_framework import viewsets, mixins
class ContentViewSet(mixins.CreateModelMixin, viewsets.ReadOnlyModelViewSet):
queryset = models.Content.objects.all()
serializer_class = serializers.ContentSerializer
ReadOnlyModelViewSet – This will keep only readonly methods. Which are basically "GET" and "OPTIONS" requests.
CreateModelMixin – This will only allow creating of new elements. Which is "POST" request.
All other methods like "PUT", "PATH" and "DELETE" are disabled in above example. You can enable the various methods using mixins based on your requirements.
An alternative approach for Viewsets in django-rest-framework to enable/disable methods. Here is an example api/urls.py:
user_list = UserViewSet.as_view({
'get': 'list'
})
user_detail = UserViewSet.as_view({
'get': 'retrieve'
'put': 'update',
'post': 'create',
'patch': 'partial_update',
'delete': 'destroy'
})
urlpatterns = [
path('users/', user_list, name='user-list'),
path('users/<int:pk>/', user_detail, name='user-detail')
]
View user_list has only one – get – method allowed, whereas user_detail has all methods active.
Tested on Django 4.0
reference: more details here
Just use a GenericViewSet, you have to explicitly define HTTP actions before using them. See this for more info.
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)
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:
Update:
In DRF 3.14.0, using one of the methods not implemented in the mixins gives a 405 - Method Not Allowed
:
Method Not Allowed: /status/
[06/Mar/2023 01:03:01] "POST /status/ HTTP/1.1" 405 41
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/.
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.
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']
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)
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."
}
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
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.'}
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.
You can write a little decorator:
def http_methods_disable(*methods):
def decorator(cls):
cls.http_method_names = [method for method in cls.http_method_names if method not in methods]
return cls
return decorator
It can then be used in different classes:
@http_methods_disable('patch', 'delete')
class SampleViewSet(viewsets.ModelViewSet):
...
@http_methods_disable('patch')
class AnyViewSet(viewsets.ModelViewSet):
...
This is what i prefer.
from rest_framework import viewsets, mixins
class ContentViewSet(mixins.CreateModelMixin, viewsets.ReadOnlyModelViewSet):
queryset = models.Content.objects.all()
serializer_class = serializers.ContentSerializer
ReadOnlyModelViewSet – This will keep only readonly methods. Which are basically "GET" and "OPTIONS" requests.
CreateModelMixin – This will only allow creating of new elements. Which is "POST" request.
All other methods like "PUT", "PATH" and "DELETE" are disabled in above example. You can enable the various methods using mixins based on your requirements.
An alternative approach for Viewsets in django-rest-framework to enable/disable methods. Here is an example api/urls.py:
user_list = UserViewSet.as_view({
'get': 'list'
})
user_detail = UserViewSet.as_view({
'get': 'retrieve'
'put': 'update',
'post': 'create',
'patch': 'partial_update',
'delete': 'destroy'
})
urlpatterns = [
path('users/', user_list, name='user-list'),
path('users/<int:pk>/', user_detail, name='user-detail')
]
View user_list has only one – get – method allowed, whereas user_detail has all methods active.
Tested on Django 4.0
reference: more details here
Just use a GenericViewSet, you have to explicitly define HTTP actions before using them. See this for more info.