ManyToMany Through Field Serialization - python

I am having problems serializing my items/orders with quantity.
Results I am getting:
{
"id": 1,
"_current_status": null,
"orderitem_set": [
{
"item_id": 2,
"quantity": 5
},
{
"item_id": 1,
"quantity": 1
}
],
"items": [
{
"id": 2,
"name": "Blue Shoe",
"description": "Sweet blue shoe bro",
"weight": "99.99",
"price": "99.99"
},
{
"id": 1,
"name": "Red Shoe",
"description": "sweet red shoe bro",
"weight": "1.00",
"price": "100.00"
}
],
"_last_modified": "2015-06-10T22:32:08.007833Z",
"_weight": "500.95",
"_total_price": "599.95",
"placed": false,
"date_placed": null
}
Results I want:
{
"id": 1,
"_current_status": null,
"items": [
{
"id": 2,
"name": "Blue Shoe",
"description": "Sweet blue shoe bro",
"weight": "99.99",
"price": "99.99",
"quantity": 5
},
{
"id": 1,
"name": "Red Shoe",
"description": "sweet red shoe bro",
"weight": "1.00",
"price": "100.00",
"quantity": 1
}
],
"_last_modified": "2015-06-10T22:32:08.007833Z",
"_weight": "500.95",
"_total_price": "599.95",
"placed": false,
"date_placed": null
}
Classes
class Order(models.Model):
_current_status = models.ForeignKey("main.Status", blank=True, null=True)
_last_modified = models.DateTimeField(auto_now=True)
_weight = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True)
_total_price = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True)
user = models.ForeignKey("main.ShopUser")
items = models.ManyToManyField("main.Item", through='main.OrderItem')
placed = models.BooleanField(default=False)
date_placed = models.DateTimeField(null=True, blank=True)
class OrderItem(models.Model):
order = models.ForeignKey('main.Order')
item = models.ForeignKey('main.Item')
quantity = models.IntegerField(default=1)
Serializers
I realize that my serializer is what is putting the orderitem_set into my json but I couldn't figure out how to get the quantity data into items other than in a custom view that loops through the items and the orderitem_set and adds a quantity to items. But that is just terrible and I was hoping there was a way to do this with the built in stuff with the framework or at least make it so I don't have to do that for every view.
class OrderItemField(serializers.RelatedField):
def to_representation(self, value):
return {'item_id': value.item_id, 'quantity': value.quantity}
class OrderSerializer(serializers.ModelSerializer):
_current_status = StatusSerializer(required=False, many=False)
orderitem_set = OrderItemField(read_only=True, many=True)
items = ItemSerializer(required=True, many=True)
class Meta:
model = Order
exclude = ('user',)
The view
This view gives me what I am looking for, but it is just terrible. Especially when I have other views that would be giving me a list of orders vs just a single order.
def set_quantities(data):
# Gets the quantity for each item and then appends it to each item instead of having 'orderitem_set'
for order_item in data['orderitem_set']:
for item in data['items']:
if item['id'] == order_item['item_id']:
item['quantity'] = order_item['quantity']
del data['orderitem_set']
return data
def get(self, request, *args, **kwargs):
serializer = self.get_serializer(self.get_object())
data = set_quantities(serializer.data)
return Response(data, status=status.HTTP_200_OK)

By changing the OrderSerializer items field to be an OrderItemField I was able to get back to root Order by using self.root.instance and then using that in conjuction with the value, which is an Item instance, build a query to find the OrderItem object to get the quantity. Then I can get a serialized Item using my ItemSerializer and just append the quantity to that data.
Doing it this way applies to all of my order views and it is a lot less messy. If there is a better way please let me know!
Serializers
class OrderItemField(serializers.RelatedField):
def to_representation(self, value):
if type(self.root.instance) == list:
self.root.instance = self.root.instance[0]
order_item = OrderItem.objects.filter(item=value, order=self.root.instance).first()
quantity = order_item.quantity
value = ItemSerializer(value).data
value['quantity'] = quantity
return value
class OrderSerializer(serializers.ModelSerializer):
_current_status = StatusSerializer(required=False, many=False)
items = OrderItemField(many=True, read_only=True)
class Meta:
model = Order
exclude = ('user',)

