DRF - ModelViewSet Create - "This field is required." - django

I have the following json I'm trying to post -
{
"title": "test",
"description": "testing desc",
"username": "admin",
"password": "Welcome1",
"owner": 4
}
The ModelViewSet is below -
class PasswordViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows users to be viewed or edited.
"""
queryset = Password.objects.all()
serializer_class = PasswordSerializer
filter_class = PasswordFilter
def list(self, request):
my_passwords = Password.objects.filter(owner=request.user)
page = self.paginate_queryset(my_passwords)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = PasswordSerializer(my_passwords, many=True)
return Response(serializer.data)
If I remove the def list I can do a post just fine. The problem is trying to post with that def in there. it returns that all my fields are required. Why is it not passing the data?
This is my serializer for reference -
class PasswordSerializer(serializers.ModelSerializer):
class Meta:
model=Password
edit - no idea what's going wrong testing some more here and now it's not even working with the def list removed.
model -
class Password(models.Model):
title = models.CharField(max_length=100)
description = models.CharField(max_length=1024)
username = models.CharField(max_length=50)
password = models.CharField(max_length=200)
owner = models.ForeignKey('MyUser', related_name='MyUser_owner')
The response is telling me all the fields in the model are required so to me on post the request.data is empty even though the json should be it.

Related

Django Rest Framework unique field constraint on array

So, I'm trying to make an endpoint where I insert a list of objects.
My issue is the behavior and response when inserting duplicates.
What I want to accomplish is to:
Send the duplicate lead external_id(s) in the error response
Insert any other non duplicated object
I'm pretty sure this logic (for the response and behavior) could be accomplished in the modifying the serializer.is_valid() method... but before doing that, I wanted to know if anyone had experience with this kind of request.. Maybe there is a "clean" way to do this while keeping the unique validation in the model.
Data on OK response:
[
{
"external_id": "1",
"phone_number": "1234567890"
}
]
Data for a FAIL request (1 is dup, but 2 should be inserted. Expecting a response like "external_id" : "id 1 is duplicated"):
[
{
"external_id": "1",
"phone_number": "1234567890"
},
{
"external_id": "2",
"phone_number": "2234567890"
}
]
models.py
class Lead(models.Model):
external_id = models.CharField(max_length=20, unique=True)
phone_number = models.CharField(max_length=50)
serializers.py
class LeadSerializer(serializers.ModelSerializer):
class Meta:
model = Lead
fields = '__all__'
def create(self, validated_data):
lead = Lead.objects.create(**validated_data)
log.info(f"Lead created: {lead.import_queue_id}")
return lead
views.py
class LeadView(APIView):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
#extend_schema(description="Insert campaign data", request=LeadSerializer(many=True), responses=None, tags=["Leads"])
def post(self, request):
serializer = LeadSerializer(data=request.data, many=True)
valid = serializer.is_valid()
if serializer.is_valid():
serializer.save()
return Response({"status": "success", "data": serializer.data}, status=status.HTTP_200_OK)
else:
return Response({"status": "error", "data": serializer.errors}, status=status.HTTP_400_BAD_REQUEST)
You can customize the behaviour of list of object through list_serializer_class option in meta class. Like this:
class LeadListSerializer(serializers.ListSerializer):
def validate(self, data):
items = list(map(lambda x: x['phone_number'], data))
if len(set(items)) == len(items)):
return super().validate(data)
raise ValidationError()
class LeadSerializer(serializers.ModelSerializer):
class Meta:
model = Lead
fields = '__all__'
list_serializer_class = LeadListSerializer

Configure Django Rest Framework to populate user field with logged in user before serializer validation

I have an API endpoint that supports the POST method which works if I include the user in the request payload.
I would like to eliminate this from the request payload and have DRF just use the logged in user.
I am getting the following error when I omit the user from the request body:
HTTP 400 Bad Request
Allow: GET, POST, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept
{
"user": [
"This field is required."
]
}
This appears to be coming from when the framework calls serializer.is_valid().
How do I configure DRF to populate the user from request.user so that serializer validation doesn't fail?
models.py
class Task(models.Model):
created = models.DateTimeField(auto_now_add=True)
user = models.ForeignKey(CustomUser, on_delete=models.CASCADE)
domain = models.CharField(max_length=settings.MAX_CHAR_COUNT)
serializers
class TaskSerializer(serializers.HyperlinkedModelSerializer):
user = serializers.SlugRelatedField(queryset=CustomUser.objects.all(), slug_field='email')
class Meta:
model = Task
fields = ['id', 'created', 'domain', 'user']
views.py
class TaskApiViewSet(viewsets.ModelViewSet):
permission_classes = [permissions.IsAuthenticated,]
serializer_class = TaskSerializer
task_service = TaskService()
"""
GET /api/tasks
"""
def get_queryset(self):
user = self.request.user
if user.is_superuser:
return Task.objects.all()
return Task.objects.filter(user=self.request.user)
"""
POST /api/tasks
"""
def create(self, request):
domain = request.data['domain']
can_create_task = self.task_service.can_create_task(user=request.user, domain_name=domain)
if not can_create_task:
raise PermissionDenied(code=None, detail=None)
return super().create(request)
The correct method of doing this, according to Django Rest Framework, is to override the perform_create() method in your ViewSet, which is responsible of creating an instance of a model using a serializer. You can pass additional data to the serializer:
def perform_create(self, serializer):
serializer.save(user=self.request.user)
The original definition of the perform_create() method can be found here.
You might need to completely override your ViewSet's create() method to be able to pass a modified dict to the serializer. You can just copy the original definition
Afterwards you can add the user to the request data by making a copy, modifying it, and passing it to the serializer:
def create(self, request, *args, **kwargs):
user_id = self.request.user.id
domain = request.data['domain']
new_data = request.data.copy()
new_data.update({"user": user_id})
can_create_task = self.task_service.can_create_task(user=request.user, domain_name=domain)
if not can_create_task:
raise PermissionDenied(code=None, detail=None)
# original definition of create() with new_data
serializer = self.get_serializer(data=new_data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
A simple solution to your problem would be using the "extra_kwargs" field inside your Meta class,for your case it would be something like this:
class Meta:
model = Task
fields = ['id', 'created', 'domain']
extra_kwargs = {'user': {'default': serializers.CurrentUserDefault()}}

Can i make retrieve request in django from model parameter?

I am using Django rest framework for my api. In my views.py file i am using Viewset.ModelViewset.
class SchemaViewSet(viewsets.ModelViewSet):
queryset = models.Schema.objects.all()
serializer_class = serializers.SchemaSerializer
My URL for this is : http://127.0.0.1:7000/api/schema/
THis is giving me GET and POST option. The response is something like this:
{
"id": 1,
"name": "yatharth",
"version": "1.1"
},
To Delete/Put/Patch i have to pass id which is 1 like this: http://127.0.0.1:7000/api/schema/1/.
Can i do this by name like this: http://127.0.0.1:7000/api/schema/yatharth/ instead of id.
My model.py (I can Set name to unique = True)
class Schema(models.Model):
"""Database model for Schema """
name= models.TextField()
version = models.TextField()
def __str__(self):
return self.name
I need id parameter also, so removing it isn't an option but instead of making query by id, i need to make it by name (both are unique)
what i found on django rest framework documentation
def retrieve(self, request, pk=None):
queryset = User.objects.all()
user = get_object_or_404(queryset, pk=pk)
serializer = UserSerializer(user)
return Response(serializer.data)
But don't know how to change primary key?
You can set do this by setting lookup_field = 'name' in your SchemaViewSet.
eg
class SchemaViewSet(viewsets.ModelViewSet):
queryset = models.Schema.objects.all()
serializer_class = serializers.SchemaSerializer
lookup_field = 'name'

Django Rest Framework: How to pass data to a nested Serializer and create an object only after custom validation

I have two models as:
class Book(AppModel):
title = models.CharField(max_length=255)
class Link(AppModel):
link = models.CharField(max_length=255)
class Page(AppModel):
book= models.ForeignKey("Book", related_name="pages", on_delete=models.CASCADE)
link = models.ForeignKey("Link", related_name="pages", on_delete=models.CASCADE)
page_no = models.IntegerField()
text = models.TextField()
and serializers
class LinkSerializer(serializers.ModelSerializer):
class Meta:
model = Link
fields = ['link']
class PageSerializer(serializers.ModelSerializer):
class Meta:
model = Page
fields = ('link','text','page_no')
def validate_text(self, value):
#some validation is done here.
def validate_link(self, value):
#some validation is done here.
class BookSerializer(serializers.ModelSerializer):
pages = PageSerializer(many=True)
class Meta:
model = Book
fields = ('title','pages')
#transaction.atomic
def create(self, validated_data):
pages_data= validated_data.pop('pages')
book = self.Meta.model.objects.create(**validated_data)
for page_data in pages_data:
Page.objects.create(book=book, **page_data)
return book
There is a validate_text method in PageSerializer. The create method will never call the PageSerializer and the page_data is never validated.
So I tried another approach as:
#transaction.atomic
def create(self, validated_data):
pages_data = validated_data.pop('pages')
book = self.Meta.model.objects.create(**validated_data)
for page_data in pages_data:
page = Page(book=book)
page_serializer = PageSerializer(page, data = page_data)
if page_serializer.is_valid():
page_serializer.save()
else:
raise serializers.ValidationError(page_serializer.errors)
return book
Posted data:
{
"title": "Book Title",
"pages": [
{
"link": 1, "page_no": 52, "text": "sometext"
}
]
}
But the above approach throws error:
{
"link": [
"Incorrect type. Expected pk value, received Link."
]
}
I also found why this error is caused: Though I am posting data with pk value 1 of a Link, the data when passed to the PageSerializer from the BookSerializer appears as such: {"link": "/go_to_link/", "page_no":52, "text": "sometext"}
Why is an instance of Link passed to the PageSerializer whereas what I sent is pk of Link?
To validate a nested object using a nested serializer:
#transaction.atomic
def create(self, validated_data):
pages_data = validated_data.pop('pages') #pages data of a book
book= self.Meta.model.objects.create(**validated_data)
for page_data in pages_data:
page = Page(book=book)
page_serializer = PageSerializer(page, data = page_data)
if page_serializer.is_valid(): #PageSerializer does the validation
page_serializer.save()
else:
raise serializers.ValidationError(page_serializer.errors) #throws errors if any
return book
Suppose you send the data as:
{
"title": "Book Title",
"pages": [{
"link":2,#<= this one here
"page_no":52,
"text":"sometext"}]
}
In the above data we are sending an id of the Link object. But in the create method of the BookSerializer defined above, the data we sent changes to:
{
"title": "Book Title",
"pages": [{
"link":Link Object (2),#<= changed to the Link object with id 2
"page_no":52,
"text":"sometext"}]
}
And the PageSerializer is actually meant to receive an pk value of the link i.e, "link": 2 instead of "link":Link Object (2). Hence it throws error:
{
"link": [
"Incorrect type. Expected pk value, received Link."
]
}
So the workaround is to override the to_internal_value method of the nested serializer to convert the received Link Object (2) object to its pk value.
So your PageSerializer class should then be:
class PageSerializer(serializers.ModelSerializer):
class Meta:
model = Page
fields = ('link','text','page_no')
def to_internal_value(self, data):
link_data = data.get("link")
if isinstance(link_data, Link): #if object is received
data["link"] = link_data.pk # change to its pk value
obj = super(PageSerializer, self).to_internal_value(data)
return obj
def validate_text(self, value):
#some validation is done here.
def validate_link(self, value):
#some validation is done here.
and the parent serializer:
class BookSerializer(serializers.ModelSerializer):
pages = PageSerializer(many=True)
class Meta:
model = Book
fields = ('title','pages')
#transaction.atomic
def create(self, validated_data):
pages_data = validated_data.pop('pages')#pages data of a book
book= self.Meta.model.objects.create(**validated_data)
for page_data in pages_data:
page = Page(book=book)
page_serializer = PageSerializer(page, data = page_data)
if page_serializer.is_valid(): #PageSerializer does the validation
page_serializer.save()
else:
raise serializers.ValidationError(page_serializer.errors) #throws errors if any
return book
That should allow the nested serializer to do the validation instead of writing validation inside the create method of the parent serializer and violating DRY.
When you call serializer.is_valid(raise_exception=True/False) it automatically call validate functions of nested serializer. When you call serializer.save(**kwargs) serializer passes validated data into your create(self, validated_data) or update(self, instance, validated_data) functions of serializer. Moreover, in validated data your ForeignKey fields returned an object.
def create(self, validated_data):
pages_data = validated_data.pop('pages') # [{'link': Linkobject, ...}]
book= self.Meta.model.objects.create(**validated_data)
for page_data in pages_data:
page = Page(book=book)
page_serializer = PageSerializer(page, data = page_data) # here you are sending object to validation again
if page_serializer.is_valid():
page_serializer.save()
else:
raise serializers.ValidationError(page_serializer.errors)
return book

Django models default error messages into one field

This very simple django restframework code.
models.py
class User(models.Model)
Email = models.CharField(max_length=100)
Username = models.CharField(max_length=100)
State = models.CharField(max_length=100)
serializers.py
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('Email','Username','State')
views.py
class UserList(generics.ListCreateAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
If use this I am getting error out put like this
{
"Email": [
"This field may not be blank."
],
"Username": [
"This field may not be blank."
],
"Country": [
"This field may not be blank."
],
}
But I need to change the error out like this.How I can i archive this and any suggestion greatly appreciated.
{"error":
[
"Email is required",
"Username is required",
"County is required"
]
}
You can always overwrite the create/update methods from the generic views. It would be something like:
class UserList(generics.ListCreateAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
if not serializer.is_valid(raise_exception=False):
# TODO: add here your custom error dict using serializer.errors
return Response({"error":...}, status=...)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
Or... you could try to overwrite the serializers... if you don't want to overwrite the view.
(However, there must be a good reason for a JS developer not being able to parse a simple json error object :P)
Hope this helps
You can define your own error messages for any error case:
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('Email','Username','State')
def __init__(self, *args, **kwargs):
super(UserSerializer, self).__init__(*args, **kwargs)
for field in self.Meta.fields:
self.fields[field].error_messages['required'] = "%s is required" % field