I have 2 models - Module and Room. A module can have zero or multiple rooms and a room can be added into multiple modules. So, there is a simple many-to-many relationship between them.
In post request, raw-data input works, but not form-data.
module/models.py -
class Module(models.Model):
module_id = models.AutoField(primary_key=True)
title = models.CharField(max_length=100)
desc = models.TextField()
rooms = models.ManyToManyField(Rooms, blank=True)
room_list = models.CharField(max_length = 100, blank=True)
rooms/models.py -
class Rooms(models.Model):
room_id = models.AutoField(primary_key=True)
title = models.CharField(max_length=100)
desc = models.TextField()
level = models.CharField(max_length=100)
module/serializers.py -
class ModuleSerializer(serializers.ModelSerializer):
rooms = RoomSerializerWrite(many=True)
class Meta:
model = Module
fields = '__all__'
def create(self, validated_data):
rooms_data = validated_data.pop('rooms')
module = Module.objects.create(**validated_data)
for data in rooms_data:
room = Rooms.objects.get(**data)
module.rooms.add(room)
return module
def update(self, instance, validated_data):
# Updating rooms
rooms_data = validated_data.get('rooms')
instance.rooms.clear()
for room_data in rooms_data:
room = Rooms.objects.get(**room_data)
instance.rooms.add(room)
# Updating other fields
fields = [
'title',
'desc',
'thumbnail',
'is_deleted',
]
for field in fields:
setattr(instance, field, validated_data[field])
instance.save()
return instance
rooms/serialier.py -
class RoomSerialize(serializers.ModelSerializer):
room_id = serializers.IntegerField()
class Meta:
model = Rooms
fields = "__all__"
module/views.py -
class add_module(APIView):
def post(self, request, format=None):
# Adding the rooms to module from room_list
new_request = request.data.copy()
room_list=[]
if 'room_list' in new_request:
room_list_data = list(new_request['room_list'].split(" "))
for room in room_list_data:
room_object = Rooms.objects.get(room_id=room)
room_serializer = RoomSerializer(room_object)
room_list.append(room_serializer.data)
new_request.update({'rooms':room_list})
# creating the module
module_serializer = ModuleSerializer(data=new_request)
if module_serializer.is_valid():
module_serializer.save()
return Response(module_serializer.data['module_id'])
return Response(module_serializer.errors)
POST request body for updating a module in POSTMAN -
{
"module_id": 2,
"room_list": "1 2",
"title": "4",
"desc": "22",
}
Pls notice that while taking input of ManyToMany field - "rooms", I'm taking a string "room_list" as input that contains all the room_ids to be included.
This works perfectly fine when I take input as raw-data in postman, but when I use form-data, it shows -
{
"rooms": [
"This field is required."
]
}
What to do?
Nested serializers don't work with multipart/form-data.
Please refer to the following issues:
https://github.com/encode/django-rest-framework/issues/7650
https://github.com/encode/django-rest-framework/issues/7262
Related
I have 2 models - Module and Room. A module can have zero or multiple rooms and a room can be added into multiple modules. So, there is a simple many-to-many relationship between them.
When I use post request, raw-data works, but not form-data.
module/models.py -
class Module(models.Model):
module_id = models.AutoField(primary_key=True)
title = models.CharField(max_length=100)
desc = models.TextField()
room_list = models.CharField(max_length = 100, blank=True)
rooms = models.ManyToManyField(Rooms, blank=True)
rooms/models.py -
class Rooms(models.Model):
room_id = models.AutoField(primary_key=True)
title = models.CharField(max_length=100)
level = models.CharField(max_length=100)
desc = models.TextField()
module/serializers.py -
class ModuleSerializer(serializers.ModelSerializer):
rooms = RoomSerializer(many=True)
class Meta:
model = Module
fields = '__all__'
def create(self, validated_data):
rooms_data = validated_data.pop('rooms')
module = Module.objects.create(**validated_data)
for data in rooms_data:
room = Rooms.objects.get(**data)
module.rooms.add(room)
return module
def update(self, instance, validated_data):
# Updating rooms
rooms_data = validated_data.get('rooms')
instance.rooms.clear()
for room_data in rooms_data:
room = Rooms.objects.get(**room_data)
instance.rooms.add(room)
# Updating other fields
fields = [
'title',
'desc',
'thumbnail',
'is_deleted',
]
for field in fields:
setattr(instance, field, validated_data[field])
instance.save()
return instance
rooms/serialier.py -
class RoomSerialize(serializers.ModelSerializer):
room_id = serializers.IntegerField()
class Meta:
model = Rooms
fields = "__all__"
module/views.py -
class add_module(APIView):
def post(self, request, format=None):
# Adding the rooms to module from room_list
new_request = request.data.copy()
room_list=[]
if 'room_list' in new_request:
room_list_data = list(new_request['room_list'].split(" "))
for room in room_list_data:
room_object = Rooms.objects.get(room_id=room)
room_serializer = RoomSerializer(room_object)
room_list.append(room_serializer.data)
new_request.update({'rooms':room_list})
# creating the module
module_serializer = ModuleSerializer(data=new_request)
if module_serializer.is_valid():
module_serializer.save()
return Response(module_serializer.data['module_id'])
return Response(module_serializer.errors)
POST request body for updating a module in POSTMAN -
{
"module_id": 2,
"room_list": "1 2",
"title": "4",
"desc": "22",
}
Pls notice that while taking input of ManyToMany field - "rooms", I'm taking a string "room_list" as input that contains all the room_ids to be included. This works perfectly fine when I take input as raw-data in postman, but when I use form-data, it shows
{
"rooms": [
"This field is required."
]
}
What to do?
First of all, serializer fields with (many=True) create nested objects.
If you input data with Rooms as already serialized, then it means that you create other Rooms instances. It will be shown with if module_serializer.is_valid(): part. Therefore, if you intended to implement as just link already instantiated Rooms to Module, it's better to make it as primary key list.
Here are my example codes.
class ModuleSerializer(serializers.ModelSerializer):
rooms = RoomSerializer(many=True, read_only=True)
def create(self, validated_data):
rooms_data = validated_data['room_list']
module = Module.objects.create(**validated_data)
for data in rooms_data.split(' '):
room = Rooms.objects.get(room_id=data)
module.rooms.add(room)
return module
def update(self, instance, validated_data):
# Updating rooms
rooms_data = validated_data.get('rooms')
instance.rooms.clear()
for room_data in rooms_data:
room = Rooms.objects.get(**room_data)
instance.rooms.add(room)
# Updating other fields
fields = [
'title',
'desc',
'thumbnail',
'is_deleted',
]
for field in fields:
setattr(instance, field, validated_data[field])
instance.save()
return instance
class Meta:
model = Module
fields = '__all__'
class AddModule(APIView):
def post(self, request, format=None):
# creating the module
module_serializer = ModuleSerializer(data=request.data)
if module_serializer.is_valid():
module_serializer.save()
return Response(module_serializer.data['module_id'])
return Response(module_serializer.errors)
Any other parts are same as yours. Regardless of whether data are sent by form-data or json raw data, results are same as below.
I have 2 models - Module and Room. A module can have zero or multiple rooms and a room can be added into multiple modules. So, there is a simple many-to-many relationship between them.
But when I define it in my module/models.py file, it is not taking any input as rooms. here are my files -
module/models.py -
class Module(models.Model):
module_id = models.AutoField(primary_key=True)
title = models.CharField(max_length=100)
desc = models.TextField()
rooms = models.ManyToManyField(Rooms)
rooms/models.py -
class Rooms(models.Model):
room_id = models.AutoField(primary_key=True)
title = models.CharField(max_length=100)
desc = models.TextField()
level = models.CharField(max_length=100)
is_deleted = models.BooleanField(default=False)
module/serializers.py -
class ModuleSerializer(serializers.ModelSerializer):
rooms = RoomSerializer(read_only=True, many=True)
class Meta:
model = Module
fields = "__all__"
module/views.py -
class add_module(APIView):
def post(self, request, format=None):
module_serializer = ModuleSerializer(data=request.data)
if module_serializer.is_valid():
module_serializer.save()
return Response(module_serializer.data['module_id'], status = status.HTTP_201_CREATED)
return Response(module_serializer.errors, status = status.HTTP_400_BAD_REQUEST)
POST request body for creating a module in POSTMAN -
{
"rooms": [
{
"room_id": 2,
"title": "4",
"desc": "22",
"level": "2",
}
],
"title": "4",
"desc": "22",
}
With this request, module is being created, but no room is getting added in it.
Can someone tell me why my rooms are not getting added while creating modules?
You need to write an explicit serializer create method to support writable nested representations.
https://www.django-rest-framework.org/api-guide/serializers/#writable-nested-representations
Example:
Given a RoomSerializer like this:
class RoomSerializer(serializers.ModelSerializer):
room_id = serializers.IntegerField()
class Meta:
model = models.Rooms
fields = '__all__'
We explicitly set room_id in RoomSerializer, to avoid <room_id> stripped away from the input data, and a MultipleObjectsReturned raised.
class ModuleSerializer(serializers.ModelSerializer):
rooms = RoomSerializer(many=True) # remove *read_only=True*
class Meta:
model = Module
fields = "__all__"
def create(self, validated_data):
rooms_data = validated_data.pop('rooms')
module = Module.objects.create(**validated_data)
for data in rooms_data:
room, created = Rooms.objects.get_or_create(**data)
module.rooms.add(room)
return module
I have 2 models - Rooms and Modules. A module can contain many rooms and a room can be contained by many different modules. below are the models -
Rooms model -
class Rooms(models.Model):
room_id = models.AutoField(primary_key=True)
title = models.CharField(max_length=100)
desc = models.TextField()
level = models.CharField(max_length=100)
Module model -
class Module(models.Model):
module_id = models.AutoField(primary_key=True)
title = models.CharField(max_length=100)
desc = models.TextField()
is_deleted = models.BooleanField(default=False)
rooms = models.ManyToManyField(Rooms)
Module serializer -
class ModuleSerializer(serializers.ModelSerializer):
rooms = RoomSerializer(read_only=True, many=True)
class Meta:
model = Module
fields = "__all__"
Module view.py file -
class add_module(APIView):
def post(self, request, format=None):
module_serializer = ModuleSerializer(data=request.data)
if module_serializer.is_valid():
module_serializer.save()
return Response(module_serializer.data['module_id'], status = status.HTTP_201_CREATED)
return Response("response":module_serializer.errors, status = status.HTTP_400_BAD_REQUEST)
How do I take multiple rooms as input in views.py file while creating my module object. Also if i want to test my API in postman, then how can i take multiple inputs in postman.
You can use a similar construction:
class ModuleSerializer(serializers.ModelSerializer):
rooms = RoomSerializer(read_only=True, many=True)
class Meta:
model = Module
fields = "__all__"
def create(self, validated_data):
rooms = validated_data.pop('rooms')
...
for room in rooms:
...
return Response(...)
I have 2 models - Module and Room. A module can have zero or multiple rooms and a room can be added into multiple modules. So, there is a simple many-to-many relationship between them.
While updating the modules field using a put request, I don't want to update any rooms in it, I just want to add/remove rooms in the module. Here are my files -
module/models.py -
class Module(models.Model):
module_id = models.AutoField(primary_key=True)
title = models.CharField(max_length=100)
desc = models.TextField()
rooms = models.ManyToManyField(Rooms)
rooms/models.py -
class Rooms(models.Model):
room_id = models.AutoField(primary_key=True)
title = models.CharField(max_length=100)
desc = models.TextField()
level = models.CharField(max_length=100)
is_deleted = models.BooleanField(default=False)
module/serializers.py -
class ModuleSerializer(serializers.ModelSerializer):
rooms = RoomSerializer(read_only=True, many=True)
class Meta:
model = Module
fields = "__all__"
def create(self, validated_data):
rooms_data = validated_data.pop('rooms')
module = Module.objects.create(**validated_data)
for data in rooms_data:
room, created = Rooms.objects.get_or_create(**data)
module.rooms.add(room)
return module
rooms/serialier.py -
class RoomSerializerWrite(serializers.ModelSerializer):
room_id = serializers.IntegerField()
class Meta:
model = Rooms
fields = "__all__"
module/views.py -
class add_module(APIView):
def post(self, request, format=None):
module_serializer = ModuleSerializer(data=request.data)
if module_serializer.is_valid():
module_serializer.save()
return Response(module_serializer.data['module_id'], status = status.HTTP_201_CREATED)
return Response(module_serializer.errors, status = status.HTTP_400_BAD_REQUEST)
POST request body for updating a module in POSTMAN -
{
"module_id": 2,
"rooms": [
{
"room_id": 2,
"title": "4",
"desc": "22",
"level": "2",
}
],
"title": "4",
"desc": "22",
}
Can someone help me with the update function in module/serializer?
What I understood is you don't want to update your module's rooms with your post body, actually you want to replace them. So just clear every room from m2m relationship and add new rooms from request. Something like;
def update(self, instance, validated_data):
# get new rooms
rooms_data = validated_data.get('rooms')
# clear m2m relationship
instance.rooms_set.clear()
# I assume your rooms_data is a list that contains dicts with room fields
for room_data in rooms_data:
# get room instance
room, created = Rooms.objects.get_or_create(**room_data)
# Add it to m2m relationship
instance.rooms.add(room)
instance.save()
return instance
So if you do not send existing room to your serializer, it will be removed from m2m relationship on instance.
My form triggers form_invalid when the field "category" is empty.
The weird thing is, when the view displays, the "description" field does not have the asterisk indicating it's required, unlike "name" or "enabled", for instance. Also, when I try to send the form with an empty name, it correctly displays a little yellow mark and says "This field is required", but it doesn't say that when the category is empty.
So, it seems to correctly recognize that the category is not required, it just says it's invalid after I send the form.
My form looks like this:
class ProductForm(forms.Form):
name = forms.CharField(max_length=80, required=True)
category = forms.ModelChoiceField(queryset=None, required=False, label='Categoría')
description = forms.CharField(max_length=150, required=False)
price = forms.FloatField(required=True)
image = forms.ImageField(allow_empty_file=True, required=False)
extras = forms.FileField(allow_empty_file=True, required=False)
enabled = forms.BooleanField(required=False, initial=True)
def __init__(self, user, *args, **kwargs):
self.user = user
super(ProductForm, self).__init__(*args, **kwargs)
self.fields['name'].label = 'Nombre'
self.fields['description'].label = 'Descripción'
self.fields['price'].label = 'Precio'
self.fields['image'].label = 'Imagen'
self.fields['extras'].label = 'Extras'
categories = Category.objects.filter(store=Profile.objects.get(user=user).store)
if categories.count() == 0:
self.fields['category'].required = False
self.fields['category'].queryset = categories
self.fields['enabled'].label = 'Habilitado'
It is included to my view in this way:
class ProductCreateView(LoginRequiredMixin, CreateView):
template_name = 'products/product_form.html'
model = Product
fields = ["name", "category", "description", "price", "image", "enabled", "extra"]
success_url = reverse_lazy("orders:products")
And my model looks like this:
class Product(models.Model):
store = models.ForeignKey(Store, related_name="products", on_delete=models.PROTECT)
name = models.CharField(max_length=100, verbose_name="Nombre")
description = models.CharField(max_length=500, verbose_name="Descripción", null=True)
price = models.FloatField(verbose_name="Precio")
image = models.ImageField(upload_to="media/", verbose_name="Imagen", null=True, blank=True)
enabled = models.BooleanField(default=False, verbose_name="Habilitado")
extra = models.FileField(upload_to="media/files/", verbose_name="Extras", max_length=254, null=True, blank=True)
category = models.ForeignKey(Category, on_delete=models.PROTECT, null=True)
detail_enabled = models.BooleanField(default=False)
You never use the ModelForm you constructed. Django will create its own since nowhere you specify form_class=… [Django-doc] in your CreateView. But that will not be sufficient, since Django will not pass a user by default. You will need to override the .get_form_kwargs(…) [Django-doc] as well to pass the user.
You also should make the ProductForm a ModelForm, since otherwise it has no .save() method:
class ProductForm(forms.ModelForm):
# …
class Meta:
model = Product
fields = ['name', 'category', 'description', 'price', 'image', 'enabled', 'extra']
In your view you thus specify the form_class, and override the get_form_kwargs, to inject the user in the ModelForm constructor:
class ProductCreateView(LoginRequiredMixin, CreateView):
template_name = 'products/product_form.html'
model = Product
form_class = ProductForm
success_url = reverse_lazy('orders:products')
def get_form_kwargs(self, *args, **kwargs):
fk = super().get_form_kwargs(*args, **kwargs)
fk['user'] = self.request.user
return fk