Django REST framework: save related models in ModelViewSet
Asked Answered
R

2

15

I'm trying to figure out how to save related models using Django REST framework. In my app I have a model Recipe with 2 related models: RecipeIngredient and RecipeStep. A Recipe object MUST have at least 3 related RecipeIngredient and 3 RecipeStep. Before the introduction of the REST framework I was using a Django CreateView with two formsets and the save process was the following (follow the code from form_valid()):

def save_formsets(self, recipe):
    for f in self.get_formsets():
        f.instance = recipe
        f.save()

def save(self, form):
    with transaction.atomic():
        recipe = form.save()
        self.save_formsets(recipe)
    return recipe

def formsets_are_valid(self):
        return all(f.is_valid() for f in self.get_formsets())

def form_valid(self, form):
    try:
        if self.formsets_are_valid():
            try:
                return self.create_ajax_success_response(form)
            except IntegrityError as ie:
                return self.create_ajax_error_response(form, {'IntegrityError': ie.message})
    except ValidationError as ve:
        return self.create_ajax_error_response(form, {'ValidationError': ve.message})
    return self.create_ajax_error_response(form)

Now I have my RecipeViewSet:

class RecipeViewSet(ModelViewSet):
    serializer_class = RecipeSerializer
    queryset = Recipe.objects.all()
    permission_classes = (RecipeModelPermission, )

which uses RecipeSerializer:

class RecipeSerializer(serializers.ModelSerializer):
    class Meta:
        model = Recipe
        fields = (
            'name', 'dish_type', 'cooking_time', 'steps', 'ingredients'
        )

    ingredients = RecipeIngredientSerializer(many=True)
    steps = RecipeStepSerializer(many=True)

and these are the related serializers:

class RecipeIngredientSerializer(serializers.ModelSerializer):
    class Meta:
        model = RecipeIngredient
        fields = ('name', 'quantity', 'unit_of_measure')

class RecipeStepSerializer(serializers.ModelSerializer):
    class Meta:
        model = RecipeStep
        fields = ('description', 'photo')

Now... how I'm supposed to validate related models (RecipeIngredient and RecipeStep) and save them when RecipeViewSet's create() method is called? (is_valid() in RecipeSerializer is actually ignoring nested relationships and reporting only errors related to the main model Recipe). At the moment I tried to override the is_valid() method in RecipeSerializer, but is not so simple... any idea?

Raffo answered 11/2, 2015 at 15:55 Comment(0)
N
21

I was dealing with similiar issue this week and I found out, that django rest framework 3 actually supports nested writable serialisation (http://www.django-rest-framework.org/topics/3.0-announcement/#serializers in subchapter Writable nested serialization.)

Im not sure if nested serialisers are writable be default, so I declared them:

ingredients = RecipeIngredientSerializer(many=True, read_only=False)
steps = RecipeStepSerializer(many=True, read_only=False)

and you should rewrite your create methon inside RecipeSerializer:

class RecipeSerializer(serializers.ModelSerializer):
    ingredients = RecipeIngredientSerializer(many=True, read_only=False)
    steps = RecipeStepSerializer(many=True, read_only=False)

    class Meta:
        model = Recipe
        fields = (
            'name', 'dish_type', 'cooking_time', 'steps', 'ingredients'
        )

    def create(self, validated_data):
        ingredients_data = validated_data.pop('ingredients')
        steps_data = validated_data.pop('steps')
        recipe = Recipe.objects.create(**validated_data)
        for ingredient in ingredients_data:
            #any ingredient logic here
            Ingredient.objects.create(recipe=recipe, **ingredient)
        for step in steps_data:
            #any step logic here
            Step.objects.create(recipe=recipe, **step)
        return recipe

if this structure Step.objects.create(recipe=recipe, **step) wont work, maybe you have to select data representeng each field separatly from steps_data / ingredients_data.

This is link to my earlier (realted) question/answer on stack: How to create multiple objects (related) with one request in DRF?

Nonrepresentational answered 11/2, 2015 at 17:32 Comment(9)
Thanks for the tip, but it's not that simple... I get an empty list of ingredients in validated_dataRaffo
This could mean, that your serializer doesnt know how to create nested relationships from data you are sending to it. Are you sure you are sending data in a right format/structure?Barlow
that's actually my doubt :P I'm sending a json like: {"ingredients": [{"name": "the name", "quantity": "the quantity", "unit_of_measure": "the unit"}, ...]}... it should be fine... or not?Raffo
{ "name":"bigMac", "dish_type":"dietfood", "cooking_time":"131251321", "ingredients":[ {"name": "the name", "quantity": "the quantity", "unit_of_measure": "the unit"}, ...]}Barlow
So you send something like this and in validated data, you can find just name, dish type, but ingredients is empty OrderedList?Barlow
I was actually passing a string rather than a json (I was using Django's test client now I switched to APIClient)... it works perfeclty! thank you :)Raffo
ps. there is no need to specifiy read_only=False since it's the defaultRaffo
Do you have any answer/question for the update function implementation with similar related models like this case?Cyclo
@Matúš Bartko @daveoncode, I have a similar issue but I am storing the address as JSON data or organization. But I a getting this error "address": { "Error": [ "Invalid data. Expected a dictionary, but got list." ] }Irish
M
1

I think that I get the answer.

class RecetaSerializer(serializers.ModelSerializer):

    ingredientes = IngredientesSerializer(many=True, partial=True)
    autor = serializers.PrimaryKeyRelatedField(queryset=User.objects.all())
    depth = 2

    class Meta:
        model = Receta
        fields = ('url','pk','nombre','foto','sabias_que','ingredientes','pasos','fecha_publicacion','autor')   

    def to_internal_value(self,data):

        data["fecha_publicacion"] = timezone.now()
        ingredientes_data = data["ingredientes"]
      
        for ingrediente in ingredientes_data:
                       
            alimento_data = ingrediente["alimento"]
                     
            if Alimento.objects.filter(codigo = alimento_data['codigo']).exists():

                alimento = Alimento.objects.get(codigo= alimento_data['codigo'])              
                ingrediente["alimento"] = alimento

            else:
                alimento = Alimento(codigo = alimento_data['codigo'], nombre = alimento_data['nombre'])
                alimento.save()                
                ingrediente["alimento"] = alimento
        data["ingredientes"] = ingredientes_data
        return data

    def create(self, validated_data):
              
        ingredientes_data = validated_data.pop('ingredientes')
        
        receta_data = validated_data
        usuario = User.objects.get(id = validated_data["autor"])
        receta_data['autor'] = usuario
       
        receta = Receta.objects.create(**validated_data)
        
        
        for ingrediente in ingredientes_data:
            
            alimento_data = ingrediente["alimento"]
            ingrediente = Ingredientes(receta= receta, cantidad = ingrediente['cantidad'], unidad = ingrediente['unidad'], alimento = alimento_data)
            ingrediente.save()
        
        receta.save()
            
            
        return receta

It's important to override to_internal_value(). I had problems with the function is_valid(). So every change make in the function to_internal_value() is before the function is_valid()

Megalo answered 5/9, 2016 at 8:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.