55

I'm writing a recipe organizer as a sample project for a class. I'm not very experienced with DRF other than using some very basic functionality. Here's the objective:

Create a new Recipe with associated Ingredients. Create the Ingredient objects at the same time as creating the Recipe object.

models.py:

class Ingredient(models.Model):
    name = models.CharField(max_length=100)

    def __str__(self):
        return self.name


class Recipe(models.Model):
    name = models.CharField(max_length=100)
    description = models.TextField(blank=True, null=True, help_text="This is a quick description of your recipe")
    directions = models.TextField(help_text="How to make the recipe")
    ingredients = models.ManyToManyField(Ingredient)

    def __str__(self):
        return self.name


serializers.py

class IngredientSerializer(serializers.ModelSerializer):

    class Meta:
        model = Ingredient


class RecipeSerializer(serializers.ModelSerializer):
    ingredients = IngredientSerializer(many=True)

    class Meta:
        model = Recipe

    def create(self, validated_data):
        ingredients_data = validated_data.pop('ingredients')
        recipe = Recipe.objects.create(**validated_data)
        for ingredient_data in ingredients_data:
            Ingredient.objects.create(**ingredient_data)
        return recipe

This successfully creates the Recipe object AND the Ingredients objects in the database, but doesn't associate the list of Ingredients with the Recipe. I assume this is because when I run ingredients_data = validated_data.pop('ingredients'), the validated_data dictionary gets its Ingredients removed, so when I create a new Recipe using validated_data, there aren't associated ingredients.

However I can't seem to figure out a way to keep ingredients associated with the recipe.

2 Answers 2

55

I figured out that ManyToMany relationships can't be established until all of the uncreated objects have been created. (See the Django Docs page on many-to-many relationships.)

Here's the working code:

serializers.py

class RecipeSerializer(serializers.ModelSerializer):
    ingredients = IngredientSerializer(many=True)

    class Meta:
        model = Recipe

    def create(self, validated_data):
        ingredients_data = validated_data.pop('ingredients')
        recipe = Recipe.objects.create(**validated_data)

        for ingredient in ingredients_data:
            ingredient, created = Ingredient.objects.get_or_create(name=ingredient['name'])
            recipe.ingredients.add(ingredient)
        return recipe

UPDATE:

Per request of @StevePiercy, below is my update() code. However, I haven't looked at this in years and have no idea whatsoever if it is correct or good. I haven't been working in Python or Django for some time now, so take this with a grain of salt:

def update(self, instance, validated_data):
    ingredients_data = validated_data.pop('ingredients')

    instance.name = validated_data.get('name', instance.name)
    instance.description = validated_data.get('description', instance.description)
    instance.directions = validated_data.get('directions', instance.directions)
    instance.photo = validated_data.get('photo', instance.photo)

    ingredients_list = []

    for ingredient in ingredients_data:
        ingredient, created = Ingredient.objects.get_or_create(name=ingredient["name"])
        ingredients_list.append(ingredient)

    instance.ingredients = ingredients_list
    instance.save()
    return instance
Sign up to request clarification or add additional context in comments.

5 Comments

What do you do with ingredients_list?
@iankit Whoops, I realized I had pasted in a snippet from my update() method instead. I've fixed it to reflect the correct code. Thanks!
I am noticing when I try to do something similar to this, I am getting "Lists are not currently supported in HTML input." on the browsable API. Any tips on how to work around this?
@bobbyz would you please be so kind to share your update() method, too?
Above code will work only if the name field is unique
14

Below is a helpful example for this question.

Change that part of code like this:

def create(self, validated_data):
    ingredients_data = validated_data.pop('ingredients')
    recipe = Recipe.objects.create(**validated_data)

    for ingredient in ingredients_data:
        ingredient, created = Ingredient.objects.get_or_create(name=ingredient['name'])
        recipe.ingredients.add(ingredient)
    return recipe

And this is the method to edit, cause an error when you want to edit.

def update(self, instance, validated_data):
    ingredients_data = validated_data.pop('ingredients')
    instance.name = validated_data['name']
    instance.description = validated_data['description']
    instance.directions = validated_data['directions']

    for ingredient in ingredients_data:
        ingredient, created = Ingredient.objects.get_or_create(name=ingredient['name'])
        recipe.ingredients.add(ingredient)
    return instance

Here is a link with an example. This code is similar to another answer, but if you want to try the code without any problems, you can use this repo. Good luck! DRF Nested serializers

2 Comments

How did this get down voted when the code looks the same as the accepted answer?
I would suggest this small change for robustness: ingredient, created = ingredient.objects.get_or_create(**ingredients_data) That way the dictionary is passed as a **kwargs to the constructor, thus abstracting the actual class structure. Otherwise, this answer helped me a lot! Thanks!

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.