Serializer for nested objects on POST query - python

Im new to DRF and i have problem with a nested serializer. I cannot save/create the list of ingredients in recipe.
I'll start with model.Recipe that has a attribute ingredients
ingredients = models.ManyToManyField(Ingredient,
through='IngredientInRecipe',
blank=True)
model.IngredientInRecipe has the following attributes (i need through='IngredientInRecipe' because of the "amount" field)
class IngredientInRecipe(models.Model):
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
ingredient = models.ForeignKey(Ingredient, on_delete=models.SET_NULL, null=True)
amount = models.DecimalField(max_digits=6,
decimal_places=1,
validators=[MinValueValidator(1)]
)
When sending a POST query to /recipes/ with data, i get the following errror
{
"ingredients": [
{
"id": 1,
"amount": 10
}
],
"tags": [
1,
2
],
"name": "Recipe 1",
"text": "Recipe 1",
"cooking_time": 1
}
TypeError at /api/recipes/
Field 'id' expected a number but got (<IngredientInRecipe: IngredientInRecipe object (14)>, True).
the RecipeSerializer looks like below and debug show that the problem is in
recipe_instance.ingredients.add(ingredient_in_recipe_instance)
class RecipeSerializer(serializers.ModelSerializer):
"""Serializer for recipe objects"""
author = UserSerializer(required=False)
name = serializers.CharField(max_length=200, required=True)
image = serializers.ImageField(required=False)
ingredients = IngredientInRecipeSerializer(many=True)
tags = serializers.PrimaryKeyRelatedField(
many=True,
queryset=Tag.objects.all()
)
def create(self, validated_data):
tags_data = validated_data.pop('tags')
ingredients_data = validated_data.pop('ingredients')
recipe_instance = Recipe.objects.create(author=self.context['request'].user, **validated_data)
for tag in tags_data:
recipe_instance.tags.add(tag.id)
for ingredient in ingredients_data:
ingredient_instance = get_object_or_404(Ingredient,id=ingredient['id'])
ingredient_in_recipe_instance = IngredientInRecipe.objects.get_or_create(ingredient=ingredient_instance,
amount=ingredient['amount'],
recipe = recipe_instance)
recipe_instance.ingredients.add(ingredient_in_recipe_instance)
return recipe_instance
IngredientInRecipeSerializer - any thoughts would be greate
class IngredientInRecipeSerializer(serializers.ModelSerializer):
"""Serializer for ingredient in recipe objects"""
id = serializers.IntegerField()
amount = serializers.DecimalField(max_digits=6,
decimal_places=1)
name = serializers.StringRelatedField()
measurement_unit = serializers.StringRelatedField()
class Meta:
model = IngredientInRecipe
fields = (
'id',
'amount',
'name',
'measurement_unit',
)

IngredientInRecipe.objects.get_or_create returns a tuple of (object, created), not the object itself

Related

validated_data dropping some fields during POST requests on validation in .create(): nested serializer

