I'm trying to filter lists according to:
the user can work with all of their lists
the user can use safe methods on public lists
I have this code:
In views.py:
class LinkListViewSet(viewsets.ModelViewSet,
generics.ListAPIView,
generics.RetrieveAPIView):
queryset = LinkList.objects.all()
serializer_class = LinkListSerializer
permission_classes = [IsOwnerOrPublic]
In permissions.py:
class IsOwnerOrPublic(BasePermission):
def has_permission(self, request, view):
return request.user and request.user.is_authenticated
def has_object_permission(self, request, view, obj):
return obj.owner == request.user or (
obj.public and (request.method in SAFE_METHODS))
The problem is, I believe the view just skips checking the permission classes and returns all lists, and I am not sure why, or how to fix it.
It will only check the has_object_permission for requests that work with an object, so for example the RetrieveAPIView, not the ListAPIView.
You should filter for the latter, so we can make a custom IsOwnerOrPublicFilterBackend filter backend:
from django.db.models import Q
from rest_framework import filters
class IsOwnerOrPublicFilterBackend(filters.BaseFilterBackend):
def filter_queryset(self, request, queryset, view):
return queryset.filter(Q(owner=request.user) | Q(public=True))
and then use that filter as filter_backend in the ModelViewSet:
class LinkListViewSet(viewsets.ModelViewSet):
queryset = LinkList.objects.all()
serializer_class = LinkListSerializer
filter_backends = [IsOwnerOrPublicFilterBackend]
permission_classes = [IsOwnerOrPublic]
Related
I am using Django APIView to include all my CRUD operation in a single api endpoint. But later on I had to use filtering logic based on the query parameters that have been passed. Hence I found it difficult to include it in a get api of APIView and made a separate api using generic view, ListAPiview.
Here is the view:
class LeadsView(APIView):
permission_classes = [IsAuthenticated]
def get(self, request, pk=None, *args, **kwargs):
id = pk
if id is not None:
abc = Lead.objects.get(id=id)
serializer = LeadSerializer(abc)
return serializer.data
def post(self,request,*args,**kwargs):
abc = LeadSerializer(data=request.data,many=True)
if abc.is_valid():
abc.save()
return Response(abc.data, status=status.HTTP_201_CREATED)
return Response(abc._errors, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request,pk, *args, **kwargs):
Now, I have to use filter class and also some custom filtering logic, I need to use get_queryset. Hence I have to create another api just for get method which I dont want.
class LeadAPIView(ListAPIView):
authentication_classes = (TokenAuthentication,)
permission_classes = (IsAuthenticated,)
queryset = Lead.objects.all().order_by('-date_created')
serializer_class = LeadSerializer
filter_backends = [django_filters.rest_framework.DjangoFilterBackend]
pagination_class = CustomPagination
# filterset_fields = [ 'email','first_name','last_name','phone']
filterset_class = LeadsFilter
def get_queryset(self):
source = self.request.GET.get("source", None) #
lead_status = self.request.GET.get("lead_status", None)
if source is not None:
source_values = source.split(",")
if lead_status is not None:
lead_status_values= lead_status.split(",")
return Lead.objects.filter(source__in=source_values,lead_status__in=lead_status_values)
else:
return Lead.objects.filter(source__in=source_values)
elif lead_status is not None:
lead_status_values = lead_status.split(",")
if source is not None:
source_values = source.split(",")
return Lead.objects.filter(lead_status__in=lead_status_values,source__in=source_values)
else:
return Lead.objects.filter(lead_status__in=lead_status_values)
return Lead.objects.all()
My question is, can I use get_queryset in the APIView instead of making another api?? Also, if I can use it, I assume I cant import filterset_class = LeadsFilter and also pagination? What will be the best approach??
My urls:
path('leads', LeadAPIView.as_view(), name='leads'),
path('lead', LeadsView.as_view(), name='leads-create'),
path('lead/<int:pk>', LeadsView.as_view()),
APIView stands for MVT framework. There are 2 types of cases.
If you want to return response to your django templates, you use views.
In cases of returning json, xml (in short response) objects, you use viewset terminology. Viewsets supports filter-class, pagination-class, serialization, queryset, (custom mixins and many more).
p.s. in viewset if you wanted to overwrite default queryset, you define get_queryset method. Views don't support this. Also please check for #action decorators in django.
I am trying to set up an API endpoint that returns a singular object.
Right now I have:
class ShoppingCartViewSet(viewsets.GenericViewSet, mixins.ListModelMixin):
"""
API endpoint that allows users to be viewed or edited.
"""
serializer_class = ShoppingCartSerializer
authentication_classes = (TokenAuthentication,)
permission_classes = (IsAuthenticated,)
def get_paginated_response(self, data):
return Response(data)
def get_queryset(self):
return ShoppingCart.objects.filter(user=self.request.user)
Which uses the ListModelMixin and a filter to return one item, becasue each user has 1 shopping cart.
The issue is the filter function returns a queryset, but I only want a single item.
I attempted to use the RetrieveModelMixin but that doesn't run on the endpoint that I want. Retrieve runs on .../api/shopping-cart/id but I want to retrieve on .../api/shopping-cart because the filtering is done via the person who is logged in.
Any solutions?
I'm not sure if you have tried this but it should be worthwhile (although I'm not sure if it'll work)
def get_queryset(self):
return ShoppingCart.objects.filter(user=self.request.user)[:1]
Why not use simple api view instead of viewset class?
from django.http import Http404
from rest_framework import status, views
class ShoppingCartAPIView(views.APIView):
authentication_classes = (TokenAuthentication,)
permission_classes = (IsAuthenticated,)
def get_object(self):
# use .get if you are really sure it can only be one shopping cart per user
try:
return ShoppingCart.objects.get(user=self.request.user)
except ShoppingCart.DoesNotExist:
raise Http404()
def get(self, request):
obj = self.get_object()
data = ShoppingCartSerializer().to_representation(obj)
return Response(data, status=status.HTTP_200_OK)
You can even create logic for other http methods like post, delete.
Then add the url pattern api/shopping-cart as normal url (in urls.py), not router ones.
The way I ended up solving it was still using the ListModelMixin, as I needed to benefits of the viewset.
I overwrote the list() method with:
class ShoppingCartViewSet(viewsets.GenericViewSet, mixins.ListModelMixin):
"""
API endpoint that allows users to be viewed or edited.
"""
serializer_class = ShoppingCartSerializer
# authentication_classes = (TokenAuthentication,)
# permission_classes = (IsAuthenticated,)
def get_paginated_response(self, data):
return Response(data)
def list(self, request, *args, **kwargs):
instance = ShoppingCart.objects.get(user=self.request.user)
serializer = self.get_serializer(instance)
return Response(serializer.data)
which returns me a singular item on the root url .../api/shopping-cart without having to pass parameters because it filters based on user.
I am trying to make sure the user has permission to view the object they are calling. Here is my permissions class:
from rest_framework import permissions
class IsOwner(permissions.BasePermission):
"""
Custom permission to only allow owners of an object to do actions.
"""
message = 'You must be the owner of this object.'
def has_object_permission(self, request, view, obj):
print("CHECK THAT I GOT HERE")
return obj.user == request.user
And here is my ViewSet:
class TopLevelJobViewSet(ModelViewSet):
permission_classes = (IsOwner,)
serializer_class = TopLevelJobSerializer
queryset = TopLevelJob.objects.all()
filter_backends = [DjangoFilterBackend, RelatedOrderingFilter]
filter_class = TopLevelJobFilter
ordering_fields = '__all__'
Thehas_object_permissions is not being called, anyone visiting the endpoint is able to access all the objects.
Why is this? How do I get has_object_permissions to get called?
This post: Django rest framework ignores has_object_permission talks about it being an issue with not having GenericAPIView. But ModelViewSet has GenericViewSet which has generics.GenericAPIView. Is something overriding this somewhere?
EDIT: My issue was that I was calling list instead of get. How can I only returns objects in list that belong to a user?
This link: https://www.django-rest-framework.org/api-guide/filtering/#filtering-against-the-current-user shows I could implement something like this:
def get_queryset(self):
username = self.kwargs['username']
return Purchase.objects.filter(purchaser__username=username)
This seems to violate DRY if I have to add this to every viewset. Is there a way to turn this into a permissions class that I could always call?
You can implement custom generic filtering [drf-doc]. For example:
class IsOwnerFilter(filters.BaseFilterBackend):
def filter_queryset(self, request, queryset, view):
return queryset.objects.filter(user=request.user)
Then you can add this to your ModelViewSet:
class TopLevelJobViewSet(ModelViewSet):
permission_classes = (IsOwner,)
serializer_class = TopLevelJobSerializer
queryset = TopLevelJob.objects.all()
filter_backends = [IsOwnerFilter, DjangoFilterBackend, RelatedOrderingFilter]
filter_class = TopLevelJobFilter
ordering_fields = '__all__'
I have a basic Viewset:
class UsersViewSet(viewsets.ModelViewSet):
permission_classes = (OnlyStaff,)
queryset = User.objects.all()
serializer_class = UserSerializer
It is bind to the /api/users/ endpoint. I want to create a user profile page, so I need only a particular user, so I can retrieve it from /api/users/<id>/, but the problem is that I want /api/users/<id>/ to be allowed to anyone, but /api/users/ to keep its permission OnlyStaff, so no one can have access to the full list of users.
Note: Perhaps it's not such a good implementation, since anyone could brute force the data incremeting the id, but I'm willing to change it from <id> to <slug>.
How can I delete the permission from detail route?
Thanks in advance.
Override the get_permissions() method as below
from rest_framework.permissions import AllowAny
class UsersViewSet(viewsets.ModelViewSet):
permission_classes = (OnlyStaff,)
queryset = User.objects.all()
serializer_class = UserSerializer
def get_permissions(self):
if self.action == 'retrieve':
return [AllowAny(), ]
return super(UsersViewSet, self).get_permissions()
It would help if you posted the permission class.
But going off what you posted, it appears that only staff users can have access to the endpoints bound to that viewset. Meaning no other user type/role can access those endpoints.
Going off your question, it seems like you want to setup a IsOwnerOrStaffOrReadOnly permission and over ride the list route function of the ModelViewSet and replace permission_classes and then call super
class UsersViewSet(viewsets.ModelViewSet):
permission_classes = (IsOwnerOrStaffOrReadOnly,)
queryset = User.objects.all()
serializer_class = UserSerializer
def list(self, request, *arg, **kwargs):
self.permission_classes = (OnlyStaffCanReadList,)
super(UsersViewSet, self).list(request, *args, **kwargs) // python3 super().list(request, *args, **kwargs)
is Owner object permission class
class IsOwnerOrStaffOrReadOnly(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
# Read permissions are allowed to any request,
# so we'll always allow GET, HEAD or OPTIONS requests.
if request.method in permissions.SAFE_METHODS:
return True
if request.user.role == 'staff':
return True
# Instance must have an attribute named `owner`.
return obj.owner == request.user
only staff can read permission class
class OnlyStaffCanReadList(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.user.role == 'Staff':
return True
else:
return False
as provided in the comments, your user model must have the owner role. if you are using the django user model you can just do a obj.id == request.user.id comparison
Suppose I have a ViewSet:
class ProfileViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows a user's profile to be viewed or edited.
"""
permission_classes = (permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly)
queryset = Profile.objects.all()
serializer_class = ProfileSerializer
def perform_create(self, serializer):
serializer.save(user=self.request.user)
...and a HyperlinkedModelSerializer:
class ProfileSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Profile
read_only_fields = ('user',)
I have my urls.py set up as:
router.register(r'profiles', api.ProfileViewSet, base_name='profile')
This lets me access e.g. /api/profile/1/ fine.
I want to set up a new endpoint on my API (similar to the Facebook API's /me/ call) at /api/profile/me/ to access the current user's profile - how can I do this with Django REST Framework?
Using the solution by #Gerard was giving me trouble:
Expected view UserViewSet to be called with a URL keyword argument named "pk". Fix your URL conf, or set the .lookup_field attribute on the view correctly..
Taking a look at the source code for retrieve() it seems the user_id is not used (unused *args)
This solution is working:
from django.contrib.auth import get_user_model
from django.shortcuts import get_object_or_404
from rest_framework import filters
from rest_framework import viewsets
from rest_framework import mixins
from rest_framework.decorators import list_route
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from ..serializers import UserSerializer
class UserViewSet(viewsets.ModelViewSet):
"""
A viewset for viewing and editing user instances.
"""
serializer_class = UserSerializer
User = get_user_model()
queryset = User.objects.all()
filter_backends = (filters.DjangoFilterBackend, filters.SearchFilter)
filter_fields = ('username', 'email', 'usertype')
search_fields = ('username', 'email', 'usertype')
#list_route(permission_classes=[IsAuthenticated])
def me(self, request, *args, **kwargs):
User = get_user_model()
self.object = get_object_or_404(User, pk=request.user.id)
serializer = self.get_serializer(self.object)
return Response(serializer.data)
Accessing /api/users/me replies with the same data as /api/users/1 (when the logged-in user is user with pk=1)
You could create a new method in your view class using the list_route decorator, like:
class ProfileViewSet(viewsets.ModelViewSet):
#list_route()
def me(self, request, *args, **kwargs):
# assumes the user is authenticated, handle this according your needs
user_id = request.user.id
return self.retrieve(request, user_id)
See the docs on this for more info on #list_route
I hope this helps!
You can override the get_queryset method by filtering the queryset by the logged in user, this will return the logged in user's profile in the list view (/api/profile/).
def get_queryset(self):
return Profile.objects.filter(user=self.request.user)
or
def get_queryset(self):
qs = super(ProfileViewSet, self).get_queryset()
return qs.filter(user=self.request.user)
or override the retrieve method like so, this will return the profile of the current user.
def retrieve(self, request, *args, **kwargs):
self.object = get_object_or_404(Profile, user=self.request.user)
serializer = self.get_serializer(self.object)
return Response(serializer.data)
From Gerard's answer and looking at the error pointed out by delavnog, I developed the following solution:
class ProfileViewSet(viewsets.ModelViewSet):
#list_route(methods=['GET'], permission_classes=[IsAuthenticated])
def me(self, request, *args, **kwargs):
self.kwargs.update(pk=request.user.id)
return self.retrieve(request,*args, **kwargs)
Notes:
ModelViewSet inherits GenericAPIView and the logic to get an object is implemented in there.
You need to check if the user is authenticated, otherwise request.user will not be available. Use at least permission_classes=[IsAuthenticated].
This solution is for GET but you may apply the same logic for other methods.
DRY assured!
Just override the get_object()
eg.
def get_object(self):
return self.request.user
Just providing a different way. I did it like this:
def get_object(self):
pk = self.kwargs['pk']
if pk == 'me':
return self.request.user
else:
return super().get_object()
This allows other detail_routes in the ViewSet to work like /api/users/me/activate
I've seen quite a few fragile solutions so I thought I'll respond with something more up-to-date and safer. More importantly you don't need a separate view, since me simply acts as a redirection.
#action(detail=False, methods=['get', 'patch'])
def me(self, request):
self.kwargs['pk'] = request.user.pk
if request.method == 'GET':
return self.retrieve(request)
elif request.method == 'PATCH':
return self.partial_update(request)
else:
raise Exception('Not implemented')
It's important to not duplicate the behaviour of retrieve like I've seen in some answers. What if the function retrieve ever changes? Then you end up with a different behaviour for /me and /<user pk>
If you only need to handle GET requests, you could also use Django's redirect. But that will not work with POST or PATCH.
Considering a OneToOneField relationship between the Profile and the User models with related_name='profile', I suggest the following as the #list_route has been deprecated since DRF 3.9
class ProfileViewSet(viewsets.GenericViewSet):
serializer_class = ProfileSerializer
#action(methods=('GET',), detail=False, url_path='me', url_name='me')
def me(self, request, *args, **kwargs):
serializer = self.get_serializer(self.request.user.profile)
return response.Response(serializer.data)