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?
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.
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.
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() .
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.
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?
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()
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With