: purchased_products = validated_data.pop("products") KeyError: 'products'
I have a M2M relationship between Product and Purchase. What I am trying to achieve is when a purchase is made, to also fill the PurchasedProduct(the through model) model. But every time I send the data to the API and I try to access the products key in the serializer from the validated_data a keyError exception is thrown but if I return the validated_data for the purpose of debugging the product key is part of the response.
djangorestframework==3.11.0
django==2.2.10
class Product(Model):
name = CharField(max_length=20, unique=True)
date_added = DateTimeField(default=now)
models.py
class Purchase(Model):
manager = ForeignKey('users.User', on_delete=PROTECT, related_name='purchases')
quantity = DecimalField(max_digits=6, decimal_places=2)
products = ManyToManyField('branches.Product', through='PurchasedProduct',
through_fields=('purchase', 'product'))
amount_fc = IntegerField(default=0)
amount_usd = IntegerField(default=0)
total_amount = IntegerField(default=0)
date_purchased = DateTimeField(default=now)
class PurchasedProduct(Model):
purchase = ForeignKey('Purchase', on_delete=CASCADE, related_name="to_products", blank=True)
product = ForeignKey('branches.Product', on_delete=CASCADE, related_name='purchases')
unit_price = DecimalField(max_digits=12, decimal_places=4, default=0.00)
quantity = DecimalField(max_digits=5, decimal_places=2)
amount_fc = DecimalField(max_digits=10, decimal_places=2)
date_purchased = DateTimeField(default=now)
serializer.py
class PurchasedProductSerializer(ModelSerializer):
class Meta:
model = PurchasedProduct
fields = [
"id",
"purchase",
"product",
"unit_price",
"quantity",
"amount_fc",
"date_purchased"
]
class PurchaseSerializer(ModelSerializer):
# https://github.com/encode/django-rest-framework/issues/5403
products = PurchasedProductSerializer(source="to_products", many=True)
class Meta:
model = Purchase
fields = [
"id",
"manager",
"quantity",
"amount_fc",
"amount_usd",
"total_amount",
"products",
"date_purchased"
]
def create(self, validated_data):
purchased_products = validated_data.pop("products")
manager = validated_data.pop('manager')
quantity = validated_data.pop('quantity')
amount_fc = validated_data.pop('amount_fc')
amount_usd = validated_data.pop('amount_usd')
total_amount = validated_data.pop('total_amount')
purchase = Purchase.objects.create(
manager=manager,
quantity=quantity,
amount_fc=amount_fc,
amount_usd=amount_usd,
total_amount=total_amount
)
for purchased_product in purchased_products:
product = Product.objects.get(pk=purchased_product.pop("product"))
purchase.products.add(product, through_default=purchased_product)
return purchase
view.py
class PurchasesListView(ListCreateAPIView):
queryset = Purchase.objects.all()
serializer_class = PurchaseSerializer
permission_classes = [AllowAny]
filterset_fields = ['date_purchased', 'manager']
data
{
"amount_fc": 13303340.0,
"amount_usd": 1500,
"manager": 2,
"quantity": 100,
"total_amount": 1230945,
"products": [
{
"amount_fc": 1200334,
"product": 8,
"quantity": 120,
"unit_price": 10003.34
},
{
"amount_fc": 1600334,
"product": 6,
"quantity": 100,
"unit_price": 16003.34
}
]
}
Error:
purchased_products = validated_data.pop("products") KeyError: 'products'
But when I change purchased_products = validated_data.pop("products") to purchased_products = validated_data.pop("products", []) It works but it does not fill the through model(PurchasedProduct)
What I have tried
DRF example
DRF doc exampe
drf-writable-nested
drf-writable-nested does not support M2M with a through model
removing source
products = PurchasedProductSerializer(many=True) added write_only=True read_only=False. I have also tried to suppress the uniqueValidator on the Product model but still does not work.
After a bit of googling plus some try and error. I found two ways of solving this problem.
the first one is to get the key directly from self.context.
purchased_products = self.context['request'].data['products']
and it works fine. But I still don't understand why when trying to get products from validated_data it's throwing KeyError.
the second option is to use drf-nested.
shout out to the team.🎊🎊🎉🎉
the package provides an implementation of .create() and .update() and many more for your serializer and the most important thing is they support M2M relationships with a through model. their implementation examples

Add extra field in response output in DRF 3.0

