I have 2 related models and I am trying to perform an ajax 'multipart/form' post request. But it seems like data regarding the related model is not identified by the serializer for some reason. I have tried editing the 'create' method of the viewset, to understand why the data is not passed, but to no avail. I think the issue is related to json serialization, but i do not know how to fix it
Here are some things i tried:
models.py
class Dream(models.Model):
# Some fields
...
class Milestone(models.Model):
title = models.CharField(max_length=100)
user = models.ForeignKey(User, on_delete=models.CASCADE)
date = models.DateTimeField(auto_now_add=True)
description = models.TextField(max_length=500)
dream = models.ForeignKey(
Dream, on_delete=models.CASCADE, related_name='milestones')
serializers.py
class DreamSerializer(TaggitSerializer, serializers.ModelSerializer):
milestones = MilestoneSerializer(many=True, read_only=False)
class Meta:
model = Dream
fields = (..., 'milestones')
class MilestoneSerializer(serializers.ModelSerializer):
class Meta:
model = Milestone
fields = ('id', 'title', 'user', 'date', 'description', 'dream')
read_only_fields = ('user', 'dream', 'date')
views.py
class DreamViewSet(viewsets.ModelViewSet):
queryset = Dream.objects.prefetch_related('user').all()
serializer_class = DreamSerializer
permission_classes = [permissions.IsAuthenticated]
# Tried to manually override the create function to fix the error
def create(self, request, *args, **kwargs):
print(request.data) # <QueryDict: {'title': ['test'], 'image':[<InMemoryUploadedFile: somePhoto.jpg (image/jpeg)>], ... , 'milestones': ['[{"title":"test1","description":"test1"}]']}>
# seems like it is evaluating to string literal '[{"title":"test1","description":"test1"}]'
print(request.data['milestones']) # [{"title":"test1","description":"test1"}]
print(type(request.data['milestones'])) # <class 'str'>
cleaned_data = request.data.copy()
milestones = cleaned_data.pop('milestones', [])
print(milestones) # ['[{"title":"test1","description":"test1"}]'] (type list)
if len(milestones):
# tried to manually deserialize data
cleaned_data['milestones'] = json.loads(milestones[0])
milestone_serializer = MilestoneSerializer(
data=cleaned_data['milestones'], many=True)
print(cleaned_data) # <QueryDict: {'title': ['test'], 'image': [<InMemoryUploadedFile: somePhoto.jpg (image/jpeg)>], 'milestones': [[{'title': 'test1', 'description': 'test1'}]]}>
print(milestone_serializer.is_valid()) # True
print(milestone_serializer.data) # [OrderedDict([('title', 'test1'), ('description', 'test1')])]
serializer = self.get_serializer(data=cleaned_data)
if serializer.is_valid(): # False
# not executedd
print(serializer.validated_data)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=201, headers=headers)
else:
print(serializer.errors) # {'milestones': [ErrorDetail(string='This field is required.', code='required')]}
on the frontend:
async add({title, image, ..., milestones}) {
const imageFile = new File([image], `somePhoto.jpg`, {
type: image.type,
});
const formData = new FormData();
formData.append("title", title);
formData.append("image", imageFile);
....
formData.append("milestones", JSON.stringify(milestones));
await axios.post(endpoint, formData); // status: 400, statusText: 'Bad Request', milestones: ['This field is required.']
}
CodePudding user response:
It doesn't work because in the DreamSerializer, you set the milestones field as the list of milestone objects. But in frontend you set it as string.
You need to change that first.
And I think create logic should be written in the create method of the serializer.
First you define other field for writing data of the milestones.
import json
class DreamSerializer(TaggitSerializer, serializers.ModelSerializer):
milestones = MilestoneSerializer(many=True, read_only=True)
milestone_str = serializers.CharField(write_only = True)
class Meta:
model = Dream
fields = (..., 'milestones', 'milestone_str')
def create(self, validated_data):
milestone_str = validated_data.pop('milestone_str')
milestone_data = json.loads(milestone_str)
milestone_serializer = MilestoneSerializer(
data=milestone_data, many=True)
if milestone_serializer.is_valid():
# create Dream object first
dream = Dream.objects.create(**validated_data)
for milestone_item in milestone_data:
Milestone.objects.create(dream = dream, **milestone_item)
return dream
else:
raise serializers.ValidationError("invalid milestone data")
Then in views.py, you don't need to define create method.
class DreamViewSet(viewsets.ModelViewSet):
queryset = Dream.objects.prefetch_related('user').all()
serializer_class = DreamSerializer
permission_classes = [permissions.IsAuthenticated]
In frontend,
async add({title, image, ..., milestones}) {
...
formData.append("milestone_str", JSON.stringify(milestones));
...
}
Hope it could help.
