Django rest nested serializer fails - django

My model relationships are: A 'Reader' will have a single 'Wishlist' and a 'Wishlist' will contain many 'Book's. I want to create an empty Wishlist automatically during Reader object instance creation.
My Models:
class Wishlist(models.Model):
wishlist_id = models.AutoField(primary_key=True)
class Reader(models.Model):
user = models.OneToOneField(User)
phone = models.CharField(max_length=30)
address = models.CharField(max_length=80)
dob = models.DateField(auto_now=False, auto_now_add=False, null=True, blank=True)
# A library has many readers
which_library = models.ForeignKey('Library', related_name='readers', on_delete=models.CASCADE)
wishlist = models.OneToOneField(Wishlist, null=True, blank=True)
class Book(models.Model):
book_id = models.AutoField(primary_key=True)
title = models.CharField(max_length=30)
which_wishlist = models.ForeignKey('Wishlist', related_name='my_wishlist', on_delete=models.CASCADE, null=True, blank=True)
And serializer:
class ReaderSerializer(serializers.ModelSerializer):
username = serializers.CharField(source='user.username')
email = serializers.CharField(source='user.email')
password = serializers.CharField(source='user.password')
class Meta:
model = Reader
#fields = '__all__'
#depth = 1
fields = ('id', 'username', 'email', 'password', 'phone', 'address', 'dob', 'which_library', 'wishlist')
def update(self, instance, validated_data):
instance.user.email = validated_data.get('user.email', instance.user.email)
instance.user.password = validated_data.get('user.password', instance.user.password)
instance.phone = validated_data.get('phone', instance.phone)
instance.address = validated_data.get('address', instance.address)
instance.dob = validated_data.get('dob', instance.dob)
instance.which_library = validated_data.get('which_library', instance.which_library)
instance.wishlist = validated_data.get('wishlist', instance.wishlist)
instance.save()
return instance
def create(self, validated_data):
user_data = validated_data.pop('user')
user = User.objects.create(**user_data)
user.set_password(user_data['password'])
user.save()
wishlist_data = validated_data.pop('wishlist')
reader = Reader.objects.create(**validated_data)
Wishlist.objects.create(reader=reader, **wishlist_data)
return reader
My view that handles the creation:
#api_view(['GET', 'POST'])
def reader(request, library_id):
"""
List all readers in a specific library, or create a new
"""
if request.method == 'GET':
...
elif request.method == 'POST':
serializer = ReaderSerializer(data=request.data)
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)
I am making a POST call to create a Reader with following JSON data:
{
"username": "sample",
"email": "sample#gmail.com",
"password": "012345",
"phone": "012345",
"address": "sample address",
"which_library": "2",
"wishlist": []
}
Where it gives me following error:
{
"wishlist": [
"Incorrect type. Expected pk value, received list."
]
}
What am I doing wrong?

I would throw away the Wishlist model completely as it does not hold any data.
class Book(models.Model):
book_id = models.AutoField(primary_key=True)
title = models.CharField(max_length=30)
reader = models.ManyToManyField('Reader', related_name='wishlist')
This should make the whole thing easier as you no longer need to create the wishlist automagically and I guess your problem will desolve in air.
If you need the Model (e.g. you expect data to arise later) you might use through on the ManyToManyField. Details can be found in the docs
UPDATE
I rethought the advise regarding the through model. This approach would not reflect your current state as it would have one entry per relation not per reader. Nevertheless I would recommend the simplification of your models as suggested above.

Related

ValueError: Cannot assign "1": "LeadFacilityAssign.assigned_facilities" must be a "Facility" instance