I have the following models
class Restaurant(models.Model):
name_of_the_restaurant = models.CharField(max_length=30, blank=True)
opening_time = models.TimeField(auto_now=False, auto_now_add=False)
closing_time = models.TimeField(auto_now=False, auto_now_add=False)
And
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
bio = models.TextField(max_length=500, blank=True)
city = models.CharField(max_length=30, blank=True)
country = models.CharField(max_length=30, blank=True)
postal_code = models.CharField(max_length=30, blank=True)
birth_date = models.DateField(null=True, blank=True)
favourite_restaurant = models.ManyToManyField(Restaurant,
blank=True,
related_name='favourite_restaurant',
related_query_name='favourite_restaurant')
I have defined a serializer for Restaurant model which is mainly :
class RestaurantSerializer(serializers.ModelSerializer):
class Meta:
model = Restaurant
fields = '__all__'
Now in my ViewSet logic I am doing the following :
class RestaurantListView(generics.ListAPIView):
serializer_class = RestaurantSerializer
def get_queryset(self):
queryset = {'Error': 'Please pass valid url parameters'}
city = self.request.query_params.get('city', None)
postal_code = self.request.query_params.get('postalcode', None)
country = self.request.query_params.get('country', None)
if city is not None or postal_code is not None:
queryset = Restaurant.objects.filter(
Q(city=city) | Q(pincode=postal_code))
if country and city is not None and postal_code is None:
queryset = Restaurant.objects.filter(country=country, city=city)
return queryset
def get(self, request, format=None):
restaurant_qs = self.get_queryset()
ids_list = [restaurant.id for restaurant in restaurant_qs]
favourite_restaurant = is_favourite_restaurant(ids_list, self.request.user)
serializer = RestaurantSerializer(restaurant_qs, many=True)
return Response(serializer.data)
where is_favourite_restaurant is a custom function function which returns queryset of FAVOURITE restaurant(s) of a user. Now in the output for this GET request I am getting result as :
[
{
"id": 2,
"name_of_the_restaurant": "Aniket",
"opening_time": "14:08:33.413402",
"closing_time": "22:08:33.413414"
},
{
"id": 3,
"name_of_the_restaurant": "Aniket-1",
"opening_time": "14:13:37.656385",
"closing_time": "22:13:37.656397"
}
]
Whereas the desired output I want is to append an extra field is_favourite:true to that restaurant which user has previously marked favourite. And hence the output should be
[
{
"id": 2,
"name_of_the_restaurant": "Aniket",
"opening_time": "14:08:33.413402",
"closing_time": "22:08:33.413414",
"is_favourite": true,
},
{
"id": 3,
"name_of_the_restaurant": "Aniket-1",
"opening_time": "14:13:37.656385",
"closing_time": "22:13:37.656397"
}
]
EDIT :
Definition of is_favourite_restaurant function :
def is_favourite_restaurant(restaurant_qs, user):
favourite_restaurant_qs = Profile.objects.get(user=user).favourite_restaurant.filter(
pk__in=restaurant_qs.values_list('id', flat=True))
return favourite_restaurant_qs
You can use SerializerMethodField. SerializerMethodField allows add extra field which is read only as you want.
class RestaurantSerializer(serializers.ModelSerializer):
is_favorite = serializers.SerializerMethodField()
class Meta:
model = Restaurant
fields = ('your', 'fields', 'is_favorite')
def get_is_like(self, obj):
return is_favourite_restaurant(obj.id, self.context['request'].user)
Normally, ListAPIView add context to serializer. As you use your create method, you should add manually.
serializer = RestaurantSerializer(restaurant_qs, many=True, context={'request': self.request})
Context allows access some data which is we send from the view.
As you did not shown your is_favourite_restaurant, i can't say that what should you do in that function. I guess you should change ids parameter from array to one id.
Your response looks like
[
{
"id": 2,
"name_of_the_restaurant": "Aniket",
"opening_time": "14:08:33.413402",
"closing_time": "22:08:33.413414",
"is_favourite": True,
},
{
"id": 3,
"name_of_the_restaurant": "Aniket-1",
"opening_time": "14:13:37.656385",
"closing_time": "22:13:37.656397",
"is_favourite": False,
}
]
def is_favourite_restaurant(restaurant_id, user):
favourite_restaurant_qs = Profile.objects.get(user=user).favourite_restaurant.filter(
pk=restaurant_id).exists()
return favourite_restaurant_qs

How to retrieve a many-to-many field with backward relationships lookup in Django REST Framework serializer?

