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

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?

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

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

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.

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.

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)

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.

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)

Related

DRF name undefined in custom serializer

When I try to hit my api/atoms/ endpoint in the browser, I am getting a name undefined error in the views.py file, but it has a base name in urls.
Note: this is a non-model serializer and a ViewSet.
error
...views.py", line 74, in list
instance = atom.values(), many=True)
NameError: name 'atoms' is not defined
views.py
class AtomViewSet(viewsets.ViewSet):
serializer_class = AtomSerializer
def list(self, request):
serializer = AtomSerializer(
instance = atoms.values(), many=True) #<-------------
return Response(serializer.data)
urls.py
# for viewsets in views.py
router = DefaultRouter()
router.register(r'snippets', views.SnippetViewSet)
router.register(r'atoms', views.AtomViewSet, base_name='atoms')
urlpatterns = [
path('', views.api_root),
path('', include(router.urls)),
]
serializer.py
class AtomSerializer(serializers.Serializer):
uid = UniqueIdProperty()
created_at = DateTimeProperty()
updated_at = DateTimeProperty()
charge = IntegerProperty()
mass = FloatProperty()
def create(self, validated_data):
return Atom(id=None, **validated_data)
def update(self, instance, validated_data):
for field, value in validated_data.items():
setattr(instance, field, value)
return instance
This is a basic python NameError exception raised when a local or global name is not found.
The variable atoms is not defined in the list() method or globally, that's why the python interpreter raised the exception.
In your code, you'd write atoms.values(), which forces me to think that you are dealing with a QuerySet, which might be an Atom model.
class AtomViewSet(viewsets.ViewSet):
serializer_class = AtomSerializer
def list(self, request):
serializer = AtomSerializer(instance=Atom.objects.all(), many=True)
return Response(serializer.data)
Note: this is a non-model serializer and a ViewSet.
You are doing create and update operations in your AtomSerializer class, and those are directly connected to the model. I don't see any particular reason that pulls you back from using a ModelSerializer here. Apart from that, you are using the routers, which become a good choice when you deal with the CRUD operations, hence I strongly suggest you use the combination of ModelViewset and ModelSerializer in your code.
In your views.py you did not define atom, you need to define it first before using it or else you will get that error.
class AtomViewSet(viewsets.ViewSet):
serializer_class = AtomSerializer
def list(self, request):
# You need to define the atom first before passing it to your AtomSerializer
atoms = [] # or atom = Atom.objects.all()
serializer = AtomSerializer(
data=atoms,
many=True
)
return Response(serializer.data)

'type' object is not iterable with Django Rest Framework and django oauth2-toolkit

Well I am trying to create new access token for the login user on creation with custom authentication class in views.
Serializer.py
class UserCreateSerializer(ModelSerializer):
def create(self, validated_data):
user = User.objects.create_user(validated_data['username'],
validated_data['email'],
validated_data['password'])
return user
class Meta:
model = User
fields = ('username', 'email' ,'password')
views.py
class User_Create_view(CreateAPIView):
serializer_class = UserCreateSerializer
queryset = User.objects.all()
permission_classes = [AllowAny]
authentication_classes = Has_Access_Token
def create(self, request):
serializers =self.serializer_class(data=request.data)
if serializers.is_valid():
# pdb.set_trace()
serializers.save()
# Has_Access_Token.access_token(Has_Access_Token())
return Response(serializers.data)
return Response(status=status.HTTP_202_ACCEPTED))
permission.py
class Has_Access_Token(BaseAuthentication):
def access_token(self):
app = Application.objects.get(name="testing")
tok = generate_token()
pdb.set_trace()
acce_token=AccessToken.objects.get_or_create(
user=User.objects.all().last(),
application=app,
expires=datetime.datetime.now() + datetime.timedelta(days=365),
token=tok)
return acce_token
#method_decorator(access_token)
def authenticate(self):
return request
If I use the Decorator
File "/usr/lib/python2.7/functools.py", line 33, in update_wrapper
setattr(wrapper, attr, getattr(wrapped, attr))
AttributeError: 'tuple' object has no attribute 'module'
If I am Not using the Decorator
File "/home/allwin/Desktop/response/env/local/lib/python2.7/site-packages/rest_framework/views.py", line 262, in get_authenticators
return [auth() for auth in self.authentication_classes]
TypeError: 'type' object is not iterable
The Problem I am facing is that when i use the Has_Access_Token fuction implicitly after serializer.save() Access token is generated in admin with respect to user but that's not effective method, so I need to override the custom authentication_class in views.
Could somebody please suggest some ways to tackle this issue or perhaps let me know the decorator correction with the above code.
Thanks in advance.
While setting REST_FRAMEWORK.DEFAULT_AUTHENTICATION_CLASSES in settings.py file, the customAuthencationClass must like below:
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [ # need to be list not tuple
'CustomAuthentication',
],
}