I've been trying to create an api endpoint to update my "lead" objects and add a list of facilities to them when sending a put request (each time a different amount of facilities). The lead objects already exist inside the database so do the facility objects. Since i need a date and time associated to each facility when they are being added to a lead i created the "LeadFacilityAssign" class.
Since i wasn't able to get it to work i tried to do it just with a post request for now, during the lead creation process. I was told that i need to use bulk_create if i need to add more than one facility this way. I couldn't find anything on bulk_create inside the drf documentation so i decided to do this for now just with one facility and improve my code from there one issue at a time since i'm new to drf.
Does anyone know what is causing this error? I tried a few different things but nothing worked so far.
ValueError: Cannot assign "1": "LeadFacilityAssign.assigned_facilities" must be a "Facility" instance.
serializers.py
class LeadUpdateSerializer(serializers.ModelSerializer):
is_owner = serializers.SerializerMethodField()
assigned_facilities = serializers.IntegerField(required=True)
datetime = serializers.DateTimeField(required=True)
class Meta:
model = Lead
fields = (
"id",
"first_name",
"last_name",
"assigned_facilities",
"datetime",
)
read_only_fields = ("id", "created_at", "agent", "is_owner")
def get_is_owner(self, obj):
user = self.context["request"].user
return obj.agent == user
def create(self, validated_data):
assigned_facilities = validated_data.pop("assigned_facilities")
datetime = validated_data.pop("datetime")
instance = Lead.objects.create(**validated_data)
instance.leadfacility.create(assigned_facilities=assigned_facilities,datetime=datetime)
print(instance)
return instance
models.py
class Facility(models.Model):
name = models.CharField(max_length=150, null=True, blank=False)
def __str__(self):
return self.name
class Lead(models.Model):
first_name = models.CharField(max_length=40, null=True, blank=True)
last_name = models.CharField(max_length=40, null=True, blank=True)
def __str__(self):
return f"{self.first_name} {self.last_name}"
class LeadFacilityAssign(models.Model):
assigned_facilities = models.ForeignKey(Facility, on_delete=models.CASCADE, related_name='leadfacility')
lead = models.ForeignKey(Lead, on_delete=models.CASCADE, related_name='leadfacility')
datetime = models.DateTimeField()
views.py
class LeadCreateView(CreateAPIView):
permission_classes = [IsAuthenticated, IsLeadOwner]
serializer_class = LeadUpdateSerializer
def perform_create(self, serializer):
serializer.save(agent=self.request.user)
class LeadUpdateView(UpdateAPIView):
permission_classes = [IsAuthenticated, IsLeadOwner]
serializer_class = LeadUpdateSerializer
def get_queryset(self):
return Lead.objects.all()
You are trying to add Integer value into FK field.
You have 2 options. You can change the serializer field.
assigned_facilities = serializers.PrimaryKeyRelatedField(queryset=Facility.objects.all(), required=True, write_only=True)
OR
assigned_facilities = serializers.IntegerField(required=True, write_only=True)
instance.leadfacility.create(assigned_facilities_id=assigned_facilities,datetime=datetime)
I would rather use 1 option.
Another potential solution you can apply:
class LeadUpdateSerializer(serializers.ModelSerializer):
is_owner = serializers.SerializerMethodField()
assigned_facilities = serializers.IntegerField(required=True)
datetime = serializers.DateTimeField(required=True)
class Meta:
model = Lead
fields = (
"id",
"first_name",
"last_name",
"assigned_facilities",
"datetime",
)
read_only_fields = ("id", "created_at", "agent", "is_owner")
def validate_assigned_facility(self, facility_pk)->:
assigned_facility = Facility.objects.filter(pk=facility_pk).first()
if assigned_facility:
return assigned_facility
raise ValidationError('Facility not found, provide a valid pk')
def get_is_owner(self, obj):
user = self.context["request"].user
return obj.agent == user
def create(self, validated_data):
assigned_facilities = validated_data.pop("assigned_facilities")
datetime = validated_data.pop("datetime")
instance = Lead.objects.create(**validated_data)
instance.leadfacility.create(assigned_facilities=assigned_facilities,datetime=datetime)
print(instance)
return instance
This solution is kind of big but is so flexible 'cause give you the opportunity to add more business logic around the input and the expected data in the model or datasource.

TypeError: Object of type ManyRelatedManager is not JSON serializable in django rest framework

