Django Rest Framework ListCreateAPIView not checking has_object_permissions? - python

I've been trying to figure this out for almost a full day now, and I can't seem to figure out why has_object_permission method isn't called when the ListCreateAPIView is called in DRF. I've tried all the solutions I could find, but according to the docs check_object_permissions is called in this class already.
I know it has to be something stupid I'm missing. Code snippets are below, please help!
views.py:
from accountability.models import AccountabilityItem
from accountability.serializers import AccountabilityItemSerializer
from rest_framework import generics
from .permissions import InGroup
class AccountabilityItemListCreate(generics.ListCreateAPIView):
queryset = AccountabilityItem.objects.all()
serializer_class = AccountabilityItemSerializer
permission_classes = (InGroup,)
permissions.py:
from rest_framework import permissions
class InGroup(permissions.BasePermission):
"""
Custom permission to only allow owners of an object to edit it.
"""
def has_object_permission(self, request, view, obj):
print('Checking for object')
return False
Another note, I've added the has_permission method to the permissions.py file, and this method runs all the time no matter what.
Thanks!

Calling has_object_permission doesn't make sense for lists. It is intended for single instances.
What you want is to filter your list of objects so it only leaves those for which the user has some permissions. DjangoObjectPermissionsFilter does it but requires django-guardian. You might get a similar result but creating your own filtering class (sources for DjangoObjectPermissionsFilter)

Related

The order of inherited classes matters in Python?

I came across this error in my django application after hitting submit on a create or edit form:
No URL to redirect to. Either provide a url or define a get_absolute_url method on the Model..
This was confusing because I have a get_success_url passed down through inheritance. To be clear, I have found the issue, but have no earthly idea why my solution worked.
Here was the code causing the error inside
.../views.py:
class FormViews():
model = Ticket
form_class = TicketForm
def get_success_url(self):
return reverse('tickets:index')
class TicketCreate(CreateView, FormViews):
template_name = 'tickets/ticket_create_form.html'
model = Ticket
form_class = TicketForm
class TicketUpdate(UpdateView, FormViews):
model = Ticket
form_class = TicketForm
template_name_suffix = '_update_form'
I created the FormViews class so there would not be any repeated code for the model, form_class, and get_success_url.
I was able to resolve this error by switching the parameters in my function definitions:
class TicketCreate(CreateView, FormViews) became class TicketCreate(FormViews, CreateView)
class TicketUpdate(UpdateView, FormViews) became class TicketUpdate(FormViews, UpdateView)
This fixed it. Now I redirect to the index page without any issues. Why is it that the get_success_url is recognized after switching the listed parent classes? I would have thought that the attributes and functions are inherited and recognized by Django regardless of order. Is this a Python or Django related issue?
In python every class has something called an MRO (Method Resolution Order), this explains it pretty well. Your FormViews (also for the most part classes in python are singular) is more of a mixin, I would call it as such: FormViewMixin.
Since CreateView and UpdateView are proper classes that have get_success_url defined, the order ABSOLUTELY matters. So I would put the things you want "discovered", first.
class TicketCreateView(FormViewMixin, CreateView):
...
is what you want.

return list of ids in a Get. Django Rest Framework

I have a model called "document-detail-sample" and when you call it with a GET, something like this, GET https://url/document-detail-sample/ then you get every "document-detail-sample".
Inside the model is the id. So, if you want every Id, you could just "iterate" on the list and ask for the id. Easy.
But... the front-end Developers don't want to do it :D they say it's too much work...
So, I gotta return the id list. :D
I was thinking something like GET https://url/document-detail-sample/id-list
But I don't know how to return just a list. I read this post and I know how to get the id_list in the backend. But I don't know what should I implement to just return a list in that url...
the view that I have it's pretty easy:
class DocumentDetailSampleViewSet(viewsets.ModelViewSet):
queryset = DocumentDetailSample.objects.all()
serializer_class = DocumentDetailSampleSerializer
and the url is so:
router.register(r'document-detail-sample', DocumentDetailSampleViewSet)
so:
1- is a good Idea do it with an url like .../document-detail-sample/id-list" ?
2- if yes, how can I do it?
3- if not, what should I do then?
You could use #list_route decorator
from rest_framework.decorators import detail_route, list_route
from rest_framework.response import Response
class DocumentDetailSampleViewSet(viewsets.ModelViewSet):
queryset = DocumentDetailSample.objects.all()
serializer_class = DocumentDetailSampleSerializer
#list_route()
def id_list(self, request):
q = self.get_queryset().values('id')
return Response(list(q))
This decorator allows you provide additional endpoint with the same name as a method. /document-detail-sample/id_list/
reference to docs about extra actions in a viewset
Assuming you don't need pagination, just override the list method like so
class DocumentDetailSampleViewSet(viewsets.ModelViewSet):
queryset = DocumentDetailSample.objects.all()
serializer_class = DocumentDetailSampleSerializer
def list(self, request):
return Response(self.get_queryset().values_list("id", flat=True))

Permission class on #detail_route not working, Django Rest Framework

