Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django REST framework: save related models in ModelViewSet

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?

like image 505
daveoncode Avatar asked Feb 11 '15 15:02

daveoncode


People also ask

How do you pass extra context data to Serializers in Django REST framework?

In function based views we can pass extra context to serializer with "context" parameter with a dictionary. To access the extra context data inside the serializer we can simply access it with "self. context". From example, to get "exclude_email_list" we just used code 'exclude_email_list = self.

What is difference between APIView and ViewSet?

APIView allow us to define functions that match standard HTTP methods like GET, POST, PUT, PATCH, etc. Viewsets allow us to define functions that match to common API object actions like : LIST, CREATE, RETRIEVE, UPDATE, etc.

What is Viewsets Modelviewset?

A ViewSet class is simply a type of class-based View, that does not provide any method handlers such as .get() or .post() , and instead provides actions such as .list() and .create() .

What is the difference between ModelSerializer and HyperlinkedModelSerializer?

The HyperlinkedModelSerializer class is similar to the ModelSerializer class except that it uses hyperlinks to represent relationships, rather than primary keys. By default the serializer will include a url field instead of a primary key field.


2 Answers

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?

like image 187
Matúš Bartko Avatar answered Sep 20 '22 21:09

Matúš Bartko


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()

like image 40
Sonia Avatar answered Sep 23 '22 21:09

Sonia