Django ManyToManyField with many same IDs

Viewed 294

I'm trying to make a simple pizza order app in Django. I have 3 models (Toppings, Pizzas, Orders). In Orders model have ManyToManyField to Pizza. It's working fine if "user" orders one each pizza (Margarita and Pepperoni for example) but if order 2 Margarita in POST request I got only one Margarita ID in my result. How i can pass n-pizzas in one Order?

My models looks like this:

class Toppings(models.Model):
    name = models.CharField(blank=False, max_length=100, unique=True)

    class Meta:
        ordering = ['name']

    def __str__(self):
        return self.name


class Pizza(models.Model):
    name = models.CharField(blank=False, max_length=100, unique=True)
    ingredients = models.ManyToManyField(Toppings, blank=False, null=False)

    class Meta:
        ordering = ['name']

    def __str__(self):
        return self.name


class PizzaOrder(models.Model):
    created = models.DateTimeField(auto_now_add=True)
    items = models.ManyToManyField(Pizza, blank=False, null=False)
    status = models.CharField(max_length=1, choices=[
        (1, 'placed'),
        (2, 'approved'),
        (3, 'cooked'),
        (4, 'delivered'),
        (5, 'canceled')
    ], default=1)

    class Meta:
        ordering = ['created']

    def __str__(self):
        return self.created

I send POST with this data:

{
    "items": [1, 1, 1, 2, 2],
    "status": 1
}

and got this only 1 and 2 in items list (not 1,1,1,2,2):

{
    "id": 2,
    "items": [
        1,
        2
    ],
    "status": 1
}

Serializer and view for Order

class OrdersSerializer(serializers.ModelSerializer):

    class Meta:
        model = PizzaOrder
        fields = ['id', 'items', 'status', 'created']


class PizzaOrdersList(ModelViewSet):
    queryset = PizzaOrder.objects.all()
    serializer_class = OrdersSerializer
1 Answers

A through model is perfect for this. ManyToManyFields create this kind of database relationship in the background, which only creates unique combinations of Pizza and PizzaOrder, but you can easily implement it yourself.

Simply create a class called PizzaOrderItem, which contains the ForeignKeys to both Pizza and PizzaOrder models:

class PizzaOrderItem(models.Model):
    pizza = ForeignKey(Pizza)
    pizza_order = ForeignKey(PizzaOrder)

You can then change the ManyToManyField to use your custom created through model:

class PizzaOrder(models.Model):
    ...
    items = models.ManyToManyField(Pizza, through='<your_app_name>.PizzaOrderItem',
        blank=False, 
        null=False)
    ...

You could even add extra fields to your through model like toppings or quantity, like this:

class PizzaOrderItem(models.Model):
        pizza = ForeignKey(Pizza)
        pizza_order = ForeignKey(PizzaOrder)

        toppings = ManyToManyField(Toppings)
        quantity = IntegerField(default=1)

You can access the pizzas in an order through order.items and the actual through model objects through order.pizza_order_item_set.


Serializers

Retrieving the items for an order is pretty simple. To properly show your new PizzaOrderItems, you'll need to do add a serializer, and set items on PizzaOrderSerializer to use your new serializer:

class PizzaOrderItemSerializer(serializer.ModelSerializer):
    class Meta:
        model = PizzaOrderItem
        # If you add a field like quantity you can add them to the fields list below.
        fields = ['pizza', 'pizza_order']


class PizzaOrderSerializer(serializer.ModelSerializer):
    items = PizzaOrderItemSerializer(source='pizza_order_item_set', many=True)

    class Meta:
        model = PizzaOrder
        fields = ['id', 'items', 'status', 'created']

Creating PizzaOrderItems

There are two ways to create PizzaOrderItems. The first one is pretty straightforward. Simply create a view similar to PizzaOrder, and use the PizzaOrderItemSerializer:

class PizzaOrderItemViewSet(generics.ListCreateAPIView):
    queryset = PizzaOrderItem.objects.all()
    serializer_class = PizzaOrderItemSerializer

You can then create items for an order using a POST data:

{
     "pizza" : <pizza_id_here>
     "pizza_order" : <pizza_order_id_here>
}

The second way is to only use the PizzaOrder view, and override the create functions to the PizzaOrder serializer. This is because a nested serializer is read-only by default, so what we do here is override the create method for the root serializer, and create sub-objects using the nested serializer's validated data.

class PizzaOrderSerializer(serializer.ModelSerializer):
    ...

    def create(self, validated_data):
        items = validated_data.pop('items')
        order = PizzaOrder.objects.create(**validated_data)
        for item_data in items:
            PizzaOrderItem.objects.create(pizza_order=order, **item_data)
        return order

If you use this method, you can create order with POST data this:

{
    "items": [
                  { "pizza" : 1 },
                  { "pizza" : 1 },
                  { "pizza" : 2 },
                  { "pizza" : 2 },
             ]
    "status": 1
}
Related