Please correct my title if it's not correct. My problem is I want to retrieve FinishType's name from Product. I have tried 2 ways to achieve this: first attempt and second attempt.
My simplifed related models in models.py:
class Product(models.Model):
product_id = models.CharField(max_length=6)
color = models.ForeignKey(ColorParent, on_delete=models.SET_NULL, null=True)
collection = models.ForeignKey(ProductCollection, on_delete=models.SET_NULL, null=True)
#property
def get_distributors(self):
return Distributor.objects.filter(distributor__products=self).count()
def __str__(self):
return self.product_id
class FinishType(models.Model):
name = models.CharField(max_length=50)
def __str__(self):
return self.name
class ProductFinishType(models.Model):
product = models.ForeignKey(Product, on_delete=models.CASCADE)
market = models.ForeignKey(Market, on_delete=models.CASCADE)
finish_types = models.ManyToManyField(FinishType)
def __str__(self):
return '%s - %s' % (self.product, self.market)
class ProductAlias(models.Model):
product = models.ForeignKey(Product, on_delete=models.CASCADE)
market = models.ForeignKey(Market, on_delete=models.CASCADE)
name = models.CharField(max_length=50, null=True, blank=True)
def __str__(self):
return '%s - %s' % (self.product, self.name)
My serializers.py:
class ProductGridSerializer(serializers.ModelSerializer):
name = serializers.SerializerMethodField(source='get_name')
finishing = serializers.SerializerMethodField('get_finish_types')
distributor = serializers.ReadOnlyField(source='get_distributors')
#staticmethod
def get_name(obj):
return [pa.name for pa in obj.productalias_set.all()]
#staticmethod
def get_finish_types(obj):
return [pft.name for pft in obj.productfinishtype_set.all().select_related('finish_types')] # first attempt
class Meta:
model = Product
fields = ['id', 'product_id', 'name', 'collection', 'finishing', 'distributor']
First attempt works for name field which fetches ProductAlias's name but gives me this error:
FieldError at /api/product_grids/
Invalid field name(s) given in select_related: 'finish_types'. Choices are: product, market
My get_finish_types() on second attempt:
#staticmethod
def get_finish_types(obj):
product_finish_types = obj.productfinishtype_set.all()
response = ProductFinishTypeSerializer(product_finish_types, many=True, source='finish_types').data
return response
It gives me the whole object datas:
{
"id": 1,
"product_id": "BQ1111",
"name": [
"White Stone"
],
"collection": 1,
"finishing": [
{
"id": 1,
"product": 1,
"market": 1,
"finish_types": [
1,
3,
5
]
}
],
"distributor": 5
},
My desired output is something like:
{
"id": 1,
"product_id": "BQ1111",
"name": [
"White Stone"
],
"collection": 1,
"finishing": [
"Polished",
"Carved",
"Melted"
],
"distributor": 5
},
Create a serializer for FinishType,
class FinishTypeSerializer(serializers.ModelSerializer):
class Meta:
model = FinishType
fields = ('name',)
and wire-up it in ProductGridSerializer using SerializerMethodField
class ProductGridSerializer(serializers.ModelSerializer):
name = serializers.SerializerMethodField(source='get_name')
distributor = serializers.ReadOnlyField(source='get_distributors')
finishing = serializers.SerializerMethodField()
def get_finishing(self, product):
qs = FinishType.objects.filter(productfinishtype__product=product)
return FinishTypeSerializer(qs, many=True).data
#staticmethod
def get_name(obj):
return [pa.name for pa in obj.productalias_set.all()]
class Meta:
model = Product
fields = ['id', 'product_id', 'name', 'collection', 'finishing', 'distributor']
Inspired by #Arakkal Abu's queryset, I tried it using my first attempt.
FinishTypeSerializer added in serializers.py:
class FinishTypeSerializer(serializers.ModelSerializer):
class Meta:
model = FinishType
fields = ('name',)
ProductGridSerializer in serializers.py:
class ProductGridSerializer(serializers.ModelSerializer):
name = serializers.SerializerMethodField(source='get_name')
finishing = serializers.SerializerMethodField(source='get_finishing')
distributor = serializers.ReadOnlyField(source='get_distributors')
#staticmethod
def get_name(obj):
return [pa.name for pa in obj.productalias_set.all()]
#staticmethod
def get_finishing(product):
return [pft.name for pft in FinishType.objects.filter(productfinishtype__product=product)]
class Meta:
model = Product
fields = ['id', 'product_id', 'name', 'collection', 'finishing', 'distributor']
The JSON output is:
{
"id": 1,
"product_id": "BQ1111",
"name": [
"White Stone"
],
"collection": 1,
"finishing": [
"Polished",
"Honed",
"Carved"
],
"distributor": 5
},

Post to nested fields with Django Rest Framework serializers

