How can I serialize one to many models in Django Rest? - python

So I have two models Invoice and Items. Invoices can have many Items. Here's how they are defined:
class Invoice(models.Model):
customer_name = models.CharField(max_length = 200, verbose_name='Customer Name')
customer_phone = models.IntegerField(null = True, blank = True, verbose_name='Customer Phone')
customer_address = models.TextField(null = True, blank = True, verbose_name='Customer Address')
invoice_id = models.UUIDField(primary_key = True, unique = True, default=uuid.uuid4, verbose_name='Invoice ID')
invoice_date = models.DateField(auto_now_add=True, verbose_name='Invoice Date')
def __str__(self):
return self.customer_name + ' - ' + str(self.invoice_id)
class Meta:
verbose_name = 'Invoice'
verbose_name_plural = 'Invoices'
class Items(models.Model):
invoice = models.ForeignKey(Invoice, on_delete = models.CASCADE, related_name='invoice')
item_name = models.CharField(max_length = 200, null = False)
item_quantity = models.IntegerField()
item_price = models.IntegerField()
item_id = models.AutoField(primary_key=True)
def __str__(self):
return self.item_name
class Meta:
verbose_name = 'Items'
verbose_name_plural = 'Items'
I want to implement two API endpoints, one to get the list of invoices, and another to get a specific invoice with its items.
For example, /api/ will return all invoices
[
{
"customer_name": "John Doe",
"customer_phone": 111666,
"customer_address": "Palo Alto, California",
"invoice_id": "a8aeb5a8-5498-40fd-9b4f-bb09a7057c71",
"invoice_date": "2022-05-04"
},
{
"customer_name": "Cassian Green",
"customer_phone": 111000,
"customer_address": "2112 Illinois Avenue",
"invoice_id": "7d7b7878-3ffc-4dd2-a60a-fa207c147860",
"invoice_date": "2022-05-04"
},
{
"customer_name": "Chelsea Davis",
"customer_phone": 666777,
"customer_address": "2260 Coplin Avenue",
"invoice_id": "3dda2054-49d7-49dc-9eba-ddc0fdacfd3b",
"invoice_date": "2022-05-04"
},
{
"customer_name": "Derek Elliott",
"customer_phone": 111100,
"customer_address": "3236 Sundown Lane",
"invoice_id": "da112c0d-aff4-43d3-a465-910cc1483fc5",
"invoice_date": "2022-05-04"
}
]
and now if I request a specific invoice with its items, like /api/a8aeb5a8-5498-40fd-9b4f-bb09a7057c71 should return the following response
{
"invoice_id": "a8aeb5a8-5498-40fd-9b4f-bb09a7057c71",
"items": [
{
"item_name": "PC Cabinet",
"item_quantity": 1,
"item_price": 200
},
{
"item_name": "Motherboard",
"item_quantity": 1,
"item_price": 1000
},
{
"item_name": "PSU",
"item_quantity": 1,
"item_price": 300
}
]
}
Here are the serializers:
class ItemSerializer(serializers.ModelSerializer):
class Meta:
model = Items
fields = [
'item_name',
'item_quantity',
'item_price',
]
class InvoiceSerializer(serializers.ModelSerializer):
items = ItemSerializer(many=True, read_only=True)
class Meta:
model = Invoice
fields = [
'customer_name',
'customer_phone',
'customer_address',
'invoice_id',
'invoice_date',
'items'
]
And the views:
#api_view(['GET',])
def InvoiceList(request):
if request.method == 'GET':
queryset = Invoice.objects.all()
serializer_class = InvoiceSerializer(queryset, many=True)
return Response(serializer_class.data)
#api_view(['GET',])
def InvoiceDetail(request, pk):
if request.method == 'GET':
queryset = list(Items.objects.filter(invoice = pk))
serializer_class = ItemSerializer(instance=queryset, many=True)
return Response(serializer_class.data)
The first API endpoint is working as expected but the I need help with implementing the second one. How can I get that nested response?
Thanks.