DRF: Pagination without queryset

I am trying to make use of Django Rest Framework's pagination mechanisms in my case without success.
class TransactionView(viewsets.ViewSet):
serializer_class = TransactionSerializer
def list(self, request):
# fetching data from external API...
serializer = self.serializer_class(data=list_of_json, many=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
else:
return Response(serializer.errors)
class TransactionSerializer(serializers.Serializer):
# Serializer (transaction's) fields ...
def create(self, validated_data):
return APITransaction(**validated_data)
class APITransaction(object):
def __init__(self, arg1, arg2, ...):
self.arg1 = arg1
...
The problem is that registering the pagination_class (like I have done for the rest of my resources which are represented by Models), doesn't work since the data are created/fetched on the fly, thus I don't have a Model/queryset.
Any ideas on how I could use DRF's pagination mechanism?
Here's the class I wound up creating and using locally for this sort of thing. (Thanks to stelios above for the initial clues.) Of course the contents of "data" must be JSONable.
from typing import List, Any
from collections import OrderedDict
from django.core.paginator import Paginator
from django.http.response import JsonResponse
from rest_framework.request import Request
class ListPaginator:
def __init__(self, request: Request):
# Hidden HtmlRequest properties/methods found in Request.
self._url_scheme = request.scheme
self._host = request.get_host()
self._path_info = request.path_info
def paginate_list(self, data: List[Any], page_size: int, page_number: int) -> JsonResponse:
paginator = Paginator(data, page_size)
page = paginator.page(page_number)
previous_url = None
next_url = None
if self._host and self._path_info:
if page.has_previous():
previous_url = '{}://{}{}?limit={}&page={}'.format(self._url_scheme, self._host, self._path_info, page_size, page.previous_page_number())
if page.has_next():
next_url = '{}://{}{}?limit={}&page={}'.format(self._url_scheme, self._host, self._path_info, page_size, page.next_page_number())
response_dict = OrderedDict([
('count', len(data)),
('next', next_url),
('previous', previous_url),
('results', page.object_list)
])
return JsonResponse(response_dict, status=200, safe=False)
You can't reuse existing DRF's pagination because they are supposed to work with queryset.
However, you may roll your own class by inheriting BasePagination though I haven't done myself.

How to dynamically remove fields from serializer output

I'm developing an API with Django Rest framework, and I would like to dynamically remove the fields from a serializer. The problem is that I need to remove them depending on the value of another field. How could I do that?
I have a serializer like:
class DynamicSerliazer(serializers.ModelSerializer):
type = serializers.SerializerMethodField()
url = serializers.SerializerMethodField()
title = serializers.SerializerMethodField()
elements = serializers.SerializerMethodField()
def __init__(self, *args, **kwargs):
super(DynamicSerliazer, self).__init__(*args, **kwargs)
if self.fields and is_mobile_platform(self.context.get('request', None)) and "url" in self.fields:
self.fields.pop("url")
As you can see, I'm already removing the field "url" depending whether the request has been done from a mobile platform. But, I would like to remove the "elements" field depending on the "type" value. How should I do that?
Thanks in advance
You can customize the serialization behavior by overriding the to_representation() method in your serializer.
class DynamicSerliazer(serializers.ModelSerializer):
def to_representation(self, obj):
# get the original representation
ret = super(DynamicSerializer, self).to_representation(obj)
# remove 'url' field if mobile request
if is_mobile_platform(self.context.get('request', None)):
ret.pop('url')
# here write the logic to check whether `elements` field is to be removed
# pop 'elements' from 'ret' if condition is True
# return the modified representation
return ret
You can create multiple serializers and choose the proper one in view
class IndexView(APIView):
def get_serializer_class(self):
if self.request.GET['flag']:
return SerializerA
return SerializerB
use inheritance to make serializers DRY.
My problem was somewhat similar to yours and I solved it with inheritance.
class StaticSerializer(serializers.ModelSerializer):
class Meta:
model = StaticModel
fields = (
'first_name', 'last_name', 'password', 'username',
'email'
)
class DynamicSerializer(StaticSerializer):
class Meta:
model = StaticModel
fields = (
'first_name',
)
Solution (ViewSet mixin)
I have solved this problem by writing my own ViewSet mixin. It provides quite easy and DRY way to override serializers depending on request action.
class ActionBasedSerializerClassMixin(viewsets.ModelViewSet):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def get_serializer_class(self):
attr_name = f'{self.action}_serializer_class'
if hasattr(self, attr_name):
serializer_class = getattr(self, attr_name)
self.serializer_class = serializer_class
return super().get_serializer_class()
Usage
To use this mixin inherit from it at your viewset (It must be before ModelViewSet parent).
The default serializer is always used as fallback
To use different serializer on list action just set attribute list_serializer_class at your viewset:
class MyViewSet(ViewSet):
serializer_class = MySerializer
list_serializer_class = MyListSerializer
With this code you will have MyListSerializer when action is 'list' and MySerializer for all other actions.
The same patterns works for all other action types: list, create, retrieve, update, partial_update, destroy.
You just need to append _serializer_class to get desired attribute name.
How serailizers should look like
class MySerializer(serializers.ModelSerializer):
some_reverse_rel = MyOtherSerializer(many=True, read_only=True)
class Meta:
model = MyModel
fields = ['field1', 'field2', 'foo', 'bar', 'some_reverse_rel']
class MyListSerailizer(MySerializer): # Note that we inherit from previous serializer
some_reverse_rel = None # Getting rid of reverse relationship
class Meta(MySerializer.Meta):
fields = ['foo', 'bar', 'field1']

Require authorization for all but the listing view in a viewset

I have a ViewSet called BuildViewSet, with a bunch of detail views and one list view:
class BuildViewSet(viewsets.ModelViewSet):
queryset = Build.objects.all()
def list(self, request):
# Do some filtering on self.queryset based on user preferences
return super(BuildViewSet, self).list(request)
#detail_route(methods=['post'])
def transition(self, request):
…
# And a bunch of other methods, all prefixed with #detail_route
I set up REST Framework so the default authorization class is rest_framework.permissions.IsAuthenticated:
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.TokenAuthentication',
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
}
However, I want my list() view to be available for everyone, even unauthenticated. I tried changing my list() method like this:
#list_route(permission_classes=(AllowAny,))
def list(self, request):
…
But this seems to have no effect:
AppError: Bad response: 401 UNAUTHORIZED (not 200 OK or 3xx redirect for http://localhost/api/v1/builds/)
'{"detail":"Authentication credentials were not provided."}
Changing the #detail_route to #permission_classes like this gives the same result:
#permission_classes((AllowAny,))
def list(self, request):
…
So it seems that list_route(…) is not the way to go, but in this case, what is?
You need to decorate the list method with the #permission_classes decorator, but the problem is that this decorator is only used for function-based views. Thus, you have two choices:
1) Convert the list view to a function-based view.
2) Authorize all views from the viewset by setting permission_classes = (AllowAny,) at the class level. In order to limit access to the other views, you will have to manually check the permissions using either a decorator or by calling the check_is_authenticated method:
def check_is_authenticated(self, request):
"""
Inspired from rest_framework.views.check_permissions
"""
if not IsAuthenticated.has_permission(request, self):
self.permission_denied(
request, message=getattr(permission, 'message', None)
)
Since all views that require a permission are already decorated with #detail_route, all you have to do is create a new #authenticated_detail_route decorator.
EDIT 3) Another, alternative solution would be to overload the check_permissions method:
def check_permissions(self, request):
if self.is_request_list(request):
return
return super(BuildViewSet, self).check_permissions(request)
The implementation of the is_request_list method is left as an exercise to the reader :-)
(seriously though, I'm not sufficiently familiar with django-rest-framework to offer an implementation. It would probably involve checking the request.method attribute)
EDIT As mentioned by the OP in a comment, in check_permissions the self.action attribute holds the "list" method name.

Resources