I have setup my serializer to return nested content successfully.
However, I have not been able to post data within the nested fields.
I don't get an error when posting the data- but it only posts to the non-nested fields.
I would like for it to take the "name" field OR primary key (of model "TAG") for posting item.
Models.py
class Tag(models.Model):
name = models.CharField("Name", max_length=5000, blank=True)
def __str__(self):
return self.name
class Movie(models.Model):
title = models.CharField("Whats happening?", max_length=100, blank=True)
tag = models.ManyToManyField('Tag', blank=True)
def __str__(self):
return self.title
Serializers.py:
class TagSerializer(serializers.ModelSerializer):
taglevel = filters.CharFilter(taglevel="taglevel")
class Meta:
model = Tag
fields = ('name', 'taglevel', 'id')
class MovieSerializer(serializers.ModelSerializer):
tag = TagSerializer(many=True, read_only=False)
info = InfoSerializer(many=True, read_only=True)
class Meta:
model = Movie
fields = ('title', 'tag', 'info')
def validate(self, data):
print(self.initial_data.__dict__)
data['tag_name'] = []
if 'tag' in self.initial_data.keys():
for entry in self.initial_data['tag']:
data['tag_name'].append(entry['name'])
return data
def create(self, validated_data):
print(validated_data)
tags_data = validated_data.pop('tag')
movie = Task.objects.create(**validated_data)
for tag_data in tags_data:
Movie.objects.create(name=name, **tag_data)
return movie
Sample of posting data:
r = requests.post('http://localhost:8000/api/Data/',{ "title": "TEST_title", "tag": [ { "name": "test1", "name": "test2" } ], "info": [] })
Your json should be.
{
"title": "TEST_title",
"tag": [ {"name": "test1" },
{"name": "test2"}
],
"info": []
}
class TagSerializer(serializers.ModelSerializer):
taglevel = filters.CharFilter(taglevel="taglevel")
class Meta:
model = Tag
fields = ('name', 'taglevel', 'id')
class MovieSerializer(serializers.ModelSerializer):
tag = TagSerializer(many=True, read_only=False)
info = InfoSerializer(many=True, read_only=True)
class Meta:
model = Movie
fields = ('title', 'tag')
def create(self, validated_data):
tags_data = validated_data.pop('tag')
movie = Movie.objects.create(**validated_data)
for tag_data in tags_data:
movie.tag.create(**tag_data)
return movie

Django Rest Framework 3.0: Saving Nested, Many-To-One Relationship

I'm attempting to build a nested relationship using Django Rest Framework 3.0. I've created my serializers and have attempted to override the create() function. My models are defined as follows:
class Item(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL)
name = models.CharField(max_length=200)
description = models.CharField(max_length=1000)
categories = models.ManyToManyField(Category, null=True, blank=True)
class Price(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL)
item = models.ForeignKey(Item, related_name='prices')
name = models.CharField(max_length=100)
cost = models.FloatField()
As you'll note, I can have multiple prices for my items. My serializers are defined as follows:
class PriceSerializer(serializers.ModelSerializer):
class Meta:
model = Price
owner = serializers.Field(source='owner.username')
exclude = ('user',)
class ItemSerializer(serializers.ModelSerializer):
prices = PriceSerializer(many=True, required=False)
categories = CategorySerializer(many=True, required=False)
class Meta:
model = Item
owner = serializers.Field(source='owner.username')
fields = ('id', 'name', 'description', 'prices', 'categories')
def create(self, validated_data):
user = validated_data.get('user')
# Get our categories
category_data = validated_data.pop('categories')
# Create our item
item = Item.objects.create(**validated_data)
# Process the categories. We create any new categories, or return the ID of existing
# categories.
for category in category_data:
category['name'] = category['name'].title()
category, created = Category.objects.get_or_create(user=user, **category)
item.categories.add(category.id)
item.save()
return item
When I try and POST a new item:
{
"name": "Testing",
"description": "This is a test",
"categories": [
{
"name": "foo"
},
{
"name": "bar"
}
],
"prices": [
{
"name": "Red",
"cost": 10
}
]
}
I get the following error:
{
"prices": [
{
"item": [
"This field is required."
]
}
]
}
Presumably because the Price serializer has no idea what the ID of the new item is. I've tried overriding this functionality in the create() function of my serializer, but it appears as though the serializer's validation is being hit before I have the opportunity to create the item and associate it with the price.
So - How do I create a new item, get the item ID, and then create each of the new prices?
The problem is that your PriceSerializer is looking for the item key because it is specified on the Price model. This isn't immediately obvious because you are using Meta.exclude instead of Meta.fields.
class PriceSerializer(serializers.ModelSerializer):
class Meta:
model = Price
exclude = ('user',)
Is the same as writing
class PriceSerializer(serializers.ModelSerializer):
class Meta:
model = Price
fields = ('id', 'item', 'name', 'cost', )
Which makes it very clear what your issue is. Because your item field on the model does not have empty=True (or null=True) set, Django REST Framework automatically generates it as a PrimaryKeyRelatedField with required=True. This is why you are getting the This field is required is required error, because Django REST Framework cannot automatically detect that it is coming from a parent serializer which already has that field.
You can get around this by removing the field from the serializer, as it doesn't appear to ever be needed.
class PriceSerializer(serializers.ModelSerializer):
class Meta:
model = Price
fields = ('id', 'name', 'cost', )
This will no longer display the item field though, but I suspect this isn't actually an issue for you.

Categories

Resources