I have a view with a custom action which should have a custom permission "IsRightUser". However, the has_object_permission of it is never called, even though I try to access the object with self.get_object() in my view.
class MyView(mixins.ListModelMixin, viewsets.GenericViewSet):
serializer_class = MySerializer
lookup_field = 'uuid'
queryset = MyObject.objects.all()
#action(methods=['get'], detail=True)
#permission_classes([IsRightUser])
def groups(self, request, uuid=None):
# prints [<class 'rest_framework.permissions.IsAuthenticated'>]
print(self.permission_classes)
my_object = self.get_object()
groups = Group.objects.filter(my_object=my_object)
serializer = MySerializer(groups, many=True)
return Response(serializer.data)
Here you can see my custom permission which is never called.
class IsRightUser(BasePermission):
def has_object_permission(self, request, view, obj):
# never called
return True
When I use permission_classes = [IsRightUser] in my view (i.e. directly underneath the lookup_field) it works (unfortunately this is not feasible for me).
Any help is very much appreciated.
You should pass permission classes as action argument directly:
#action(methods=['get'], detail=True, permission_classes=[IsRightUser])
def groups(self, request, uuid=None):
# prints [<class 'rest_framework.permissions.IsAuthenticated'>]
print(self.permission_classes)
my_object = self.get_object()
groups = Group.objects.filter(my_object=my_object)
serializer = MySerializer(groups, many=True)
return Response(serializer.data)
The first decorator perfectly works as soon as you define a default DEFAULT_AUTHENTICATION_CLASSES into settings.py under REST_FRAMEWORK for instance.
Related
Hello I have a django rest framework view set. For the create view I want to create a custom view that will create a new record based on two different parameters that are passed on through the url which are namespace and path. I looked at the documentation but i couldnt find how it should look like. I am noit sure what I need to do in order to create a record based on both url parameters.
I basically tried setting the create to a CreateAPIView but it did not work
class PreferenceViewSet(viewsets.ViewSet):
queryset = Preference.objects.all()
serializer_class = PreferenceSerializer
def get_permissions(self):
if self.action == 'create' or self.action == 'destroy':
permission_classes = [IsAuthenticated]
else:
permission_classes = [IsAdminUser]
return [permission() for permission in permission_classes]
def list(self, request):
queryset = Preference.objects.all()
serializer = PreferenceSerializer(queryset, many=True)
return Response(serializer.data)
def create(self, request):
queryset = Preference.objects.all()
serializer = PreferenceSerializer(queryset, many=True)
return Response(serializer.data)
I want to setup the create to create a preference with the two parameters that are passe in the url
path('preferences/<str:namespace>/<str:path>', preference_path, name='preference-path'),
I wanted it to create a new object with the namespace and path
You need to do this in 2 steps:
Add the url arguments to serializer context from viewset
Override create method on the serializer and use data passed on the context to create the record
So, at first override get_serializer_context method to add the arguments to context:
class PreferenceViewSet(viewsets.ViewSet):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._namespace = None
self._path = None
...
...
def get_serializer_context(self):
context = super().get_serializer_context()
context.update(namespace=self._namespace, path=self._path)
return context
def create(self, request):
self._namespace = self.kwargs['namespace']
self._path = self.kwargs['path']
queryset = Preference.objects.all()
serializer = PreferenceSerializer(queryset, many=True)
return Response(serializer.data)
Now, you can access the parameters inside the overriden create method of the serializer and create the record as you want e.g.:
class PreferenceSerializer(serializers.HyperlinkedModelSerializer):
...
...
def create(self, validated_data):
namespace = self.context['namespace']
path = self.context['path']
# Create object here based on the params
I have been trying to use django-filters but the objects are not getting filtered. Also, the permission is not working for the partial_update views
I have a Viewset which has the basic actions like - list(), retrieve(), destroy(), partial_update() and few other actions, and trying to apply filter for the same.
After some research I found that since I am creating the queryset via filters I will have to override the get_queryset() method. However, that also doesn't seem to be working. Does the filter works only with ModelViewSet or ListApiView?
ViewSet -
class PostViewSet(viewsets.ViewSet):
"""
The Endpoint to list, retrieve, create and delete Posts.
"""
filter_backends = (DjangoFilterBackend, )
# filterset_class = PostFilter
filter_fields = ('pet_age', 'pet_gender', 'breed')
def get_permissions(self):
if self.action == 'partial_update' or self.action == 'update':
permission_classes = [IsPostAuthor, ]
elif self.action == 'create' or self.action == 'destroy':
permission_classes = [IsAuthenticated, ]
else:
permission_classes = [AllowAny, ]
return[permission() for permission in permission_classes]
def get_queryset(self):
return Post.objects.active() # This is implemented via custom Manager
def list(self, request, *args, **kwargs):
"""
Method for Post listing. It can be accessed by anyone.
"""
serializer = PostListSerializer(self.get_queryset(), many=True, context={"request": request})
return Response(serializer.data)
# REST CODE TRUNCATED
Permission -
class IsPostAuthor(permissions.BasePermission):
"""
Object-level permission to only allow owners of an object to edit it.
"""
def has_object_permission(self, request, view, obj):
if request.user.is_authenticated:
if view.action in ['partial_update', 'update']:
return obj.user.id == request.user.id
return False
return False
PostFilter -
class PostFilter(filters.FilterSet):
class Meta:
model = Post
fields = ('pet_age', 'pet_gender', 'breed', )
Manager -
class PostManager(models.Manager):
def active(self):
return self.filter(post_status='Active')
Any help will be highly appreciated.
Okay, So finally found the solution from DRF Docs. The issue was that in case of normal ViewSet you have to override the method filter_queryset() and return the appropriate queryset accordingly. Then use the queryset under filter_queryset as mentioned by Aman -
serializer = PostListSerializer(self.filter_queryset(self.get_queryset()), many=True, context={"request": request})
Below is the code for reference for those who are still facing issues -
filter_queryset -
def filter_queryset(self, queryset):
filter_backends = (DjangoFilterBackend, )
# Other condition for different filter backend goes here
for backend in list(filter_backends):
queryset = backend().filter_queryset(self.request, queryset, view=self)
return queryset
You have overwrite the list method, so it's not working to work call the filter_queryet method.
def list(self, request, *args, **kwargs):
"""
Method for Post listing. It can be accessed by anyone.
"""
serializer = PostListSerializer(self.filter_queryset(self.get_queryset()), many=True, context= .
{"request": request})
return Response(serializer.data)
I'm currently converting all my views to generics, as I like how cleaner the code gets. I am trying to make my User detail view, like so:
# User.views
from Common import view_mixins, view_filters, view_permissions
class UserDetail(view_mixins.IntOrStrLookupMixin, generics.RetrieveUpdateDestroyAPIView):
queryset = Profile.objects.all()
lookup_fields = ('user__pk', 'user__username')
lookup_url_kwarg = 'userid'
filter_backends = (view_filters.ResourceVisibilityFilter, )
permission_classes = (view_permissions.IsOwnerOrReadOnly, )
serializer_class = ProfileSerializer
def update(self, request, *args, **kwargs):
user = self.get_object()
return Response('whatever')
# Common.view_permissions
SAFE_METHODS = ('GET', 'HEAD', 'OPTIONS')
class IsOwnerOrReadOnly(permissions.BasePermission):
'''
Owner of object can GET, PUT, DELETE. Everyone else can GET.
'''
def has_permission(self, request, view):
return True
def has_object_permission(self, request, view, obj):
print('did you call me?')
return (
request.method in SAFE_METHODS
or
obj.user == request.user
)
# Common.view_mixins
class IntOrStrLookupMixin(object):
"""
Apply to views that can be looked up by slug or pk
"""
def get_object(self):
queryset = self.get_queryset()
queryset = self.filter_queryset(queryset)
filter = {}
for field in self.kwargs:
argument = self.kwargs[field]
if is_int(argument):
filter[self.lookup_fields[0]] = argument
else:
filter[self.lookup_fields[1]] = argument
return get_object_or_404(queryset, **filter)
So my issue is, the permission is never called. I can get the user just fine, but anyone can do PUT or DELETE on which I am trying to prevent.
In my mixin, I had to call the check_object_permissions method.
class IntOrStrLookupMixin(object):
"""
Apply to views that can be looked up by slug or pk
"""
def get_object(self):
queryset = self.get_queryset()
queryset = self.filter_queryset(queryset)
filter = {}
for field in self.kwargs:
argument = self.kwargs[field]
if is_int(argument):
filter[self.lookup_fields[0]] = argument
else:
filter[self.lookup_fields[1]] = argument
obj = get_object_or_404(queryset, **filter)
self.check_object_permissions(self.request, obj)
return obj
I think I encountered this problem. The object level permissions are only called when you use get_object() method to get the object being operated on. Update your update() with this line and you should see the permissions being called. And update your custom get_object() to either call the super method or call the permissions directly
class UserDetail(view_mixins.IntOrStrLookupMixin, generics.RetrieveUpdateDestroyAPIView):
... blah blah blah
def update(self, request):
user = self.get_object()
return Response('whatever')
class IntOrStrLookupMixin(object):
def get_object(self):
... retrieve the object ...
self.check_object_permissions(self.request, obj)
return obj
Edit: I filed a bug report about this with the DRF team and they updated the docs. On the permission page in the docs it says "Object level permissions are run by REST framework's generic views when .get_object() is called". I agree that this is a rather subtle thing and is easy to miss.
Edit #2: Looks like the problem is not only in the update() not calling get_object(), but also in the IntOrStrLookupMixin mixin redefining get_object() method. Updated the code to reflect
I've a model, and one of it's field refers to an overrided User instance (changed in Django settings).
When I'm performing a POST from my client, the route ends up here at the create method:
class CatView(ModelViewSet):
authentication_classes = (authentication.TokenAuthentication,)
permission_classes = (permissions.IsAuthenticated,)
serializer_class = CatListSerializer
def get_queryset(self):
return Cat.objects.filter(owner=self.request.user).order_by('id')
'''
def list(self, request, format=None):
serializer = CatGetSerializer(Cat.objects.filter(owner=request.user), context={'request': request}, many=True)
return Response(serializer.data)
'''
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
def create(self, request, *args, **kwargs):
serializer = CatPutSerializer(data=request.data)
if serializer.is_valid():
serializer.create(serializer.data)
return Response(serializer.data, status=HTTP_201_CREATED)
return Response(serializer.errors, status=HTTP_400_BAD_REQUEST)
When using a PUT to do a partial update on my model, it works fine. But creating one just doesn't work. I manually inject the user instance into the serializer and asks it to create the object. Then... nothing. No exception raises, it returns the proper data, but the object is not in my database, not being saved.
What's the issue here?
EDIT:
When I'm adding the owner field to the CatPutSerializer, it opens security issues since I don't know how to prevent this to be changed as I don't want the client to send me which user to assign. And when I'm duplicating the serializer to be used on POST only requests, it says it misses the owner field...
Here's the CatPutSerializer:
class CatPutSerializer(serializers.ModelSerializer):
class Meta:
model = Cat
fields = ('name', 'weight', 'sterilized', 'image', 'tag', 'dob', 'race', 'gender')
UPDATE:
As suggested, I'm now doing as follows :
def create(self, request, *args, **kwargs):
pdb.set_trace()
serializer = CatPutSerializer(data=request.data)
if serializer.is_valid():
serializer.save(owner=self.request.user)
return Response(serializer.data, status=HTTP_201_CREATED)
return Response(serializer.errors, status=HTTP_400_BAD_REQUEST)
Though removed the perform_create overriding.
SOLUTION:
After further investigation, it doesn't seem related to drf but to Django / PostgreSQL itself, so I checked to Django model save method, and it seems that my custom image processing prevented from new objects to be created... Changed it and now works.
You seem to be overriding both create and perform_create. If you look at the code for CreateModelMixin which the ModelViewSet inherits from you will notice that create calls perform_create, which calls serializer.save(). You don't call perform_create in your create method; you seem to be calling serializer.create(...). If you are overriding create, simply do this:
def create(self, request, *args, **kwargs):
serializer = CatPutSerializer(data=request.data)
if serializer.is_valid():
serializer.save(owner=self.request.user)
return Response(serializer.data, status=HTTP_201_CREATED)
return Response(serializer.errors, status=HTTP_400_BAD_REQUEST)
I have a viewset as follows:
class CardViewSet(viewsets.ReadOnlyModelViewSet):
"""
Standard Viewset for listing cards
"""
pagination_class = StandardCellSetPagination
permission_classes = [AllowAny, IsAuthenticated]
def list(self, request):
queryset = Card.objects.exclude(reply_to__isnull=False).order_by('-created')
cards = self.paginate_queryset(queryset)
serializer = CardCellSerializer(cards, many=True)
return self.get_paginated_response(serializer.data)
def retrieve(self, request, pk=None):
queryset = Card.objects.all()
card = get_object_or_404(queryset, pk=pk)
serializer = CardSerializer(card)
return Response(serializer.data)
My serializer for the CardSerializer is:
class CardSerializer(serializers.ModelSerializer):
class Meta:
model = Card
How do I either
Change the serializer for the retrieve method if the viewset has the permission IsAuthenticated?
OR
Add a field to the CardSerializer if viewset has IsAuthenticated?
Such that I can return True / False if a user has favorited the card via a SerializerMethodField
You can do this:
def retrieve(self, request, pk=None):
queryset = Card.objects.all()
card = get_object_or_404(queryset, pk=pk)
# Same for list method
if request.user and request.user.is_authenticated:
serializer = AuthenticatedCardSerializer(card)
else:
serializer = CardSerializer(card)
return Response(serializer.data)
AuthenticatedCardSerializer could then extend CardSerializer to include any fields visible to authenticated users.
Also if you decide to use same serialization behavior for list and retrieve, you could override get_serializer_class in your viewset instead:
def get_serializer_class(self):
if self.request.user and self.request.user.is_authenticated:
return AuthenticatedCardSerializer
else:
return CardSerializer
and leave everything else to the default list/retrieve implementations.
As an alternative, you could add the field in serializer's __init__. You can get the request from the context kwarg, do the same check and add any fields you need. I think though that it is needlessly more complicated than just having two serializers.