I'm new to DRF, but I'm trying to use a permission class on a #detail_route using the method in this stack thread: Using a permission class on a detail route
My code currently looks like this :
#detail_route(methods=['GET'], permission_classes=[IsStaffOrRestaurantUser])
def restaurant_dishes_ready_for_pickup(self, request, pk=None):
...stuff....
class IsStaffOrRestaurantUser(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
print(request)
print(view)
print(obj)
return False
The print statements never get executed... I'm probably missing something but I've looked through the documentation and can't really figure it out, is my approach right at all? Thanks!
EDIT:
I realize in our code already that we have this snippet in our Viewset, is it possible to override this in the decorator?
def get_permissions(self):
# Limit to listing and getting for non Admin user
if self.request.method in permissions.SAFE_METHODS:
return (permissions.AllowAny(),)
return (IsAdminUser(),)
Not sure if it's the most elegant solution, but you might be able to upgrade get_permissions() like so:
def get_permissions(self):
# check additional route specifics
path = self.request.path
if ("restaurant_dishes_ready_for_pickup" in path):
return (IsStaffOrRestaurantUser,)
# Limit to listing and getting for non Admin user
if (self.request.method in permissions.SAFE_METHODS):
return (permissions.AllowAny,)
return (IsAdminUser,)
PS: Also maybe return permission class objects instead of instances in get_permissions().
Quote from the documentation:
If you're writing your own views and want to enforce object level permissions, or if you override the get_object method on a generic view, then you'll need to explicitly call the .check_object_permissions(request, obj) method on the view at the point at which you've retrieved the object.
So you'll need to call explicitly the permission check.
Note that you could have that for free if you were using a RetrieveAPIView instead of a function based view for example.

How to override the CRUD methods in tastypie?

As the title suggests I'd like to know if and how I can override the get and post methods of Tastypie.
For example, every time a user sends over a json file at the API endpoint, I don't want anything to be stored in the models and instead only return a small message back.
How can I do this?
Thanks.
This example coming directly from Tastypie Cookbook:
from tastypie.utils import now
class MyResource(ModelResource):
class Meta:
queryset = MyObject.objects.all()
def get_object_list(self, request):
return super(MyResource, self).get_object_list(request).filter(start_date__gte=now)
Similar approach can be utilized for POST etc. as well. Hope it helps :)

How Can I Allow Object Editing in the Django Admin For Specific Objects ONLY?

I'm currently writing a site which uses django-guardian to assign object-level permissions which are used throughout the site.
Here is the desired functionality:
I'd like for a user to have permissions to edit a single object (or multiple objects). For example, if there is a user named "Joe", and a model named "Partyline", I may give "Joe" specific permissions to "change_partyline" for 3 specific "Partyline" objects.
When Joe logs into the Django admin panel, I'd like him to be able to edit ONLY his 3 specific "Partyline" objects, since those are the only things he has permission to edit.
Here is the current functionality:
I can assign Joe change_partyline permissions to 3 Partyline objects--no problem. And Joe can log into the admin panel just fine. The problem is that since Joe doesn't have "global" permissions to change ALL partylines, the admin panel says that he does not have any permissions, and won't let him edit anything. I'd like to find a way for the admin to recognize that Joe has permissions to edit only 3 specific objects, and let him view and edit only those objects which he has permissions to work on.
I'd LOVE to find a way to make this work. I'm using the admin extensively for my users to manage things right now, and it would really break presentation to have to move certain functionality out of the admin to other areas on the site.
If you have any suggestions, please let me know!
For reference, here is some shell output demonstrating that the user has change_partyline permissions on a Partyline object:
>>> from django.contrib.auth.models import User
>>> u = User.objects.get(id=2)
>>> from apps.partylines.models import Partyline
>>> p = Partyline.objects.get(id=3)
>>> u.has_perm('partylines.change_partyline', p)
True
And here's my partylines.admin module (which shows how the Partyline module is populated in the admin):
from django.contrib import admin
from guardian.admin import GuardedModelAdmin
from apps.partylines.models import Partyline
class PartylineAdmin(GuardedModelAdmin):
list_display = ('did', 'name', 'listed')
ordering = ('did',)
search_fields = ('did', 'name')
admin.site.register(Partyline, PartylineAdmin)
I've asked a similar question to Lukazs (guardian's author) and he told me that this feature is coming on a future release (see user_can_access_owned_objects_only property on this commit and the related issue). If you're not willing to wait, maybe you can just install the source on the master branch.
Have you thought about overriding queryset on your model? In my case was just enough:
# models.py
from django.db import models
from django.contrib.auth import models as auth_models
class MagazineUser(models.Model):
user = models.ForeignKey(auth_models.User, unique=True)
class Magazine(models.Model):
managers = models.ForeignKey(MagazineUser)
# admin.py
class MagazineAdmin(admin.ModelAdmin):
def queryset(self, request):
qs = super(admin.ModelAdmin, self).queryset(request)
if request.user.is_superuser:
return qs
user_qs = MagazineUser.objects.filter(user=request.user)
return qs.filter(managers__in=user_qs)
That functionality is known as "row-level permissions", and there is a short explanation of some of the admin methods you'll need to override on the Django wiki here.
For your specific use case, I'd guess you want a ManyToMany relationship between Partyline and User, so that the objects allowed for a user can be retrieved via:
objects = request.user.partyline_set.all()
It might simply be enough to override queryset() and return that object list.

Categories