Your related_name of invoice in Items model is wrong, you should set it as "items" because you are going to call that from Invoice model like that.
Also if you want to have different response from that second API you should change it to use other serializer than InvoiceList:
#api_view(['GET',])
def InvoiceDetail(request, pk):
if request.method == 'GET':
queryset = list(Items.objects.filter(invoice = pk))
serializer_class = InvoiceDetailSerializer(instance=queryset, many=True)
return Response(serializer_class.data)
I changed name of serializer to InvoiceDetailSerializer so you need to create new serializer:
class InvoiceDetailSerializer(serializers.ModelSerializer):
items = ItemSerializer(many=True, read_only=True)
class Meta:
model = Invoice
fields = [
'invoice_id',
'items'
]

you can try something like this. (not tested)
class ItemSerializer(serializers.ModelSerializer):
class Meta:
model = Items
fields = ['item_name', 'item_quantity', 'item_price',]
class InvoiceDetailsSerializer(serializers.ModelSerializer):
items = ItemSerializer(many=True, read_only=True)
class Meta:
model = Invoice
fields = ['id', 'items']
#api_view(['GET',])
def InvoiceDetail(request, pk):
if request.method == 'GET':
invoice = Invoice.objects.get(id=pk)
serializer_class = InvoiceDetailsSerializer(instance=invoice)
return Response(serializer_class.data)

Related

How do I "flatten" a nested serializer in DRF?

We have a nested serializer that we would like to "flatten". But I'm not having much luck finding how to achieve this in the docs.
Here is the current output.
{
"user_inventory": "UOHvaxFa11R5Z0bPYuihP0RKocn2",
"quantity": 1,
"player": {
"card_id": "c69c0808328fdc3e3f3ee8b9b7d4a7f8",
"game": "MLB The Show 22",
"name": "Jesus Tinoco",
"all_positions": [
"CP"
]
}
}
Here is what I'd like:
{
"user_inventory": "UOHvaxFa11R5Z0bPYuihP0RKocn2",
"quantity": 1,
"card_id": "c69c0808328fdc3e3f3ee8b9b7d4a7f8",
"game": "MLB The Show 22",
"name": "Jesus Tinoco",
"all_positions": [
"CP"
]
}
Here is how the serializers are setup:
class PlayerProfileSerializer(serializers.ModelSerializer):
class Meta:
model = PlayerProfile
fields = (
'card_id',
'game',
'name',
'all_positions',
)
class UserInventoryItemSerializer(serializers.ModelSerializer):
player = PlayerProfileSerializer()
class Meta:
model = UserInventoryItem
fields = (
'user_inventory',
'quantity',
'player',
)
Here is the view:
class OwnedInventoryView(viewsets.ModelViewSet):
serializer_class = UserInventoryItemSerializer
filterset_class = UserInventoryItemFilter
def get_queryset(self):
order_by = self.request.query_params.get('order_by', '')
if order_by:
order_by_name = order_by.split(' ')[1]
order_by_sign = order_by.split(' ')[0]
order_by_sign = '' if order_by_sign == 'asc' else '-'
return UserInventoryItem.objects.filter(user_inventory=self.kwargs['user_inventory_pk']).order_by(order_by_sign + order_by_name)
return UserInventoryItem.objects.filter(user_inventory=self.kwargs['user_inventory_pk'])
You can use .to_representation() method to alter data structure:
class UserInventoryItemSerializer(serializers.ModelSerializer):
player = PlayerProfileSerializer()
class Meta:
model = UserInventoryItem
fields = (
'user_inventory',
'quantity',
'player',
)
def to_representation(self, instance):
representation = super().to_representation(instance)
player = representation.pop('player')
for key, value in player.items():
representation[key] = value
return representation

django drf about many-to-many RelatedField custom field

