Getting has_object_permission() missing 4 required positional arguments: 'self', 'request', 'view', and 'obj' - django-rest-framework

I am trying to create a has_object_permission function which is in a permissions.py file in a custom class:
class IsOwnerOrAdmin(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
print("has object permissions statement")
return obj.owner == request.user
def has_permission(self, request, view):
print("has permissions statement")
return request.user and request.user.is_authenticated
My view set looks like this:
class SchoolViewSet(viewsets.ModelViewSet):
queryset = School.objects.all()
serializer_class = SchoolSerializer
permission_classes = [IsOwnerOrAdmin,
IsOwnerOrAdmin.has_object_permission()]
...
I get the following error when I call the /schools/ endpoint:
TypeError: has_object_permission() missing 4 required positional
arguments: 'self', 'request', 'view', and 'obj'
However, has_permissions works fine and I don't have to explicitly call it in my permission_classes array. It just gets called. How do I pass these arguments? Am I doing this correctly? I tried a few variations of passing self, passing SchoolViewSet, passing obj=queryset etc. Was not sure if that was correct. Could not find exactly what I needed to fix this on the internets.
Update
How do I change the code in order to call has_object_permission just like has_permission gets called automatically?

permission_classes is not defined correctly. It should only include the customised class.
class SchoolViewSet(viewsets.ModelViewSet):
queryset = School.objects.all()
serializer_class = SchoolSerializer
permission_classes = [IsOwnerOrAdmin]

Related

django - pass request.user to ModelForm

i am having a tricky issue. In my views.py i am passing a form in a DetailView. But i am not using a FormMixin. That has the reason that i only want my template to render the form. In the template i use that form to trigger an UpdateView.
class UpdateDetailView(LoginRequiredMixin, DetailView):
model = Balance
def get_context_data(self, **kwargs):
context = super(UpdateDetailView, self).get_context_data(**kwargs)
context['form'] = ManagerForm
return context
def get_queryset(self, *args, **kwargs):
request = self.request
pk = self.kwargs.get('pk')
select = self.kwargs.get('select')
queryset = Balance.objects.filter(pk=pk).filter(owner = request.user).filter(select = select)
return queryset
class BalanceUpdateView(LoginRequiredMixin, UpdateView):
form_class = ManagerForm
model = Balance
def get_success_url(self):
return reverse('url-year', kwargs={'year': self.object.year, 'select': self.object.select})
So far, so good. The issue is that the form in the UpdateDetailView the ChoiceFields are showing select option of other users which one is not supposed to see. Thus i need to override the queryset of the Modelform. Usually i could use the get_form_kwargs method to pass the request.user to the form. But since the UpdateDetailView is not a FormView it doesnt have that (i know, coz i tried desperately). I also tried context['form'] = ManagerForm(initial = {'user': self.request.user}) in the get_context_data method. But i was unable to access user properly with the __init__ method in the forms.py. I got a key error. If i passed it as arg then i get an attribute error. Does anyone have a solution to that problem? Do i need to use a FormMixin?
Thank you for your replies!

get_queryset is executed and not get_object

I am trying to use get_object to retrieve a single object from my database. However, my code always enters into get_queryset and not get_object so I always return a list of objects.
Here is my viewset :
class DiagramView(viewsets.ModelViewSet):
queryset = Diagram.objects.all()
serializer_class = DiagramSerializer
pk_url_kwarg = 'id'
def get_object(self, *args, **kwargs):
return self.queryset.get(id=kwargs.get('id'))
def get_queryset(self):
print('im here')
My route is :
router.register('api/diagramsingle', DiagramView, 'diagramsingle')
And I access this route like this :
api/diagramsingle/?id=1
Thank you for your answer.
As is written in the documentation on routers, for a SimpleRouter, the detail view has as pattern api/diagramsingle/<int:pk>/, so you access a single object with:
api/diagramsingle/1/
where you specify the primary key in the path, not in the querystring.

Override permission and authentication classes in ViewSet list method

I am trying to set specific authentication and permission classes on the ListModelMixin of a viewset. I tried the following but it is not working:
def list(self, request):
self.authentication_classes = (apiauth.SessionAuthentication, )
self.permission_classes = (permissions.IsAuthenticated, )
return super(TestViewSet, self).list(request)
Am i doing something wrong ?
#henriquesalvaro's answer does not work for me.
For some reason action is not part of self in get_authenticators(self)
So if self.action == 'list' does not work for me.
I had to write the following code to get the action.
def get_authenticators(self):
authentication_classes = [TokenAuthentication]
action_map = {key.lower(): value for key,
value in self.action_map.items()}
action_name = action_map.get(self.request.method.lower())
if action_name =="list":
return [YourPermission]
return authentication_classes
When your request reaches the list function, it means it has already passed both the authentication and the permission phases.
If you're using the same classes for permission and authentication for all actions on your ViewSet you should define them on your class declaration:
class MyViewSet(viewsets.ViewSet):
authentication_classes = (apiauth.SessionAuthentication,)
permission_classes = (permissions.IsAuthenticated,)
If you're trying to use different authentication/permission classes for the list action, you can override the get_authenticators and get_permissions methods:
class MyViewSet(viewsets.ViewSet):
...
def get_authenticators(self):
if self.action == 'list':
# Set up here
return YourAuthenticators
return super().get_authenticators()
def get_permissions(self):
if self.action == 'list':
# Set up here
return YourPermissions
return super().get_permissions()

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

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

Resources