I'm working on a REST API with Django Rest Framework and in my ModelViewSet I need to validate that the current request.user has the right to edit a particular object.
I have found the part of the documentation that specifies how permissions work– but this is on the ViewSet side, and not the serializer side:
class FooViewSet(viewsets.ModelViewSet):
model = Foo
serializer_class = FooSerializer
def get_queryset(self):
return self.request.user.foos.all()
def perform_create(self, serializer):
serializer.save(user=self.request.user)
def get_permissions(self):
if self.action == "list":
permission_classes = [permissions.IsAuthenticated]
else:
permission_classes = [IsObjectUser]
return [permission() for permission in permission_classes]
This will work fine for 403ing when it's not the appropriate user, but I believe I should also be doing serializer-level validation? How can I get the object in question in my validate method to check against?
class FooSerializer(serializers.ModelSerializer):
class Meta:
model = Foo
fields = [
"type",
"title",
"description",
"image",
]
def validate(self, attrs):
# <-- how can I get the object so I can check against the self.request.user?
My answer is that you shouldn't. Ideally your serializers don't know about the request. That's the views realm (with exceptions). Also, since user isn't specified in fields of FooSerializer it doesn't make sense to validate the user. If the user could be specified, then it should be validated.
def validate(self, attrs):
# <-- how can I get the object so I can check against the self.request.user?
userobj=self.context['request'].user.
print(attrs)
title=attrs.get("title")
enter code here
attrs.update({'title': title})
attrs = super().validate(attrs)
return attrs
Related
I don't know how to pass user_id from requests.user.id and use it in the CustomerSerializer to save the object to the database. The error stems from the fact that user_id exists in the customer table in the database but it does not show up as a field to be passed in the rest_framework API frontend (only phone and profile_image do).
Here is the Customer model:
class Customer(models.Model):
phone = models.CharField(max_length=14)
profile_image = models.ImageField(blank=True, null=True)
user = models.OneToOneField(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
Here is the ViewSet:
class CustomerViewSet(ModelViewSet):
queryset = Customer.objects.all()
permission_classes = [permissions.IsAdminUser]
serializer_class = CustomerSerializer
# Only admin users can make requests other than 'GET'
def get_permissions(self):
if self.request.method == 'GET':
return [permissions.AllowAny()]
return [permissions.IsAdminUser()]
#action(detail=False, methods=['GET', 'PUT'])
def me(self, request):
customer, created = Customer.objects.get_or_create(user_id=request.user.id)
if request.method == 'GET':
serializer = CustomerSerializer(customer)
return Response(serializer.data)
elif request.method == 'PUT':
serializer = CustomerSerializer(customer, data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data)
... and here is the Serializer:
class CustomerSerializer(serializers.ModelSerializer):
class Meta:
model = Customer
fields = ['id', 'user_id', 'profile_image', 'phone']
```python
... and when I try to save a new customer by sending a POST request to the endpoint with the following data:
```json
{
"profile_image": "Images/image.png",
"phone": "009293930"
}
I get the following error:
IntegrityError at /api/customers/
(1048, "Column 'user_id' cannot be null")
Request Method: POST
Request URL: http://127.0.0.1:8000/api/customers/
Django Version: 4.0.6
Exception Type: IntegrityError
Exception Value:
(1048, "Column 'user_id' cannot be null")
Exception Location: /home/caleb/.local/share/virtualenvs/Cribr-svgsjjVF/lib/python3.8/site-packages/pymysql/err.py, line 143, in raise_mysql_exception
Python Executable: /home/caleb/.local/share/virtualenvs/Cribr-svgsjjVF/bin/python
Python Version: 3.8.10
Python Path:
['/home/caleb/Desktop/Cribr',
'/home/caleb/Desktop/Cribr',
'/snap/pycharm-professional/290/plugins/python/helpers/pycharm_display',
'/usr/lib/python38.zip',
'/usr/lib/python3.8',
'/usr/lib/python3.8/lib-dynload',
'/home/caleb/.local/share/virtualenvs/Cribr-svgsjjVF/lib/python3.8/site-packages',
'/snap/pycharm-professional/290/plugins/python/helpers/pycharm_matplotlib_backend']
Server time: Thu, 28 Jul 2022 23:38:53 +0000
I figured the issue here is that the serializer class is not getting the user_id value from the POST request. I tried passing request.user.id to the serializer from the viewset through a context object (i.e., context={'user_id': request.user.id}) but I couldn't figure out how to then add it to the validated data which the serializer passes to the save method.
Any help on this issue will be much appreciated. Thanks in advance.
The benefit of using DRF and viewsets is that most of the work has already been done for you. In instances such as this, you usually just need to tweak a few things to get it working the way you want. I've re-written your solution for you below:
class CustomerViewSet(ModelViewSet):
queryset = Customer.objects.all()
permission_classes = [permissions.IsAdminUser]
serializer_class = CustomerSerializer
# Only admin users can make requests other than 'GET'
def get_permissions(self):
if self.request.method == 'GET':
return [permissions.AllowAny()]
return [permissions.IsAdminUser()]
def get_object(self):
customer, created = Customer.objects.get_or_create(user_id=self.request.user.id)
return customer
#action(detail=False, methods=['GET'])
def me(self, request):
return self.retrieve(request)
def create(self, request, *args, **kwargs):
customer_exists = Customer.objects.filter(user=request.user).exists()
if customer_exists:
return self.update(request, *args, **kwargs)
else:
return super().create(request, *args, **kwargs)
def perform_create(self, serializer):
serializer.save(user=self.request.user)
From the DRF docs:
The ModelViewSet class inherits from GenericAPIView and includes implementations for various actions, by mixing in the behavior of the various mixin classes.
The actions provided by the ModelViewSet class are .list(), .retrieve(), .create(), .update(), .partial_update(), and .destroy().
The ModelViewSet will also set up a urlconf for you, which, excluding list will expect an object pk (primary key) to be provided in the url to allow the view to know what resource in the database you are trying to access. In your case, you want to determine that resource based on the authentication credentials provided in the request. To do this, we can override get_object to get or create the customer based on the authenticated user's id.
The next change we make is to define our action for the GET method. We want to be able to retrieve a resource, without specifying the pk in the url conf, hence detail=False. We can then simply call the builtin retrieve function from this action, which in turn will use get_object to get and return the customer object.
Thirdly, your PUT request will be directed to update, which is inherited from ModelViewSet, so you don't need to do anything here as you've already overwritten get_object.
def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
instance = self.get_object()
serializer = self.get_serializer(instance, data=request.data, partial=partial)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
if getattr(instance, '_prefetched_objects_cache', None):
# If 'prefetch_related' has been applied to a queryset, we need to
# forcibly invalidate the prefetch cache on the instance.
instance._prefetched_objects_cache = {}
return Response(serializer.data)
Lastly, you want to set user on your serializer to read only (or just remove it entirely), as this should always be set based on the credentials passed in the request.
class CustomerSerializer(serializers.ModelSerializer):
class Meta:
model = Customer
fields = ['id', 'user_id', 'profile_image', 'phone']
read_only_fields = ['user_id']
A great resource for looking at all the functions that you inherit from DRF classes is https://www.cdrf.co/.
Good luck, hope this helps!
Okay, I managed to solve it by overriding the create method in the serializer. I added the following:
class CustomerSerializer(serializers.ModelSerializer):
class Meta:
model = Customer
fields = ['id', 'user_id', 'profile_image', 'phone']
read_only_fields = ['user_id']
# NEW ---------------------------------
def create(self, validated_data):
user = self.context['request'].user
customer = Customer.objects.filter(user_id=user)
if customer.exists():
raise serializers.ValidationError(
'Customer already exists')
else:
customer = Customer.objects.create(
user=user, **validated_data)
return customer
The object saves fine now.
I'm writing a REST API using Django Rest Framework and I would like that one of my routes accepts bulk adding on POST method, to create multiple objects. Others methods (GET, PUT, PATCH, DELETE) will still remain the same, accepting only one at a time.
What I have so far is below and it's currently working just fine for posting one at a time.
In my urls.py:
path('book', books.BookViewSet.as_view()),
books.py:
class BookViewSet(viewsets.ModelViewSet):
serializer_class = BookSerializer
queryset = Book.objects.all()
permission_classes = (IsAuthenticated, )
serializer.py:
class BookSerializer(serializers.ModelSerializer):
def create(self, validated_data):
# I assume this is the method to be overridden to get this
class Meta:
model = Book
fields = ('id', 'name', 'author_id', 'page_number', 'active')
Serializer create method, unfortunatelly creates data object by object.You can override create method of ModelViewSet and after validation use bulk_create method.
def create(self, request, *args, **kwargs):
many = True if isinstance(request.data, list) else False
serializer = BookSerializer(data=request.data, many=many)
serializer.is_valid(raise_exception=True)
author = request.user # you can change here
book_list = [Book(**data, author=author) for data in serializer.validated_data]
Book.objects.bulk_create(book_list)
return Response({}, status=status.HTTP_201_CREATED)
I'm using Django rest framework, and my model is like this, Every Act can have more than one post.
class Act(models.Model):
user = models.ForeignKey("common.MyUser", related_name="act_user")
act_title = models.CharField(max_length=30)
act_content = models.CharField(max_length=1000)
act_type = models.IntField()
class Post(models.Model):
user = models.ForeignKey("common.MyUser", related_name="post_user")
act = models.ForeignKey("activities.Act", related_name="post_act")
post_title = models.CharField(max_length=30, blank=True)
post_content = models.CharField(max_length=140)
my view.py in DRF:
class PostList(generics.ListCreateAPIView):
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
queryset = Post.objects.all()
serializer_class = PostAllSerializer
def perform_create(self, serializer): #self is a instance of class or is a class here?
serializer.save(user=self.request.user)
This works fine, but what I want now is if act_type = 1 means this is a private Act and only the act author can create post under this act.I wonder how to use different permission_classes depend on different Act.Maybe looks like:
class PostList(generics.ListCreateAPIView):
if self.act_type == 1:
permission_classes = (permissions.IsAuthenticatedOrReadOnly,IsActCreatorOrReadOnly)
else
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
queryset = Post.objects.all()
serializer_class = PostAllSerializer
def perform_create(self, serializer): #self is a instance of class or is a class here?
serializer.save(user=self.request.user)
And I also want to know how to write this permissions.py:
class IsActCreatorOrReadOnly(permissions.BasePermission):
"""
Custom permission to only allow owners of an object to edit it.
"""
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
return obj.act.user == request.user
I don't know what obj really means here, and the error tell me obj.act doesn't exist.
EDIT
Here is my postSerializer.
class PostAllSerializer(serializers.ModelSerializer):
"""Posts api fields"""
post_user = UserSerializer(source="user", read_only=True)
post_author = serializers.ReadOnlyField(source='user.user_name')
class Meta:
model = Post
fields = ("id", "act", "post_author", "post_title", "post_content",)
I tried this, but not working, I still can create the post even I'm not the author of the Act(but the act_id is wrong):
def create(self, request, *args, **kwargs):
act_type = request.data.get("act_type")
if act_type == 0:
act_id = request.data.get("act")
act = Act.objects.get(pk=act_id)
if request.user != act.user:
return Response(status=403)
return super().create(request)
For using different permission classes, there is the get_permissions method that you can overwrite on your PostList view:
def get_permissions(self):
if self.request.method == 'POST':
return (OnePermission(),)
elif # other condition if you have:
return (AnotherPermission(),)
return (YetAnotherPermission(),)
However, in your case you can't use object level permissions, because you don't have an object instance yet. From the DRF docs (highlights by me):
REST framework permissions also support object-level permissioning. Object level permissions are used to determine if a user should be allowed to act on a particular object, which will typically be a model instance.
Object level permissions are run by REST framework's generic views when .get_object() is called.
When doing a POST request, you don't have any object yet, thus the object level permissions won't be invoked.
One way you could achieve what you want is by checking it in the create method of PostList view. Something like this (hypothetical code):
class PostList(generics.ListCreateAPIView):
...
def create(self, request):
act_id = request.data.get('act') # depending on your PostSerializer, the logic of getting act id can vary a little
act = Act.objects.get(pk=act_id) # assuming act always exists, otherwise account for in-existing act
if act.user != request.user:
return Response({details: "You shall not pass!!!", status=200) # change to a status and message you need here
# logic of Post creation here
Good luck!
i am using django rest framework for my project.
i have a gallery model which has a user field. this field is a Foreign key to the user that created the gallery. and a name field which is gallery's name.
class Gallery(models.Model):
user = models.ForeignKey(User,related_name='galleries')
name = models.CharField(max_length=64)
here is my serializer:
class GallerySerializer(serializers.ModelSerializer):
user = serializers.ReadOnlyField(source='user.username')
def validate_name(self, name):
if len(name) < 3:
raise serializers.ValidationError("name must at least 3 letters")
return name
class Meta:
model = Gallery
fields = ('id', 'user', 'name',)
def create(self, validated_data):
"""
Create and return a new `Gallery` instance, given the validated data.
"""
return Galleries.objects.create(**validated_data)
and here is my views:
class GalleryList(generics.ListCreateAPIView):
queryset = Gallery.objects.all()
serializer_class = GallerySerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,
IsOwnerOrReadOnly,)
def perform_create(self, serializer):
serializer.save(user=self.request.user, )
class GalleryDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Gallery.objects.all()
serializer_class = GallerySerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,
IsOwnerOrReadOnly,)
i want to validate post (and put) data, and prevent user from creating two gallery with the same name.(if "user A" has a "gallery A", he can't make a gallery with the same name or rename another gallery to "gallery A". but "user B" is allowed to make a "gallery A")
to do so, i need to check if user.galleries.filter(name=name) exist or not.
but i don't know how to get user in serializer.
You get it from the context that was passed to the serializer. This is done automatically for you, so you can access it like that:
user = self.context['request'].user
If you want to have the ability to specify another user, you can add it to the context yourself:
# This method goes in your view/viewset
def get_serializer_context(self):
context = super().get_serializer_context()
context['user'] = #whatever you want here
return context
That would make the user available as self.context['user']. This is a bit more verbose, but it is more versalite as it allows the serializer to be passed a user different from the one who did the request.
I'm using the Django Rest Framework and I'd like to be able to add extra detail to the serializer when a single object is returned, which would be left out of the list view.
In the code below I add the celery_state field to the TestModelSerializer, but I'd only like this field to be added when its returning a single object, not when it's returning the list of TestModel data.
I've looked at the list_serializer_class option but it seems to just use the original model serializer so it will always still include the field even if I try to exclude from there.
What are my options?
class TestModelSerializer(serializers.HyperlinkedModelSerializer):
celery_state = serializers.CharField(source='celery_state', read_only=True)
class Meta:
model = TestModel
class TestModelViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows TestModels to be viewed or edited.
"""
authentication_classes = (SessionAuthentication, BasicAuthentication)
permission_classes = (IsAuthenticatedOrReadOnly,)
queryset = TestModel.objects.all()
serializer_class = TestModelSerializer
Since the serializer class (used by the viewsets) passes many argument, you can use that to control the fields output:
class TestModelSerializer(serializers.HyperlinkedModelSerializer):
# ...
def __init__(self, *args, **kwargs):
super(TestModelSerializer, self).__init__(*args, **kwargs)
if kwargs.get('many', False):
self.fields.pop('celery_state')
Inspired by #mariodev answer:
The other possibility is to override many_init static method in serializer. Acording comments in thie code (https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/serializers.py#L128 ) it is suggested variant.
from rest_framework import serializers
class ExtendedSerializer(serializers.Serializer):
...
#classmethod
def many_init(cls, *args, **kwargs):
kwargs['child'] = cls()
kwargs['child'].fields.pop('extractedFiled')
return serializers.ListSerializer(*args, **kwargs)
You can have an extra serializer called ExtendedTestModelSerializer which would contain the extra fields that you want.
After that, you can use the get_serializer_class method to decide which serializer is used based on request.action -
class TestModelViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows TestModels to be viewed or edited.
"""
authentication_classes = (SessionAuthentication, BasicAuthentication)
permission_classes = (IsAuthenticatedOrReadOnly,)
queryset = TestModel.objects.all()
# serializer_class = TestModelSerializer
get_serializer_class(self):
if self.request.action == 'list':
return TestModelSerializer
return ExtendedTestModelSerializer