Reputation: 2857
I have nested data; a List contains many Items. For security, I filter Lists by whether the current user created the list, and whether the list is public. I would like to do the same for items, so that items can only be updated by authenticated users, but can be viewed by anybody if the list is public.
Here's my viewset code, adapted from the List viewset code which works fine. This of course doesn't work for Items because the item doesn't have the properties "created_by" or "is_public" - those are properties of the parent list.
Is there a way I can replace "created_by" and "is_public" with the list properties? i.e. can I get hold of the parent list object in the item's get_queryset method, and check it's properties?
The alternative is that I assign "created_by" and "is_public" to the item as well, but I would prefer not to do that because it is duplicated data. The lists's properties should control the item's permissions.
class ItemViewSet(viewsets.ModelViewSet):
permission_classes = [permissions.AllowAny, ]
model = Item
serializer_class = ItemSerializer
def get_queryset(self):
# restrict any method that can alter a record
restricted_methods = ['POST', 'PUT', 'PATCH', 'DELETE']
if self.request.method in restricted_methods:
# if you are not logged in you cannot modify any list
if not self.request.user.is_authenticated:
return Item.objects.none()
# you can only modify your own lists
# only a logged-in user can create a list and view the returned data
return Item.objects.filter(created_by=self.request.user)
# GET method (view item) is available to owner and for items in public lists
if self.request.method == 'GET':
if not self.request.user.is_authenticated:
return Item.objects.filter(is_public__exact=True)
return Item.objects.filter(Q(created_by=self.request.user) | Q(is_public__exact=True))
# explicitly refuse any non-handled methods
return Item.objects.none()
Many thanks for any help!
Edit: between Lucas Weyne's answer and this post I think I have got this sorted now. Here's my working code in api.py:
from rest_framework import viewsets, permissions
from .models import List, Item
from .serializers import ListSerializer, ItemSerializer
from django.db.models import Q
class IsOwnerOrReadOnly(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
# handle permissions based on method
# 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 hasattr(obj, 'created_by'):
return obj.created_by == request.user
if hasattr(obj, 'list'):
if hasattr(obj.list, 'created_by'):
return obj.list.created_by == request.user
class ListViewSet(viewsets.ModelViewSet):
permission_classes = [IsOwnerOrReadOnly]
model = List
serializer_class = ListSerializer
def get_queryset(self):
# can view public lists and lists the user created
if self.request.user.is_authenticated:
return List.objects.filter(
Q(created_by=self.request.user) |
Q(is_public=True)
)
return List.objects.filter(is_public=True)
def pre_save(self, obj):
obj.created_by = self.request.user
class ItemViewSet(viewsets.ModelViewSet):
permission_classes = [IsOwnerOrReadOnly]
model = Item
serializer_class = ItemSerializer
def get_queryset(self):
# can view items belonging to public lists and lists the usesr created
if self.request.user.is_authenticated:
return Item.objects.filter(
Q(list__created_by=self.request.user) |
Q(list__is_public=True)
)
return Item.objects.filter(list__is_public=True)
Upvotes: 1
Views: 898
Reputation: 1152
Django allows lookups that span relationships. You can filter Item objects across List properties, just use the field name of related fields across models, separated by double underscores, until you get to the field you want.
class ItemViewSet(viewsets.ModelViewSet):
permission_classes = [IsOwnerOrReadyOnly]
serializer_class = ItemSerializer
def get_queryset(self):
if self.request.user.is_authenticated
return Item.objects.filter(
Q(list__created_by=self.request.user) |
Q(list__is_public__exact=True)
)
return Item.objects.filter(list__is_public=True)
To allow items to be updated only by its owners, write a custom object-level permission class.
class IsOwnerOrReadOnly(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
# Instance must have an attribute named `created_by`.
return obj.list.created_by == request.user
Upvotes: 2