I use the latest version of The Django REST Framework,
and The table in model is many-to-many related
My current model code looks like this:
model.py
class LvsDeploy(models.Model):
MASTER = 'MASTER'
BACKUP = 'BACKUP'
ROLE_CHOICES = (
(MASTER, 'MASTER'),
(BACKUP, 'BACKUP')
)
id = models.AutoField(primary_key=True)
cluster_name = models.CharField(max_length=30)
dip = models.CharField(max_length=15, verbose_name='DR IP')
role = models.CharField(max_length=10, choices=ROLE_CHOICES, verbose_name="role")
class LoadBalance(models.Model):
id = models.AutoField(primary_key=True)
lb_name = models.CharField(max_length=30, verbose_name='load balance')
cluster = models.ManyToManyField('LvsDeploy',related_name='cluster')
vip = models.CharField(max_length=50, verbose_name='VIP')
port = models.IntegerField(verbose_name='port')
def __str__(self):
return self.lb_name
My serializer code looks like this:
serializers.py
class LvsDeployReadOnly(serializers.ModelSerializer):
class Meta:
model = LvsDeploy
fields = '__all__'
class LoadBalanceSerializer(serializers.ModelSerializer):
cluster_name = serializers.RelatedField(source='cluster.name',read_only=True)
cluster = LvsDeployReadOnly(many=True)
##rs = RSInfoSerializer(many=True, read_only=True)
class Meta:
model = LoadBalance
fields = '__all__'
##extra_fields = ['rs','cluster','cluster_name']
extra_fields = ['rs','cluster','cluster_name']
My views code:
class BalanceList(views.APIView):
def get(self, request):
queryset = LoadBalance.objects.all()
serializer = LoadBalanceSerializer(queryset, many=True)
print(serializer.data)
return Response(serializer.data)
and request actual output:
[
{
"id": 2,
"cluster_name": null,
"cluster": [
{
"id": 1,
"cluster_name": "lvs_sz01",
"dip": "1.1.1.6",
"role": "BACKUP",
},
{
"id": 2,
"cluster_name": "lvs_sz01",
"dip": "1.1.1.5",
"role": "BACKUP",
}
],
"lb_name": "lb001",
"vip": "1.1.1.1",
"port": 80,
}
]
But the cluster_name filed value is same in the dictionary of lists .
I want the output to look like this:
[
{
"id": 2,
"cluster_name": "lvs_sz01",
"cluster": [
{
"dip": "1.1.1.6",
"role": "BACKUP",
},
{
"dip": "1.1.1.5",
"role": "BACKUP",
}
],
"lb_name": "lb001",
"vip": "1.1.1.1",
"port": 80,
}
]
How should I change it? Can you help me ?
I solved it myself with the following method:
class LoadBalanceSerializer(serializers.ModelSerializer):
cluster_name = serializers.SerializerMethodField()
cluster = LvsDeployReadOnly(many=True, read_only=True)
rs = RSInfoSerializer(many=True, read_only=True)
class Meta:
model = LoadBalance
fields = '__all__'
extra_fields = ['rs','cluster','cluster_name']
def get_cluster_name(self,obj):
print(type(obj.lb_name))
lvs_cluster = obj.cluster.all()
cluster_name = [ i.cluster_name for i in lvs_cluster ][0]
print(cluster_name)
return cluster_name
Thanks!

POST with an ID and GET the nested object does not work in django at the same time in ModelSerializer

I have spent already a couple of days researching about this issue in similar questions and I am not able to get a solution. This should be something simple, I have a model:
model.py
class Item(models.Model):
"""Class to represent an item..."""
label = models.TextField(null=True)
name = models.TextField()
category = models.ForeignKey( "Category", on_delete=models.SET_NULL,
null=True, default=DEFAULT_CATEGORY_ID)
class Category(models.Model):
"""Class to represent the category of an Item. Like plants, bikes..."""
name = models.TextField()
description = models.TextField(null=True)
view.py
class ItemViewset(viewsets.ModelViewSet): # pylint: disable=too-many-ancestors
"""API Endpoint to return the list of items"""
queryset = Item.objects.all()
serializer_class = ItemSerializer
serializer.py
class ItemSerializer(serializers.ModelSerializer):
"""Serializer for Item."""
category = CategorySerializer(read_only=True)
class Meta: # pylint: disable=too-few-public-methods
"""Class to represent metadata of the object."""
model = Item
fields = [ 'id', 'label', 'name', 'category']
read_only_fields = ['id']
# def to_representation(self, instance):
# ret = super().to_representation(instance)
# ret['category'] = CategorySerializer(instance.category).data
# return ret
def create(self, request):
# Look up objects by arbitrary attributes.
# You can check here if your students are participating
# the classes and have taken the subjects they sign up for.
category = get_object_or_404(Category(), id=request.data.get('category'))
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save(category=category)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
If I have the line category = CategorySerializer(read_only=True) commented the nested get won't work, for example for a curl to an object. I will get just de id of the structure. If I uncomment it like it is in the example I get the right response:
{
"id": 60,
"label": null,
"name": "Delete me",
"category": {
"id": 1,
"name": "IT"
}
}
But then the post with id will not work, the category will be set to null.
How can I get both working at the same time?
Update: This is the post I want to do:
{
"label": "00000003",
"name": "Delete me",
"category": 1,
}
You can override the to_represention method of your serializer
class ItemSerializer(serializers.ModelSerializer):
"""Serializer for Item."""
class Meta: # pylint: disable=too-few-public-methods
"""Class to represent metadata of the object."""
model = Item
fields = [ 'id', 'label', 'name', 'category']
read_only_fields = ['id']
def to_representation(self, instance):
data = super().to_representation(instance)
data["category"] = CategorySerializer(instance.category).data
return data