You can just use it like this:
class OrderItemSerializer(serializers.ModelSerializer):
class Meta:
model = Order.items.through
fields = ('order', 'item', 'quantity')
class OrderSerializer(serializers.ModelSerializer):
items = OrderItemSerializer(source='orderitem_set', many=True)
class Meta:
model = Order
fields = ('your_order_fields', 'items')
Note: you have to add items field into OrderSerializer.

Related

Django: Return all nested Sub Model from a given Model (which isn't directly linked to given model)

I have my models.py as shown below:
from django.db import models
# Create your models here.
class Category(models.Model):
categoryType = models.CharField(max_length=100,blank = False, unique = True, null = False)
def __str__(self):
return self.categoryType
class SubCategory(models.Model):
subcategoryType = models.CharField(max_length=100)
categoryType = models.ForeignKey(Category,on_delete=models.CASCADE, null = True, related_name='category_type')
def __str__(self):
return f'{self.categoryType} :: {self.subcategoryType}'
class Product(models.Model):
productName = models.CharField(max_length=50,blank = False, null = False)
subCategoryType = models.ForeignKey(SubCategory,on_delete=models.SET_NULL, null=True,related_name='product_subcategories')
#categoryType = models.ForeignKey(Category,on_delete=models.SET_NULL, null=True,related_name='product_categories')
def __str__(self):
return f'{self.productName} : {self.subcategoryType}'
I have created a serializer to get all products within a given category as shown below:
class ProductSerializerSpecific(serializers.ModelSerializer):
class Meta:
model = Product
fields = ('id','productName')
class SubCategoryProductsSpecific(serializers.ModelSerializer):
products = ProductSerializerSpecific(source='product_subcategories', many=True)
class Meta:
model = SubCategory
fields = ['products']
class CategoryProducts(serializers.ModelSerializer):
products = SubCategoryProductsSpecific(source='category_type', many=True)
class Meta:
model = Category
fields = ('id','products')
My View goes like this:
class ListAllCategoryProducts(viewsets.ReadOnlyModelViewSet):
queryset = Category.objects.all()
serializer_class = CategoryProducts
And finally I registered by route like this:
router.register(r"category_products", views.ListAllCategoryProducts, basename="categories_products")
When I do a GET request to get all products with a given ID as shown below:
GET http://localhost:8000/category_products/1 HTTP/1.1
The output comes as shown below:
{
"id": 1,
"products": [
{
"products": []
},
{
"products": []
},
{
"products": []
},
{
"products": [
{
"id": 1,
"productName": "Dell XPS"
},
{
"id": 2,
"productName": "Macbook Pro"
},
{
"id": 3,
"productName": "Dell Inspiron"
},
{
"id": 4,
"productName": "Lenevo Ideapad"
},
{
"id": 5,
"productName": "Asus"
}
]
}
]
}
Where each dictionary inside list represents the subcategory, but I was interested in getting a result which had just products which would look something like this:
{
"id": 1,
"products": [
{
"id": 1,
"productName": "Dell XPS"
},
{
"id": 2,
"productName": "Macbook Pro"
},
{
"id": 3,
"productName": "Dell Inspiron"
},
{
"id": 4,
"productName": "Lenevo Ideapad"
},
{
"id": 5,
"productName": "Asus"
}
]
}
If you observe carefully the result I am getting basically is category => subcategory => products, whereas I am interested in category => products (where subcategory is fetched from category and products are fetched from subcategory). What should be the way to do the same? Since products aren't directly linked with category rather they need to go via sub category.
In the Category model, we can make a property to return the QuerySet of all related Products with:
class Category(models.Model):
categoryType = models.CharField(max_length=100,blank = False, unique = True, null = False)
#property
def products(self):
return Product.objects.filter(subCategoryType__categoryType=self)
def __str__(self):
return self.categoryType
Then we remove the SubCategoryProductsSpecific serializer in between, this is the item that makes sublists in the response. We can then set the source to:
class ProductSerializerSpecific(serializers.ModelSerializer):
class Meta:
model = Product
fields = ('id','productName')
class CategoryProducts(serializers.ModelSerializer):
products = ProductSerializerSpecific(many=True, read_only=True)
class Meta:
model = Category
fields = ('id', 'products')

how to create multiple objects with one request DRF

I have the following models
class Product(models.Model):
name = models.CharField(null=True, blank=True, max_length=500)
category = models.CharField(null=True, blank=True, max_length=120)
class SpecificationName(models.Model):
product = models.ForeignKey(Product, on_delete=models.CASCADE, null=True, related_name='specifications')
name = models.CharField(max_length=125)
class Attribute(models.Model):
spec_name = models.ForeignKey(SpecificationName, on_delete=models.CASCADE, null=True, related_name='attributes')
index = models.CharField(max_length=200, blank=True, null=True)
value = models.CharField(max_length=250, blank=True, null=True)
after saving objects in Django admin I have an example
{
"name": "Apple Smart Watch",
"category": "IT",
"specifications": [
{
"name": "Test Data",
"attributes": [
{
"index": "test",
"value": "test2"
},
{
"index": "test7",
"value": "test8"
},
{
"index": "test9",
"value": "test10"
}
]
},
{
"name": "Test Data Continued",
"attributes": [
{
"index": "bla",
"value": "bla1"
},
{
"index": "bla 2",
"value": "bla 4"
},
{
"index": "test9",
"value": "test10"
}
]
},
{
"name": "Test Spec",
"attributes": []
}
]
}
I need to save this kind of object with one request but I am failing to do this
my serializer looks like this
class ProductSerializer(serializers.ModelSerializer):
specifications = SpecNameSerializer(many=True)
# attributes = AttributeSerializer(many=True, required=False)
class Meta:
model = Product
fields = ['name', 'category', 'brand', 'price', 'specifications']
def create(self, validated_data):
specs = validated_data.pop('specifications')
instance = Product.objects.create(**validated_data)
for spec in specs:
SpecificationName.objects.create(product=instance, **spec)
print(spec)
return instance
with this code, I am getting the following result but not as expected
{
"name": "Appel watch series",
"specifications": [
{
"name": "Test Data",
"attributes": []
},
{
"name": "Test Data comn",
"attributes": []
},
{
"name": "Test Spec",
"attributes": []
}
]
}
it cannot write into attributes
I searched for many answers but I did not find or applied some of them, again it did not help me. I am using just ListCreateView in the views. Please is there anybody who can help solve this problem. Thanks in advance!
SOLVED
I am using here ModelSerializer instead I used Serializer and added some changes here my answer and it worked
class AttributeSerializer(serializers.Serializer):
index = serializers.CharField(max_length=200)
value = serializers.CharField(max_length=200)
class SpecNameSerializer(serializers.ModelSerializer):
attributes = AttributeSerializer(many=True)
class Meta:
model = SpecificationName
fields = '__all__'
class ProductSerializer(serializers.ModelSerializer):
specifications = SpecNameSerializer(many=True)
class Meta:
model = Product
fields = ['name', 'category', 'brand', 'price', 'specifications']
def create(self, validated_data):
specs = validated_data.pop('specifications')
instance = Product.objects.create(**validated_data)
for spec in specs:
SpecificationName.objects.create(product=instance, **spec)
attrs = spec.pop('attributes')
for attr in attrs:
Attribute.objects.create(spec_name=spec, **attr)
return instance

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

Could not show meal based on meal categories(Django)

I want to show meal based on the meal categories. I am using django rest framework for Rest API. I can show meat but not based on categories. The models are Restaurant, Meal and Meal Cateogies.
here is my model
class Restaurant(models.Model):
name = models.CharField(max_length=150, help_text="name of restaurant")
slug = models.SlugField(max_length=150)
class Meal(models.Model):
restaurant = models.ForeignKey(Restaurant)
meal_category = models.ForeignKey('MealCategory')
name = models.CharField(max_length=120, help_text="name of the meal")
def __str__(self):
return self.name
class MealCategory(models.Model):
name = models.CharField(max_length=80, help_text="name of the category of meal")
slug = models.SlugField(max_length=80)
Serializers.py
class MealCategorySerializer(ModelSerializer):
# meal = SerializerMethodField()
class Meta:
model = MealCategory
class MealSerializer(ModelSerializer):
meal_category = MealCategorySerializer(read_only=True)
class Meta:
model = Meal
def get_meal(self, obj):
print('object of meal',obj)
instance = MealCategory.objects.get(slug=str(obj.slug))
meal_qs = instance.meal_set.filter(available=True)
meal = MealSerializer(meal_qs, many=True).data
return meal
class RestaurantSerializer(ModelSerializer):
owner = SerializerMethodField()
meal = SerializerMethodField()
class Meta:
model = Restaurant
read_only = ('id',)
def get_owner(self, obj):
return str(obj.owner)
def get_meal(self, obj):
print('object of meal',obj)
restaurant = Restaurant.objects.get(slug=str(obj.slug))
meal_qs = restaurant.meal_set.filter(available=True)
meal = MealSerializer(meal_qs, many=True).data
return meal
my API right now looks like this
"meal": [
{
"id": 1,
"meal_category": {
"id": 1,
"name": "veg",
"slug": "veg"
},
"name": "Mushroom drumstick",
"slug": "mushroom-drumstick",
},
{
"id": 2,
"meal_category": {
"id": 2,
"name": "Non-veg",
"slug": "non-veg"
},
"name": "Ham Cheesy Burger",
"slug": "ham-cheesy-burger",
"restaurant": 1
}
],
"name": "Kathmandu Fast Food Center",
"slug": "kathmandu-fast-food-center",
But I want meal to be shown like this
{
"id":1,
"name":"kathmandu fast food center",
"slug": "kathmandu-fast-food-center",
"meal_category":[
{
"Non veg":[
"meal":[
{
"name":"Ham Burger"
},
{
"name":"Chicken BBQ"
}
]
],
"Veg":[
"meal":[
{
"name":"mushroom soup"
},
{
"name":"Aloo Drumstick"
}
]
]
}
]
}
Please guide me how can i design such API.

django-tastypie : Related data not saving

My models.py
class Orders(models.Model):
order_id = models.CharField(max_length=7, primary_key=True)
users = models.ForeignKey(ProductUsers, on_delete=models.DO_NOTHING)
address = models.ForeignKey(ProductUsersAddress, on_delete=models.DO_NOTHING)
payment_method = models.CharField(default='COD', max_length=20, choices=PAYMENT_METHOD)
class OrderedProduct(models.Model):
products = models.ForeignKey(Products, on_delete=models.DO_NOTHING)
orders = models.ForeignKey(Orders, on_delete=models.CASCADE)
quantity = models.IntegerField(default=0)
price = models.DecimalField(default=0.00, max_digits=5, decimal_places=2, blank=False)
My resources.py
class OrdersResource(ModelResource):
ordered_products = fields.ToManyField('orders.resources.OrderedProductResource',
attribute=lambda bundle: OrderedProduct.objects.filter(orders=bundle.obj),
related_name='orders', full=True, null=True)
contact_no = fields.ForeignKey(ProductUsersResource, 'users')
address = fields.ForeignKey(ProductUsersAddressResource, 'address')
class Meta:
queryset = Orders.objects.all()
resource_name = 'orders'
include_resource_uri = False
collection_name = 'orders'
allowed_methods = ['get', 'post']
always_return_data = True
class OrderedProductResource(ModelResource):
products = fields.ForeignKey(ProductsResource, 'products')
orders = fields.ForeignKey(OrdersResource, 'orders')
class Meta:
queryset = OrderedProduct.objects.all()
resource_name = 'ordered_products'
excludes = ['id']
include_resource_uri = False
I entered data using Django-Admin.
When I hit, http://localhost:8000/orders/, I get,
{
"orders": [
{
"address": "/api/v1/address/1",
"contact_no": "/api/v1/users/8269661606",
"order_id": "KJLSWI",
"ordered_products": [
{
"orders": "/api/v1/orders/KJLSWI",
"price": "40.00",
"products": "/api/v1/products/1",
"quantity": 2
},
{
"orders": "/api/v1/orders/KJLSWI",
"price": "70.00",
"products": "/api/v1/products/2",
"quantity": 4
},
{
"orders": "/api/v1/orders/KJLSWI",
"price": "67.00",
"products": "/api/v1/products/3",
"quantity": 7
}
],
"payment_method": "COD",
}
]
}
Now according to tasty documentation,
Tastypie encourages “round-trippable” data, which means the data you
can GET should be able to be POST/PUT’d back to recreate the same
object.
If you’re ever in question about what you should send, do a GET on
another object & see what Tastypie thinks it should look like.
But when I post same data by just changing primary-key,
{
"address": "/api/v1/address/1",
"contact_no": "/api/v1/users/8269661606",
"order_id": "ABCDE",
"ordered_products": [
{
"orders": "/api/v1/orders/ABCDE",
"price": "40.00",
"products": "/api/v1/products/1",
"quantity": 2
},
{
"orders": "/api/v1/orders/ABCDE",
"price": "70.00",
"products": "/api/v1/products/2",
"quantity": 4
},
{
"orders": "/api/v1/orders/ABCDE",
"price": "67.00",
"products": "/api/v1/products/3",
"quantity": 7
}
],
"payment_method": "COD",
}
I get response,
{
"address": "/api/v1/address/1",
"contact_no": "/api/v1/users/8269661606",
"ordered_products": [],
"payment_method": "COD",
}
My data in model OrderedProduct, is not getting saved. WHYYYYYYY ??????
Try this:
def hydrate_m2m(self, bundle):
for ordered_product in bundle.data['ordered_products']:
if isinstance(ordered_product, dict):
ordered_product.update({'orders': bundle.obj})
return super(OrdersResource, self).hydrate_m2m(bundle)
and remove orders key from ordered_products JSON
and replace attribute=lambda bundle: OrderedProduct.objects.filter(orders=bundle.obj) with orders
and move related_name='orders' from resource to model.
Finally:
model:
class OrderedProduct(models.Model):
products = models.ForeignKey(Products, on_delete=models.DO_NOTHING)
orders = models.ForeignKey(Orders, on_delete=models.CASCADE, related_name='orders')
quantity = models.IntegerField(default=0)
price = models.DecimalField(default=0.00, max_digits=5, decimal_places=2, blank=False)
resource:
class OrdersResource(ModelResource):
ordered_products = fields.ToManyField('orders.resources.OrderedProductResource',
'orders', full=True, null=True)
contact_no = fields.ForeignKey(ProductUsersResource, 'users')
address = fields.ForeignKey(ProductUsersAddressResource, 'address')
class Meta:
queryset = Orders.objects.all()
resource_name = 'orders'
include_resource_uri = False
collection_name = 'orders'
allowed_methods = ['get', 'post']
always_return_data = True
def hydrate_m2m(self, bundle):
for ordered_product in bundle.data['ordered_products']:
if isinstance(ordered_product, dict):
ordered_product.update({'orders': bundle.obj})
return super(OrdersResource, self).hydrate_m2m(bundle)
and POST data:
{
"address": "/api/v1/address/1",
"contact_no": "/api/v1/users/8269661606",
"order_id": "ABCDE",
"ordered_products": [
{
"price": "40.00",
"products": "/api/v1/products/1",
"quantity": 2
},
{
"price": "70.00",
"products": "/api/v1/products/2",
"quantity": 4
},
{
"price": "67.00",
"products": "/api/v1/products/3",
"quantity": 7
}
],
"payment_method": "COD",
}

Categories