Wood Pecker
Wood Pecker

Reputation: 75

Can't represent all fields of a serialized ManyToMany through model with Django Rest Framework

I'm using Django and Django Rest Framework to represent a 'BaseRecipe' model. This model has a M2M field to represent the ingredients of that recipe. That M2M field relates a 'Product' object with the 'BaseRecipe' object and extends that two models with another field to represent the 'quantity'. I'm trying to retrieve a list of those ingredients in the BaseRecipeSerializer, but only the id's are returned.

Any ideas why?

Thank you in advance!

My models.py:


class Product(models.Model):
    name = models.CharField(_('Name'), max_length=255, help_text=_('Product name'))
    supplier = models.ForeignKey(Supplier, blank=True, related_name='supplier_products', on_delete=models.CASCADE)
    serial_no = models.CharField(_('Serial number'), max_length=50, blank=True,
                                 help_text=_('Product serial number. Max 50 characters.'))
    allergens = models.ManyToManyField(Allergen, blank=True, related_name='product_allergens')
    description = models.TextField(_('Description'), blank=True, help_text=_('Additional product information.'))
    is_vegan = models.BooleanField(_('Vegan'), default=False)
    is_halal = models.BooleanField(_('Halal'), default=False)
    format_container = models.CharField(_('Format container'), max_length=6, choices=CONTAINERS, blank=True,
                                        help_text=_('Package format'))
    format_quantity = models.DecimalField(_('Format quantity'), blank=True, null=True, max_digits=9, decimal_places=3,
                                          help_text=_('Format quantity sell'))
    format_unit = models.CharField(_('Format unit'), max_length=6, choices=UNITS, blank=True)
    quantity = models.DecimalField(_('Quantity'), blank=True, null=True, max_digits=9, decimal_places=3,
                                   help_text=_('Quantity per unit provided'))
    type = models.CharField(_('Type'), max_length=255, help_text=_('Type'), blank=True)
    unit = models.CharField(_('Unit'), max_length=6, choices=UNITS)
    unit_price = models.DecimalField(_('Unit price'), blank=True, null=True, max_digits=9, decimal_places=3)

    class Meta:
        ordering = ['name', ]

    @property
    def price(self) -> Decimal:
        return self.quantity * self.unit_price

    def __str__(self):
        return self.name


class BaseRecipe(models.Model):
    user = models.ForeignKey(User, null=True, on_delete=models.CASCADE, related_name='user_base_recipes')
    restaurant = models.ForeignKey(Restaurant, null=True, on_delete=models.SET_NULL,
                                   related_name='restaurant_base_recipes')
    title = models.CharField(_('Base recipe title'), max_length=255, help_text=_('Base recipe. Example: Chicken broth'),
                             blank=True)
    elaboration = models.TextField(_('Elaboration'), blank=True, help_text=_('Base recipe making instructions.'))
    quantity = models.DecimalField(_('Quantity'), max_digits=9, decimal_places=3,
                                   help_text=_('Quantity produced with this recipe (after cooking). '))
    unit = models.CharField(_('Unit'), max_length=6, choices=UNITS)
    expiry_date = models.DateField(_('Expiry date'), help_text=_('Last day product is safe to consume.'))
    ingredients = models.ManyToManyField(Product, through='IngredientBaseRecipe', related_name='base_recipe_ingredients')
    image = models.ImageField(_('Picture'), upload_to=get_picture_path, blank=True, null=True)
    exhausted = models.BooleanField(_('Exhausted'), default=False, help_text=_('If it\'s needed to produce more.'))
    next_batch_date = models.DateField(_('Next batch date'), blank=True,
                                       help_text=_('When it\'s necessary to prepare more of this base recipe.'))
    storage = models.CharField(_('Storage'), max_length=12, choices=STORAGE_METHODS, blank=True)

    class Meta:
        ordering = ['-id']

    @property
    def cost(self) -> Decimal:
        return sum([ingredient.cost for ingredient in self.ingredients.all()])

    @property
    def allergens(self):
        allergens_ids = IngredientBaseRecipe.objects.filter(base_recipe=self).values_list('product__allergens',
                                                                                          flat=True)
        allergens = Allergen.objects.filter(id__in=allergens_ids)
        return allergens

    def __str__(self):
        return self.title


class IngredientBaseRecipe(models.Model):
    product = models.ForeignKey(Product, on_delete=models.CASCADE)
    base_recipe = models.ForeignKey(BaseRecipe, on_delete=models.CASCADE)
    product_quantity = models.DecimalField(_('Product quantity'), max_digits=9, decimal_places=3, default=0.0)

    class Meta:
        ordering = ['-id']

    @property
    def allergens(self):
        return self.product.allergens.all()

    @property
    def cost(self) -> Decimal:
        return self.quantity * self.product.unit_price

    def __str__(self):
        return self.product.name

serializer.py:


class SimpleProductSerializer(serializers.ModelSerializer):
    class Meta:
        model = Product
        fields = ['id', 'name', ]


class IngredientBaseRecipeSerializer(serializers.ModelSerializer):
    # product_name = serializers.ReadOnlyField(source='product.name')
    product = SimpleProductSerializer(read_only=True)

    class Meta:
        model = IngredientBaseRecipe
        exclude = ['base_recipe', ]


class BaseRecipeSerializer(serializers.ModelSerializer):
    ingredients = IngredientBaseRecipeSerializer(many=True, read_only=True)
    allergens = AllergenSerializer(many=True, read_only=True)
    cost = serializers.FloatField(read_only=True)

    class Meta:
        model = BaseRecipe
        fields = '__all__'

Result:

{
    "count": 3,
    "next": null,
    "previous": null,
    "results": [
        {
            "id": 5,
            "ingredients": [
                {
                    "id": 1
                },
                {
                    "id": 3
                },
                {
                    "id": 3
                }
            ],
            "allergens": [
                {
                    "id": 9,
                    "name": "Celery",
                    "icon": null
                },
                {
                    "id": 12,
                    "name": "Sulphites",
                    "icon": null
                }
            ],
            "title": "Tipical Spanish",
            "elaboration": "No se tio.",
            "quantity": "23232.000",
            "unit": "l",
            "expiry_date": "2021-02-28",
            "image": null,
            "exhausted": false,
            "next_batch_date": "2021-03-01",
            "storage": "freezer",
            "user": 1,
            "restaurant": 1
        },
        {
            "id": 3,
            "ingredients": [],
            "allergens": [],
            "cost": 0.0,
            "title": "Chicken broth",
            "elaboration": "One, two, three.",
            "quantity": "45.000",
            "unit": "l",
            "expiry_date": "2021-02-28",
            "image": null,
            "exhausted": false,
            "next_batch_date": "2021-03-01",
            "storage": "freezer",
            "user": 1,
            "restaurant": 1
        }
    ]
}

Upvotes: 0

Views: 93

Answers (1)

Iain Shelvington
Iain Shelvington

Reputation: 32284

BaseRecipe.ingredients will return a queryset of Product instances but you are passing them to the IngredientBaseRecipeSerializer. You need to change the source for this field so that you pass the related IngredientBaseRecipe instances to this field

class BaseRecipeSerializer(serializers.ModelSerializer):
    ingredients = IngredientBaseRecipeSerializer(
        many=True,
        read_only=True,
        source='ingredientbaserecipe_set'
    )
    ...

Upvotes: 1

Related Questions