How to create multiple model instances without duplicated writable nested serializer in Django REST Framework?

I have two related models: Product and ProductDescription. In 1 submit action user able to insert a new Product with multiple descriptions depend on the available languages. I use writable nested serializer to insert into Product and ProductDescription simultaneously. I do it by overriding create function in ProductDescriptionSerializer class, it works. However, I can only insert 1 ProductDescription at a time.
Then I tried to use this answer to create multiple model instances at once. The problem is it also creates the same Product twice instead of using the newly created Product Id to insert the next ProductDescription.
My models.py:
class Product(models.Model, ProductStatus):
product_code = 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)
video = models.URLField(verbose_name='Video URL', max_length=250, null=True, blank=True)
status = models.CharField(max_length=20, choices=ProductStatus.status, default=ProductStatus.active)
class ProductDescription(models.Model):
product = models.ForeignKey(Product, on_delete=models.CASCADE)
language = models.ForeignKey(Language, on_delete=models.CASCADE)
description = models.TextField(max_length=500, null=True, blank=True)
def __str__(self):
return '%s - %s' % (self.product, self.language)
My serializers.py:
class CustomRelatedField(serializers.RelatedField):
def display_value(self, instance):
return instance
def to_representation(self, value):
return str(value)
def to_internal_value(self, data):
model = self.queryset.model
return model.objects.get(id=data)
class ProductSerializer(serializers.ModelSerializer):
collection = CustomRelatedField(queryset=ProductCollection.objects.all(), many=False)
color = CustomRelatedField(queryset=ColorParent.objects.all(), many=False)
class Meta:
model = Product
fields = ['id', 'product_code', 'collection', 'color', 'video', 'status']
class ProductDescriptionSerializer(serializers.ModelSerializer):
product = ProductSerializer()
language = CustomRelatedField(many=False, queryset=Language.objects.all())
class Meta:
model = ProductDescription
fields = ['id', 'product', 'language', 'description']
def to_representation(self, instance):
data = super().to_representation(instance)
if self.context['request'].method == 'GET':
data['product'] = instance.product.product_code
return data
return Serializer.to_representation(self, instance)
# The `.create()` method does not support writable nested fields by default.
def create(self, validated_data):
# create product data for Product model.
product_data = validated_data.pop('product')
product = Product.objects.create(**product_data)
# create ProductDescription and set product FK.
product_description = ProductDescription.objects.create(product=product, **validated_data)
# return ProductDescription instance.
return product_description
My views.py:
class CreateListModelMixin(object):
def get_serializer(self, *args, **kwargs):
if isinstance(kwargs.get('data', {}), list):
kwargs['many'] = True
return super(CreateListModelMixin, self).get_serializer(*args, **kwargs)
class ProductDescriptionView(CreateListModelMixin, viewsets.ModelViewSet):
permission_classes = [permissions.DjangoModelPermissions]
queryset = ProductDescription.objects.all()
serializer_class = ProductDescriptionSerializer
http_method_names = ['get', 'head', 'post', 'put', 'patch', 'delete']
The JSON format I use to POST data:
[
{
"product": {
"product_code": "BQ1080",
"collection": 5,
"color": 7,
"video": "https://www.youtube.com/watch?v=",
"status": "Continue"
},
"language": 1,
"description": "English description."
},
{
"product": {
"product_code": "BQ1080",
"collection": 5,
"color": 7,
"video": "https://www.youtube.com/watch?v=",
"status": "Continue"
},
"language": 2,
"description": "Vietnamese description."
}
]
It creates a duplicate Product in Product List:
[
{
"id": 26,
"product_code": "BQ1080",
"collection": 5,
"color": 7,
"video": "https://www.youtube.com/watch?v=",
"status": "Continue"
},
{
"id": 27,
"product_code": "BQ1080",
"collection": 5,
"color": 7,
"video": "https://www.youtube.com/watch?v=",
"status": "Continue"
}
]
The ProductDescription datas are correct though:
[
{
"id": 5,
"product": "BQ1080",
"language": "English",
"description": "English description."
},
{
"id": 6,
"product": "BQ1080",
"language": "Vietnam",
"description": "Vietnamese description."
}
]
To avoid duplicate product you can use get_or_create() method:
class ProductDescriptionSerializer(serializers.ModelSerializer):
...
def create(self, validated_data):
# create product data for Product model.
product_data = validated_data.pop('product')
product_code = product_data.pop("product_code")
product, _ = Product.objects.get_or_create(product_code=product_code, defaults=product_data)
# create ProductDescription and set product FK.
product_description = ProductDescription.objects.create(product=product, **validated_data)
# return ProductDescription instance.
return product_description
Note that get_or_create is prone to race condition. So if two same requests came to you service at the same time you may still have duplicate products.
I think you need to override your ProductSerializer's create method. Maybe you can try like this:
class ProductSerializer(serializers.ModelSerializer):
collection = CustomRelatedField(queryset=ProductCollection.objects.all(), many=False)
color = CustomRelatedField(queryset=ColorParent.objects.all(), many=False)
def create(self, validated_data):
instance, _ = Product.objects.get_or_create(**validated_data)
return instance
class Meta:
model = Product
fields = ['id', 'product_code', 'collection', 'color', 'video', 'status']
So that, first it will try to get if the Product exists, else create the instance(hence reducing duplicate entry).
ForeignKey not for this job you should use ManyToManyField