I am trying to add some students to a teacher class using their ids as primary key but I am getting above error.
I have models of Teachers and Students like this.
class Student(TimeStampAbstractModel):
user = models.OneToOneField(User, related_name="student", on_delete=models.CASCADE)
college_name = models.CharField(max_length=255, default="", blank=True)
address = models.CharField(max_length=255, default="", blank=True)
def __str__(self):
return self.user.name
class Teacher(TimeStampAbstractModel):
user = models.OneToOneField(User, related_name="teacher", on_delete=models.CASCADE)
address = models.CharField(max_length=255, default="", blank=True)
students_in_class = models.ManyToManyField(Student,related_name="teacher")
def __str__(self):
return self.user.name
Here a teacher model can have many students in a class with thier ids. I have used an put api call to add the students to the teacher in one click.
My view:
from rest_framework import status
class AddtoClassView(APIView):
def put(self,request,pk,*args,**kwargs):
id =pk
teacher = Teacher.objects.get(id=id)
serializer = TeacherSerializer(teacher,data=request.data)
if serializer.is_valid():
serializer.save()
print("iam if")
return Response({
"message":"Student has been added to class.",
"data": serializer.data
},status=status.HTTP_200_OK)
# else:
print("iam else")
return Response(serializer.data)
My serializer:
class TeacherSerializer(serializers.ModelSerializer):
students_in_class = serializers.PrimaryKeyRelatedField(
read_only= True
)
address = serializers.CharField(required=False)
# user = serializers.PrimaryKeyRelatedField(read_only=True)
class Meta:
model = Teacher
fields = ["address","students_in_class"]
# fields = '__all__'
def update(self, instance, validated_data):
instance.address = validated_data.get("address")
instance.save()
stu = validated_data.get("students_in_class")
print(stu)
if stu is not None:
print("iam stu")
instance.students_in_class.add(stu)
instance.save()
super(TeacherSerializer,self).update(instance, validated_data)
return instance
Here I have used students_in_class as pk field ( i still havent understand when to use integarfield and when to use pk field). I know since i am adding the ids to the student_in_class field i am not supposed to use it as read_only = true, however i had to use otherwise it generates error. How to solve this? Also, i dont really know which fields to define as which in serializer class.
Updated code:
class TeacherSerializer(serializers.ModelSerializer):
# students_in_class = serializers.PrimaryKeyRelatedField(
# many = True, read_only= True
# )
students_in_class = serializers.ListField(
source="students_in_class.all",
child=serializers.PrimaryKeyRelatedField(queryset=Student.objects.all()),
)
address = serializers.CharField(required=False)
# user = serializers.PrimaryKeyRelatedField(read_only=True)
class Meta:
model = Teacher
fields = ["address","students_in_class"]
# fields = '__all__'
def update(self, instance, validated_data):
instance.address = validated_data['students_in_class']['all']
instance.save()
stu = validated_data.get("students_in_class")
print(stu)
if stu is not None:
print("iam stu")
instance.students_in_class.add(stu)
instance.save()
super(TeacherSerializer,self).update(instance, validated_data)
return instance
Since you are using m2m field, you need list of ids for students_in_class. So the solution will be something like this. (Disclaimer: Code not tested).
class TeacherSerializer(serializers.ModelSerializer):
students_in_class = serializers.ListField(
source="students_in_class.all",
child=serializers.PrimaryKeyRelatedField(queryset=Student.objects.all()),
)
Serialization error will be solved because now you have included students_in_class.all as source. You need to access the data with something like this: validated_data['students_in_class']['all']
If you want to serialize your output in different way, you could set students_in_class as write_only and override serializer representation as needed.:
class TeacherSerializer(serializers.ModelSerializer):
students_in_class = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=Student.objects.all()),
write_only=True
)
# your code
def to_representation(self, instance):
ret = super().to_representation(instance)
ret['students_in_class'] = StudentSerializer(instance.students_in_class.all(), many=True).data
return ret
The following code worked:
class TeacherSerializer(serializers.ModelSerializer):
students_in_class = serializers.PrimaryKeyRelatedField(
many = True,queryset=Student.objects.all()
)
address = serializers.CharField(required=False)
class Meta:
model = Teacher
fields = ["address","students_in_class"]
def update(self, instance, validated_data):
instance.address = validated_data.get("address")
instance.save()
stu = validated_data.pop("students_in_class")
for stus in stu:
instance.students_in_class.add(stus)
instance.save()
super(TeacherSerializer,self).update(instance, validated_data)
return instance

Django: Cannot update foreign key values in a model

