I have the following models:
class MenuItem(models.Model):
class Category(models.TextChoices):
PIZZA = "pizza"
SIDE = "side"
OTHER = "other"
name = models.CharField(max_length=40)
description = models.TextField(max_length=150)
price = models.FloatField(default=0.0)
category = models.CharField(max_length=50, choices=Category.choices, default=Category.OTHER)
class Order(models.Model):
class OrderStatus(models.TextChoices):
NEW = "new"
READY = "ready"
DELIVERED = "delivered"
customer = models.CharField(max_length=50)
order_time = models.DateTimeField(auto_now_add=True)
items = models.ManyToManyField(MenuItem, through='OrderItem')
status = models.CharField(max_length=50, choices=OrderStatus.choices, default=OrderStatus.NEW)
class OrderItem(models.Model):
order = models.ForeignKey(Order, on_delete=models.CASCADE)
menu_item = models.ForeignKey(MenuItem, on_delete=models.CASCADE)
quantity = models.PositiveIntegerField()
And the following serializers:
class MenuItemSerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
name = serializers.CharField(max_length=40)
description = serializers.CharField(max_length=150)
price = serializers.FloatField(default=0.0)
category = serializers.CharField(max_length=50)
def create(self, validated_data):
return MenuItem.objects.create(**validated_data)
class OrderItemSerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
menu_item_id = serializers.PrimaryKeyRelatedField(queryset=MenuItem.objects.all(), source='menu_item', read_only=False)
quantity = serializers.IntegerField(min_value=0)
class OrderSerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
customer = serializers.CharField(max_length=50)
order_time = serializers.DateTimeField(read_only=True)
items = OrderItemSerializer(many=True)
def create(self, validated_data):
order_items_data = validated_data.pop('items')
order = Order.objects.create(**validated_data)
for order_item_data in order_items_data:
quantity = order_item_data.pop('quantity')
menu_item = order_item_data.pop('menu_item')
OrderItem.objects.create(order=order, quantity=quantity, menu_item=menu_item)
return order
And then in views:
@csrf_exempt
def order(request):
if request.method == 'POST':
data = JSONParser().parse(request)
serializer = OrderSerializer(data=data)
if serializer.is_valid():
serializer.save()
return JsonResponse(serializer.data, status=201)
print(serializer.errors)
return JsonResponse(serializer.errors, status=400)
An example request looks like:
echo -n '{"customer": "John Doe", "items": [{"menu_item_id": 1, "quantity": 2}, {"menu_item_id": 2, "quantity": 1}]}' | http POST http://127.0.0.1:8000/order
When serializer.data is called an error is thrown:
AttributeError: Got AttributeError when attempting to get a value for field
menu_item_id
on serializerOrderItemSerializer
. The serializer field might be named incorrectly and not match any attribute or key on theMenuItem
instance. Original exception text was: 'MenuItem' object has no attribute 'menu_item'.
So its gone through the serializer.save() but errors on serializer.data. Can't work out why it can't get a value menu_item_id
as it should be related to the id
on MenuItem
with the foreign key.
The issue here is that you're accidentally applying the OrderItemSerializer
to the wrong model. In OrderSerializer
the items
field is defined as an OrderItemSerializer
, but the items
field in the model refers to the MenuItem
model. The OrderItem
model is only the through-model of the M2M relationship here.
Here are the updated models and serializers that you need.
models.py
class OrderItem(models.Model):
order = models.ForeignKey(Order, related_name="order_items", on_delete=models.CASCADE)
menu_item = models.ForeignKey(MenuItem, on_delete=models.CASCADE)
quantity = models.PositiveIntegerField()
Notice that I added related_name="order_items"
to the FK field that relates it to the Order
model, so that we can reference this data model easily from the OrderSerializer
.
serializers.py
class OrderSerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
customer = serializers.CharField(max_length=50)
order_time = serializers.DateTimeField(read_only=True)
order_items = OrderItemSerializer(many=True)
def create(self, validated_data):
order_items_data = validated_data.pop('order_items')
order = Order.objects.create(**validated_data)
for order_item_data in order_items_data:
quantity = order_item_data.pop('quantity')
menu_item = order_item_data.pop('menu_item')
OrderItem.objects.create(order=order, quantity=quantity, menu_item=menu_item)
return order
I changed the name of the field referencing OrderItemSerializer
to order_items
so that the serializer is working with OrderItem
objects now (using the related_name I just configured). The serializer now expects to receive an order_items
field in the POST body, so make sure to reformat your requests. And in the first line of the create
method I made sure we're popping order_items
as well, instead of items
.
EDIT
If you want to take just the ID of the MenuItem
in the request body, but have the response include all the MenuItem
data, you can do this with a serializer.Field
. I'll set it up so that the serializer can accept input data in a different format than it outputs.
serializers.py
class MenuItemField(serializers.Field):
def to_internal_value(self, data):
return MenuItem.objects.filter(id=data).first()
def to_representation(self, value):
return MenuItemSerializer(value).data
class OrderItemSerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
menu_item = MenuItemField()
quantity = serializers.IntegerField(min_value=0)
to_internal_value
uses the data provided (the object's ID) to find the MenuItem
object when the serializer is being used to save data. But when the serializer is being used to show data, the to_representation
method is called, which uses the MenuItemSerializer
to show all the object's data.
Notice that I replaced the menu_item_id
line of OrderItemSerializer
with menu_item
, which uses the new MenuItemField
I defined. This means that your request body should use menu_item
instead of menu_item_id
.