Serializer return incorrect output

I have a model:
class Route(models.Model):
name = models.CharField("Номер маршрута", max_length=100)
evotor_user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name="пользователь эвотор", on_delete=models.CASCADE)
nds_CHOICES = (
(0, 'БЕЗ НДС'),
(1, 'Основная ставка 0%'),
(2, 'Основная ставка 10%'),
(3, 'Расчётная ставка 10%'),
(4, 'Основная ставка 18%'),
(5, 'Расчётная ставка 18%')
)
nds = models.IntegerField('НДС', choices=nds_CHOICES, default=0)
def __str__(self):
return self.name
class Meta:
verbose_name = "Номер маршрута"
verbose_name_plural = "Номера маршрутов"
And I have serializer for that model:
class RouteSerializer(serializers.ModelSerializer):
class Meta:
model = Route
fields = ("id", "name", "nds")
My APIView
class CarView(APIView):
authentication_classes = (BearerAuthentication,)
permission_classes = (IsAuthenticated,)
def get(self, request):
if 'route_id' not in request.GET or request.GET['route_id']=="":
#return Response({'message': 'Не указан ID маршрута'}, status=status.HTTP_400_BAD_REQUEST)
car = Car.objects.filter(evotor_user=request.user)
else:
car = Car.objects.filter(route__id=request.GET['route_id'])
serializer = CarSerializer(car, many=True)
print(serializer.data)
return Response(serializer.data)
use this serializer
class CarSerializer(serializers.ModelSerializer):
route = RouteSerializer
class Meta:
model = Car
fields = ("id", "name", "route")
and must return me output like this:
[
{
"id": 1,
"name": "dfgdfgdfg",
"route": {
'id': 1,
'name': 'route_name',
'nds': 0,
}
}
]
But it returns:
[
{
"id": 1,
"name": "dfgdfgdfg",
"route": 1
}
]
CarSerializer.route declaration is wrong. It must be an instance of RouteSerializer, like this:
class CarSerializer(serializers.ModelSerializer):
route = RouteSerializer(read_only=True)
class Meta:
model = Car
fields = ("id", "name", "route")
This is called "nested relationship", see docs at https://www.django-rest-framework.org/api-guide/relations/#nested-relationships

Categories