When I create this model, the values are nulls.
class TestRequest(models.Model):
class Meta:
verbose_name_plural = "TestRequest"
title = models.CharField(max_length=256, null=True, blank=True)
testConfiguration = models.ForeignKey(
TestConfiguration, on_delete=models.PROTECT, null=True, blank=True)
testDescription = models.ForeignKey(
TestDescription, on_delete=models.PROTECT, null=True, blank=True)
The serializer:
class TestRequestSerializer(serializers.ModelSerializer):
class Meta:
model = TestRequest
fields = [
'id',
'title',
'testConfiguration',
'testDescription',
]
depth = 2
The view:
#api_view(['PUT'])
def TestRequestUpdate(request, pk):
testRequest = TestRequest.objects.get(id=pk)
serializer = TestRequestSerializer(instance=testRequest, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
And when I want to update them later from the front-end with this state:
id: 98
title: "title"
testConfiguration: 31
testDescription: 32
I get this response:
{
"id": 98,
"title": "title",
"testConfiguration": null,
"testDescription": null
}
Why can't I update it?
EDIT: I added my solution as an answer.
You can modify your views with the following code:
testRequest = TestRequest.objects.get(id=pk)
import json
data = json.loads(json.dumps(request.data))
if data.get('testConfiguration', None) is None and testRequest.testConfiguration:
data.update({'testConfiguration': testRequest.testConfiguration.id})
if data.get('testDescription', None) is None and testRequest.testDescription:
data.update({'testDescription': testRequest.testDescription.id})
serializer = TestRequestSerializer(instance=testRequest, data=data)
My solution was that i removed the depth value from the serializer for the POST requests and added a separate serializer with the depth value for the GET requests.

Add extra value before save serializer

My form sends data to django-rest-framework, but the form contains two fields, and I want to save 5 fields in the database, other fields I calculate on my own (they are not sent by the form). How can I add additional values before saving?
so, form send 'user' and 'comment' values, I want add 'article', 'ip_address' before save to DB
models.py
class Comments(models.Model):
article = models.ForeignKey(Articles, on_delete=models.CASCADE)
user = models.ForeignKey(CustomUser, on_delete=models.CASCADE)
comment = models.TextField(verbose_name=_('Comment'))
submit_date = models.DateTimeField(_('Created'), auto_now_add=True)
ip_address = models.CharField(_('IP address'), max_length=50)
is_public = models.BooleanField(verbose_name=_('Publish'), default=False)
serializers.py
class CommentsSerializer(serializers.ModelSerializer):
user = serializers.ReadOnlyField(source='user.first_name')
class Meta:
model = Comments
fields = ('user', 'comment')
views.py
class AddCommentViewSet(viewsets.ModelViewSet):
queryset = Comments.objects.all()
serializer_class = CommentsSerializer
You have to override create() method:
class CommentsSerializer(serializers.ModelSerializer):
user = serializers.ReadOnlyField(source='user.first_name')
class Meta:
model = Comments
fields = ('user', 'comment')
def create(self, validated_data):
new_comment = models.Comment()
new_comment.user = validated_data['user']
new_comment.comment = validated_data['comment']
new_comment.article = get_your_article_somehow()
new_comment.ip_address = get_your_ip_address_somehow()
new_comment.save()
return new_comment

Django rest ModelViewSet many-to-many create

I have my model relationships as following: A Reader will have a Wishlist and a Wishlist will have many Books:
class Reader(models.Model):
user = models.OneToOneField(User)
...
# A library has many readers
which_library = models.ForeignKey('Library', related_name='readers', on_delete=models.CASCADE)
class Book(models.Model):
book_id = models.AutoField(primary_key=True)
title = models.CharField(max_length=30)
...
# A library has many books
which_library = models.ForeignKey('Library', related_name='books', on_delete=models.CASCADE)
# Record the date whenever a new book is added, it will be helpful for showing new arrivals
when_added = models.DateTimeField(auto_now_add=True, blank=True, null= True)
reader = models.ManyToManyField('Reader', related_name='wishlist')
My serializers:
class ReaderSerializer(serializers.ModelSerializer):
username = serializers.CharField(source='user.username')
email = serializers.CharField(source='user.email')
password = serializers.CharField(source='user.password')
class Meta:
model = Reader
#fields = '__all__'
#depth = 1
fields = ('id', 'username', 'email', 'password', 'phone', 'address', 'dob', 'which_library')
def update(self, instance, validated_data):
...
instance.which_library = validated_data.get('which_library', instance.which_library)
instance.save()
return instance
def create(self, validated_data):
user_data = validated_data.pop('user')
user = User.objects.create(**user_data)
user.set_password(user_data['password'])
user.save()
reader = Reader.objects.create(user=user, **validated_data)
return reader
class BookSerializer(serializers.ModelSerializer):
wishlist = ReaderSerializer(many=True, read_only=True)
class Meta:
model = Book
fields = '__all__'
I can already perform CRUD operations with Reader, I want to now add books to a specific Reader's wishlist. My view:
class ReaderViewSet(viewsets.ModelViewSet):
serializer_class = ReaderSerializer
def get_queryset(self):
readers = Reader.objects.filter(which_library=self.kwargs.get('library_id'))
return readers
#detail_route(methods=['post'])
def wishlist(self):
return Response('OK')
URL that I hit:
router.register(r'readers/(?P<library_id>[0-9]+)', ReaderViewSet, base_name='readers')
Here I am expecting that on hitting api/readers/<library_id>/<book_id>/wishlist/addI will be able to perform
add operation to the Wishlist.
How can I achieve that?
You can use detail_route's argument url_path to change url of endpoint. Also you can add additional arguments like book_id directly to detail_routed method, so your method can look like this:
#detail_route(methods=['post'], url_path='(?P<book_id>[0-9]+)/wishlist/add')
def wishlist(self, library_id=None, book_id=None):
reader = self.request.user.reader
book = Book.objects.get(pk=book_id)
reader.wishlist.add(book)
return Response('OK')
And it should be accessible from api/readers/<library_id>/<book_id>/wishlist/add url.