I use the Django Rest framework together with an JavaScript app. I have some difficulties to get the posting of new data items right with the generic ModelViewSet
Most importantly I want to restrict what a poster can submit
(they should only be allowed to post items that have the user_id of this user (the authenticated user of the session).
I don't know when/where I should check for this? Is this a validation problem?
How I understand the permission classes is that they restrict the method (Post/Get) or check for user groups.
Also my user field in the item model is a foreign key to the user model
so the browsable api suggest in the Html-form a dropdown with the information about other users. (their email adresses and some other fields).
My data items look like this
[{
"id": 792,
"name": "test",
"category": 1,
"value": 5,
"user": "33"
}]
Here is my Serializer and the Viewset:
class ItemSerializer(serializers.ModelSerializer):
class Meta:
model = Item
fields = ('id',
'name',
'category',
'value',
'user',
)
class ItemViewSet(viewsets.ModelViewSet):
serializer_class = ItemSerializer
def get_queryset(self):
return Item.objects.filter(user=self.request.user)
I can't believe this issue with the DRF Create/Update (Post/Put) form isn't more widely discussed.
It's a huge data privacy issue - e.g. One can restrict the List API view to only show items owned by a User via overriding the get_queryset method inside as below:
# views.py
class ItemViewSet(viewsets.ModelViewSet):
def get_queryset(self):
return Item.objects.filter(user=self.request.user)
But as OP notes, when accessing the API Create/Post or Update/Put form for the ItemViewSet, there is seemingly no easy way to restrict the user options to the user itself.
I had a similar issue myself building a survey platform, where I want to restrict choice of survey/question/options etc. to those owned by the user, and prevent users from inadvertently seeing each other's data.
Jocelyn's answer works for the OP's particular situation where we already know that the Item.user must equal request.user, so we override this on the perform_create method.
But Jocelyn's solution is insufficient for situations where you do not know in advance what the relationship between model instances will be (e.g. in my case where a new question objected could be added to any one of a user's surveys).
The solution I came up with was the nuclear option: do away with the Viewset altogether for Create and Update functionality, and use a set of custom views.APIView classes instead, as below (adapted for the case of the OP, only showing Create).
class ItemCreateView(views.APIView):
def post(self, request, format=None):
post_user_id = int(request.data['user'].split('/')[-2])
request_user_id = request.user.id
serializer = ItemSerializer(data=request.data, context={'request': request})
if post_user_id == request_user_id:
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
else:
return Response('Not Allowed: Owner is not User', status=status.HTTP_401_UNAUTHORIZED)
Please note, I'm using a HyperlinkedModelSerializer rather than a plain ModelSerializer, hence the need for .split('/')[-2] to grab the post_user_id
Handling the user field
First set the user field to be readonly:
# serializers.py
class ItemSerializer(serializers.ModelSerializer):
user = serializers.ReadOnlyField()
class Meta:
model = Item
fields = ('id',
'name',
'category',
'value',
'user',
)
Then auto-set the user id on creation:
# views.py
class ItemViewSet(viewsets.ModelViewSet):
serializer_class = ItemSerializer
def get_queryset(self):
return Item.objects.filter(user=self.request.user)
def perform_create(self, serializer):
serializer.save(user=self.request.user.customer)
Handling permissions
Just use standard permissions mechanism to define a custom one :
# permissions.py
from rest_framework import permissions
class IsOwner(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
return (request.user.is_authenticated() and
(obj.user == request.user.customer))
...and use it in your viewset :
# views.py
from permissions import IsOwner
class ItemViewSet(viewsets.ModelViewSet):
permission_classes = [IsOwner]
...
Related
My drf view creates 1~3 items in one request.
My View:
class UserInterestAPIView(ListCreateDeleteAPIView):
queryset = UserInterest.objects.all()
serializer_class = UserInterestSerializer
def create(self, request, *args, **kwargs):
data = request.data.get("data")
serializer = self.get_serializer(data=data, many=True)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
return Response(serializer.data, status=status.HTTP_201_CREATED)
ListCreateDeleteAPIView is custom view. It inherits mixins.
My serializer:
class UserInterestSerializer(serializers.ModelSerializer):
class Meta:
model = UserInterest
fields = "__all__"
I tested with postman, so my post body looks like this:
{
"data":[{
"user":2,
"interest":1,
"sequence":4
},{
"user":2,
"interest":10,
"sequence":5
}]
}
This works well but I realized that body data will not contain user because my team will use login_decorator for checking user. I can get "user" from request.user, but I don't know how to put user into every single items in my list from request.data.
I mean, post body data should be:
{
"data":[{
"interest":1,
"sequence":4
},{
"interest":10,
"sequence":5
}]
}
and now I have User instance. My model needs user_id as well.
I guess user_passes_test doesn't fit for me. Is there any method or anything I can apply?
Edit: I'm thinking of using for loop, but I don't want to use it.
What I am trying to do is set the serializer_class attribute through get_serializer_class method like this
if user.role==1 :
return adminLoanView
elif user.role==2 :
return agentLoanView
else:
return customerLoanView
And in def put() function use these serializers based on the logged in user to perform different function that is admin and agent can only edit specific fields. And customer can not edit anything.
I am still a beginner in DRF so let me know if there is any other better way to do this.
If you're using Django's own User model which has .is_superuser and .is_staff fields, you can override the the get_serializer_class() like so:
def get_serializer_class(self):
user = self.request.user
if user.is_superuser:
return SerializerClassWithHigherPermissions
elif user.is_staff:
return SerializerClassWithIntermediatePermissions
else:
return SerializerClassWithCustomerPermissions
If you have a custom User model with a OnetoOneField on the User model, you could access that instance through the user instance in the view, and then check for permissions conditions.
I have a view set up to return a list of books to a user, which is retrieved from a simple book model based on the currently logged-in user. However, I also have ReadingSession model which has a foreign key relationship to both the Book, and the User.
When I'm retrieving the books for the user, I'd like to, at the very least, return a list of primary keys that I can use to get the length of in my client.
The following code will get the full set of readingsessions in my BookSerializer:
from rest_framework import serializers
from books.models import Book
class BookSerializer(serializers.ModelSerializer):
readingsession_set = serializers.PrimaryKeyRelatedField(
many=True, read_only=True)
class Meta:
model = Book
fields = ["id", "title", "author", "publisher",
"publish_date", "description", "category",
"language", "small_thumbnail", "thumbnail",
"readingsession_set"]
However, the problem with this is that it will return all of the readingsessions, regardless of whether or not the session belongs to that user.
I'd like to be able to filter that so that it will only return the readingsessions for the current user. Something along the lines of:
readingsession_set = serializers.PrimaryKeyRelatedField(queryset=ReadingSession.objects.filter(user=user), read_only=True)
But I've tried various ways of trying to pass the user (self.request.user) from the APIView but none seem to work. I've tried passing a context, and tried passing extra **kwargs in __init__ but none seem to work.
Is there a way of achieving this? Or am I taking the wrong approach?
Thanks
The user is not present on the serializer's declaration but during its instantiation.
Therefore, you can filter querysets by user within the __init__ method.
from rest_framework import serializers
from bar.models import Foo
class RandomSerializer(serializers.Serializer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
user_foos = Foo.objects.filter(user=self._user)
self.fields['foo_ids'] = serializers.PrimaryKeyRelatedField(
required=False,
many=True,
read_only=False,
queryset=user_foos,
default=user_foos)
#property
def _user(self):
request = self.context.get('request', None)
if request:
return request.user
Don't forget to pass the request object to the serializer in the context (if necessary, e.g., using a simple APIView.
from rest_framework import views
class RandomView(views.APIView):
serializer_class = RandomSerializer
def post(self, request):
serializer = self.serializer_class(
data=request.data, context={'request': request})
# ...
serializer = RandomSerializer(data=request.data, context={'request': request}
You can access the user of the request on the serializer by means of the context.
As mentioned in the documentation, you can always do:
serializer = AccountSerializer(account, context={'request': request})
Thus, you will be able to use self.context['request'].user inside your serializer.
Hope that's what you're after.
I am trying to implement a system where a user can subscribe to a company. For that I have to implement the following flow:
User registers
User klicks on a button to subscribe to a company
User has to enter a code (every company has a secret code. By possession the user proves that he is somewhat related to the company)
User is subscribed
For this, I have to implement an API endpoint that receives the code by the user (the user is authenticated at this point).
It is shameful, but I am lost: I am thinking of implementing a view like this.
class RegisterUserToCustomer(APIView):
permission_classes = (permissions.IsAuthenticated,)
def post(self, request, format=None):
serializer = CustomerSerializer(data=request.data)
if serializer.is_valid():
serializer.save(user=self.request.user)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.data, status=status.HTTP_400_BAD_REQUEST)
Obviously, I have to:
Receive the code from the user via POST method
Search the database and figure out, if the code corresponds to any company
Edit the ForeignKey field on the user and link it to the company
But where would I implement this logic. With my limited experience I see three possibilities:
Call serializer.save() and write a custom create() method (but I don't want to create anything so this seems like bad practice
Implement this logic in the view, but I want to access the code from the validated_data (this seems like a problem?)
Can I write a custom save() method for the serializer? Are there any examples for a custom save() method since the original drf save() method contains a lot of validation logic.
Obviously I don't expect you guys to write the whole code for me, but maybe someone has somewhat of a blueprint of how and where to implement this?
Steps to do this:
Create a field company_code in Customer table and make that
field as Foreign key.
Create a Serializer with that foreign key field.
Create a viewset or generic views to expect that field from Post method as Id of the Company. Use save() method to store foreign key.
This scenarios is only for Subscribe one organisation, if you're able to subscribe multiple organisation make that field as ManytoMany field.
Refer the code sample,
Models.py
class Customer(models.Model):
# customer related field
name = models.CharField(max_length=20)
organization = models.ForeignKey(Organization, on_delete=models.CASCADE)
Serializer.py
class CustomerSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('name', 'organization', )
Views.py
class CustomerViewSet(viewsets.ModelViewSet):
serializer_class = CustomerSerializer
queryset = Customer.objects.all()
Request data(POST Json)
{
"name": "test",
"organization": "id-of company"
}
I have a user profile class and am checking if a user exists and if not want to create that user.
Am using the filter class for userprofile so that the client can call :
http://localhost:8000/users/?email=a#b.com
and if the result is empty will create a user with the email address.
Is there a way to intercept the query result and raise an exception when its empty and handle that to create the user.
If there is a better way would like to be corrected as well.
class UserQueryFilter(django_filters.rest_framework.FilterSet):
email = django_filters.CharFilter(name="user__email")
username = django_filters.CharFilter(name="user__username")
class Meta:
model = UserProfile
fields = ['email', 'username']
class UserViewSet(viewsets.ReadOnlyModelViewSet):
queryset = UserProfile.objects.all()
serializer_class = UserSerializer
filter_class = UserQueryFilter
Any help is appreciated.
Thanks
Anand
Django Rest Framework provide a functionality that is disabled by default. Maybe it could give you another approach to resolve your problem: PUT as create
In other hand, if you really need to create the user through a GET request with a querystring, you can use a MethodFilter from django-filters, for example:
class UserFilters(FilterSet):
user = MethodFilter(action='filter_user')
class Meta:
model = User
fields = ['user']
def filter_user(self, queryset, value):
if not value:
# Here Raise Exception
else:
# Check if the user exists, if not create it
users = queryset.filter(Q(username=value) | Q(email=value))
if not users.exists:
user = User.objects.create(...)
return queryset.filter(pk=user.id)
else:
return users
Hope this can help you. I'm not pretty sure about it works in that exact way but it's the idea.
Personally, I recommend you that try to execute that tasks through a more appropriate request like POST or PUT and manage in the corresponding method.