I've got established a relationship between 2 models: Order and OrderLine. I've created serializers for both of them following the DRF documentation, yet when printing the serializer.data the nested objects don't show.
Here are my models:
class Order(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
session_id = models.CharField(max_length=256)
class OrderLine(models.Model):
order_id = models.ForeignKey(Order, on_delete=models.DO_NOTHING)
product_id = models.ForeignKey(Product, on_delete=models.DO_NOTHING)
price = models.DecimalField(decimal_places=2, max_digits=20)
quantity = models.IntegerField()
total = models.DecimalField(decimal_places=2, max_digits=20)
created_at = models.DateField(auto_now_add=True)
updated_at = models.DateField(auto_now=True)
These are the serializers:
from rest_framework import serializers
from .models import Order, OrderLine
class OrderLineSerializer(serializers.ModelSerializer):
"""
OrderLine serializer
"""
class Meta:
model = OrderLine
fields = ['product_id', 'price', 'quantity', 'total']
class OrderSerializer(serializers.ModelSerializer):
"""
Order serializer
"""
items = OrderLineSerializer(many=True, read_only=True)
class Meta:
model = Order
fields = ['session_id', 'subtotal', 'total', 'items']
read_only_fields = ['id']
and this is the view:
class OrderAPIViewSet(viewsets.ViewSet):
def create(self, request):
order = Order.objects.create(session_id=request.data['session_id'])
for item in request.data['items']:
product = Product.objects.get(pk=item['product_id'])
total = Decimal(item['price'] * item['quantity'])
OrderLine.objects.create(
order_id=order,
product_id=product,
price=Decimal(item['price']),
quantity=item['quantity'],
total=total
)
serializer = OrderSerializer(instance=order)
print("HERE")
print(serializer.data)
return Response(status=status.HTTP_200_OK)
From my REST client I'm posting the following object:
{
"session_id":uuid,
"items": [
{
"product_id": product.id,
"price": 5.80,
"quantity": 2,
}
]
}
but when the print statement in the view is executed this is what's being printed:
{
"session_id":"4def7bdb-dedb-46aa-9c70-1d9e4f522149",
"subtotal":"0.00",
"total":"0.00"
}
notice the the items subresource is not being included.
What am i missing?
Because you passed read_only=True parameter to OrderLineSerializer. When your serializer's builtin method to_internal_value() run, clear data not including in your fields list. For more details: read_only
Related
I have a field which is ManyToMany. I would like to enter the value in POSTMAN for API post operation. But everytime It says: "This field is required." even though I provided the value.
Models:
class Day(models.Model):
day_name = models.CharField(
_("Day Name"), max_length=255, null=True, blank=True)
def __str__(self):
return self.day_name
class TutorProfile(models.Model):
user = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
tutor_availablility = models.ManyToManyField(
Day,blank=True)
Serializer:
class DaySerializer(serializers.ModelSerializer):
class Meta:
model = Day
fields = '__all__'
class TutorProfileSerializer(serializers.ModelSerializer):
user = serializers.PrimaryKeyRelatedField(
read_only=True, default=serializers.CurrentUserDefault(), source='user.username')
image_url = serializers.SerializerMethodField('get_image_url')
tutor_availablility = DaySerializer(many=True)
class Meta:
model = TutorProfile
fields = '__all__'
Viewsets:
#authentication_classes([TokenAuthentication])
#permission_classes([IsAuthenticated])
class TutorprofileViewSet(ModelViewSet):
serializer_class = TutorProfileSerializer
http_method_names = ["post", "delete", "get"]
queryset = TutorProfile.objects.all()
With the following models.py (notice that your current Day.__str__ can raise an exception if day_name does not exist):
class Day(models.Model):
day_name = models.CharField(_("Day Name"), max_length=255, blank=True, null=True)
def __str__(self):
return self.day_name if self.day_name else "Unnamed"
class TutorProfile(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
tutor_availability = models.ManyToManyField(Day, blank=True)
You do not need to explicitly add tutor_availability nor user as serializer fields:
class DaySerializer(serializers.ModelSerializer):
class Meta:
model = Day
fields = "__all__"
class TutorProfileSerializer(serializers.ModelSerializer):
# Omitting `image_url` as not reflected in `models.py`
# image_url = serializers.SerializerMethodField('get_image_url')
class Meta:
model = TutorProfile
fields = "__all__"
With this viewset:
#authentication_classes([TokenAuthentication])
#permission_classes([IsAuthenticated])
class TutorProfileViewSet(ModelViewSet):
serializer_class = TutorProfileSerializer
http_method_names = ["post", "delete", "get"]
queryset = TutorProfile.objects.all()
Then, after creating days with IDs 1 and 2 in admin, by sending the tutor_availability field as you are doing it, it should work. Request:
{
"user": 1,
"tutor_availability": [1, 2]
}
Response:
{
"id": 1,
"user": 1,
"tutor_availability": [
1,
2
]
}
Notice as well that I've changed availablility to availability and that it may be unsafe to allow authenticated users to pass the user field in the request, you may want to infer that from the user who makes the request.
In your TutorProfileSerializer you are using the DaySerializer for tutor_availablility field so when you do a post request your post action will wait for a list of dict, what you you need to do in first is to delete this line : from your TutorProfileSerializer and it will works.
tutor_availablility = DaySerializer(many=True)
If you still have the problem then you need to verify the validate method of the TutorProfileSerializer.
And if it works but you want a list of dict(of Day object) for GET request, you need to override the get_serializer_class() of your ViewSet and create two serializers one for post request and a second for get request:
#authentication_classes([TokenAuthentication])
#permission_classes([IsAuthenticated])
class TutorprofileViewSet(ModelViewSet):
serializer_class = TutorProfileSerializer
http_method_names = ["post", "delete", "get"]
queryset = TutorProfile.objects.all()
def get_serializer_class(self):
if self.action.method == 'GET':
return TutorGETProfileSerializer
return super(TutorprofileViewSet, self).get_serializer_class()
and the 2 serializers:
class TutorGETProfileSerializer(serializers.ModelSerializer):
user = serializers.PrimaryKeyRelatedField(
read_only=True, default=serializers.CurrentUserDefault(), source='user.username')
image_url = serializers.SerializerMethodField('get_image_url')
tutor_availablility = DaySerializer(many=True)
class Meta:
model = TutorProfile
fields = '__all__'
class TutorProfileSerializer(serializers.ModelSerializer):
class Meta:
model = TutorProfile
fields = '__all__'
read_only_fields = ('user',)
I have created model with many to many relationship and I have join table when I keep additional variable for it:
class BorderStatus(models.Model):
STATUS_CHOICES = [("OP", "OPEN"), ("SEMI", "CAUTION"), ("CLOSED", "CLOSED")]
origin_country = models.ForeignKey(OriginCountry, on_delete=models.CASCADE, default="0")
destination = models.ForeignKey(Country, on_delete=models.CASCADE, default="0")
status = models.CharField(max_length=6, choices=STATUS_CHOICES, default="CLOSED")
extra = 1
class Meta:
unique_together = [("destination", "origin_country")]
verbose_name_plural = "Border Statuses"
def __str__(self):
return (
f"{self.origin_country.origin_country.name} -> {self.destination.name}"
f" ({self.status})"
)
Other models:
# Create your models here.
class Country(models.Model):
name = models.CharField(max_length=100, unique=True, verbose_name='Country')
class Meta:
verbose_name_plural = "Countries"
def __str__(self):
return self.name
class OriginCountry(models.Model):
origin_country = models.ForeignKey(
Country, related_name="origins", on_delete=models.CASCADE
)
destinations = models.ManyToManyField(
Country, related_name="destinations", through="BorderStatus"
)
class Meta:
verbose_name_plural = "Origin Countries"
def __str__(self):
return self.origin_country.name
Here is my serializer for the endpoint:
class BorderStatusEditorSerializer(serializers.ModelSerializer):
"""Create serializer for editing single connection based on origin and destination name- to change status"""
origin_country = serializers.StringRelatedField(read_only=True)
destination = serializers.StringRelatedField(read_only=True)
class Meta:
model = BorderStatus
fields = ('origin_country', 'destination', 'status')
And my endpoint:
class BorderStatusViewSet(viewsets.ModelViewSet):
queryset = BorderStatus.objects.all()
serializer_class = BorderStatusEditorSerializer
filter_backends = (DjangoFilterBackend,)
filter_fields=('origin_country','destination')
The problem Im having is that I cant create any new combination for the BorderStatus model in this serializer via post request.
If I remove the lines:
origin_country = serializers.StringRelatedField(read_only=True)
destination = serializers.StringRelatedField(read_only=True)
Then the form will work, but then I wont have the string representation of those variables, instead I get IDs.
Is there any way to allow request to accept origin_country and destination while being related fields?
EDIT:
To clarify how OriginCountry works, it is has a nested field:
[{ "id": 1
"origin_country": "Canada",
"dest_country": [
{
"id": 1,
"name": "France",
"status": "CLOSED"
},
{
"id": 2,
"name": "Canada",
"status": "OP"
}
]
},
]
You can try to override perform_create method of the viewset to make the necessary adjustments on-the-fly when new entry is posted:
class BorderStatusViewSet(viewsets.ModelViewSet):
queryset = BorderStatus.objects.all()
serializer_class = BorderStatusEditorSerializer
filter_backends = (DjangoFilterBackend,)
filter_fields=('origin_country','destination')
def perform_create(self, serializer):
origin_country, _ = models.Country.get_or_create(name=self.request.data.get('origin_country')
destination, _ = models.Country.get_or_create(name=self.request.data.get('destination')
return serializer.save(origin_country=origin_country, destination=destination)
Maybe you will also need to adjust your serializer to have:
class CountrySerializer(serializers.ModelSerializer):
class Meta:
model = Country
fields = ['name']
class BorderStatusEditorSerializer(serializers.ModelSerializer):
origin_country = CountrySerializer()
destination = CountrySerializer()
...
Yes, I will try to give this combination.
You get this error because of Incorrect Type exception. Django checks data type validation on the serializer. For example here your dest_country returns a list of dicts but in your model it is a primary key (pk)
That's why on post django says : pk value expected, list received
But you can solve this error by using two different serializers (one to post another by default)
1. Create two different serializers
class BorderStatusEditorSerializer(serializers.ModelSerializer):
"""The First serialiser by default"""
origin_country = serializers.StringRelatedField(read_only=True)
destination = serializers.StringRelatedField(read_only=True)
class Meta:
model = BorderStatus
fields = ('origin_country', 'destination', 'status')
class BorderStatusEditorCreateSerializer(serializers.ModelSerializer):
"""The Second serialiser for create"""
class Meta:
model = BorderStatus
fields = ('origin_country', 'destination', 'status')
2.Add get_serializer_class method to for Viewset
class BorderStatusViewSet(viewsets.ModelViewSet):
queryset = BorderStatus.objects.all()
filter_backends = (DjangoFilterBackend,)
filter_fields=('origin_country','destination')
serializer_classes = {
'create': BorderStatusEditorCreateSerializer, # serializer used on post
}
default_serializer_class = BorderStatusEditorSerializer # Your default serializer
def get_serializer_class(self):
return self.serializer_classes.get(self.action, self.default_serializer_class)
Order table
class Orders(models.Model):
restaurant = models.ForeignKey(Restaurant, on_delete=models.CASCADE, blank=True, null=True)
tableid=models.IntegerField()
orderid=models.IntegerField()
total_amount = models.DecimalField(max_digits=10, decimal_places=2)
Articles table to save articles like pizza
class OrderArticle(models.Model):
order = models.ForeignKey(Orders, on_delete=models.CASCADE)
article = models.ForeignKey(Articles, on_delete=models.CASCADE)
# article_options = models.ManyToManyField(ArticlesOptions)
Article options to save extra topping or any option available
class OrderArticleOptions(models.Model):
# order = models.ForeignKey(Orders, on_delete=models.CASCADE)
article_option = models.ForeignKey(ArticlesOptions, on_delete=models.CASCADE)
order_article = models.ForeignKey(OrderArticle, on_delete=models.CASCADE)
quantity = models.IntegerField(default=1)
price = models.DecimalField(max_digits=10, decimal_places=2)
EDIT
Article Option table
class ArticlesOptions(models.Model):
articleoptionrestaurant = models.ForeignKey(Restaurant, on_delete=models.CASCADE , blank=True, null=True)
optionname = models.ForeignKey(ArticlesOptionsName, on_delete=models.CASCADE, related_name="optionnames")
min = models.IntegerField()
max = models.IntegerField()
choice_price = models.DecimalField(max_digits=10, decimal_places=2)
choice = models.CharField(max_length=255)
def __str__(self):
return str(self.optionname)
So Now issue is When I try to get all data in one serialize I am not able to get . I am using this example to get
https://www.django-rest-framework.org/api-guide/relations/
EDIT
My serilizers are
class OrderSerializer(serializers.ModelSerializer):
restaurant=RestaurantSerializer(required=False)
class Meta:
model = Orders
fields = ['restaurant','tableid', 'orderid', 'total_amount']
class ArticlesSerializer(serializers.ModelSerializer):
order = OrderSerializer(read_only=True)
article=ListArticleSerializer(read_only=True)
class Meta:
model = OrderArticle
fields = ['order', 'article']
class ArticlesOptionSerializer(serializers.ModelSerializer):
article_option = ListCategoriesSerializer( read_only=True)
order_article=ArticlesSerializer(read_only=True)
class Meta:
model = OrderArticleOptions
fields = ['article_option','order_article','quantity','price']
depth=1
My view.py is
class OrderedArticles(APIView):
def get(self, request, restid):
Options=OrderArticleOptions.objects.filter(order_article=1)
orderserlizer=ArticlesOptionSerializer(Options , many=True)
return Response(success_response({'orders': orderserlizer.data},
"Restaurant with this all data."), status=status.HTTP_200_OK)
My JSON Response is
"article_option":{ },
"order_article":{
"order":{
"restaurant":{ },
"tableid":12,
"orderid":1,
"total_amount":"0.00"
},
"article":{
"id":1,
"category":{ },
"ingredient":[ ],
"articleoptionnames":{ },
"restaurant":{ },
"articlename":"Article1",
"price":"1.90",
"pickuptax":6,
"dineintax":21,
"description":"This is a tekst field with more information about the product",
"image":"/media/Article/c1.264f3b28_sxcPiqi.png"
}
},
While I want these "article_option" to be as child of article like Article {article_option1, article_option2} but its creating new objects with every new article option.
If I understand you correctly, you want to return a representation of OrderArticle which have ArticleOption objects as its children. Which means you should instantiate an ArticlesSerializer in your view, but also modify ArticlesSerializer so that it includes all related article_options as a list (using the source attribute). Something like the following:
class ArticlesOptionSerializer(serializers.ModelSerializer):
article_option = ListCategoriesSerializer(read_only=True)
class Meta:
model = OrderArticleOptions
fields = ['article_option', 'order_article', 'quantity', 'price']
class ArticlesSerializer(serializers.ModelSerializer):
order = OrderSerializer(read_only=True)
article = ListArticleSerializer(read_only=True)
article_options = ArticlesOptionSerializer(read_only=True, source='orderarticleoptions_set', many=True)
class Meta:
model = OrderArticle
fields = ['order', 'article', 'article_options']
Then in your view, you should instantiate your ArticlesSerializer with the appropriate OrderArticle object:
class OrderedArticles(APIView):
def get(self, request, restid):
order_article = OrderArticle.objects.get(pk=1) # get pk/id from request
serializer = ArticlesSerializer(order_article)
return Response(serializer.data, status=status.HTTP_200_OK)
I'm creating an API which list and save transactions. My Transactions Model has a FK to a Category model. My goal when creating a Transaction is to also create the Category if the category is new.
The first time I create the transaction it successfully creates the new category and transaction. The next time I attempt the create a transaction with the existing category, I get an error that the category already exists. In my Transaction serializer I added a create method that should be using get_or_create for the category. However I'm still getting an error on my unique fields. My expectation is that it would be returning the existing Category.
It seems like it's throwing the error before it gets to the create method in the Transaction serializer before it has a chance to use get_or_create.
Models:
class Category(models.Model):
name = models.CharField(max_length=128, null=False)
owner = models.ForeignKey(settings.AUTH_USER_MODEL,
on_delete=models.CASCADE)
created_time = models.DateTimeField(auto_now_add=True)
modified_time = models.DateTimeField(auto_now=True)
class Meta:
unique_together = ('name', 'owner')
class Transaction(models.Model):
date = models.DateField()
payee = models.CharField(max_length=256)
category = models.ForeignKey(Category,
related_name='category',
on_delete=models.CASCADE)
amount = MoneyField(max_digits=19,
decimal_places=2,
default_currency='USD')
created_time = models.DateTimeField(db_index=True,
auto_now_add=True)
modified_time = models.DateTimeField(auto_now=True)
Serializers:
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = ('id', 'name', 'owner', 'created_time', 'modified_time')
class TransactionSerializer(serializers.ModelSerializer):
balance = serializers.DecimalField(
decimal_places=2, max_digits=19, read_only=True)
category = CategorySerializer(many=False, read_only=False)
class Meta:
model = Transaction
fields = ('id', 'date', 'payee', 'category',
'amount', 'balance', 'created_time', 'modified_time',
'is_cleared', 'paid_or_deposited')
def create(self, validated_data):
category_data = validated_data.pop('category')
category, created = Category.objects.get_or_create(**category_data)
transaction = Transaction.objects.create(category=category,
**validated_data)
return transaction
POST:
{
"date": "2018-12-19",
"payee": "Test",
"category": {"owner": 1, "name": "TEST"},
"amount": "-134"
}
Error:
{
"category": {
"non_field_errors": [
"The fields name, owner must make a unique set."
]
}
}
You're right about not reaching your create() method.
This happens because ModelSerializer by default creates validators based on your model Meta.unique_together value: https://www.django-rest-framework.org/api-guide/serializers/#modelserializer
Simplest way to disable this type of validators is to override get_unique_together_validators for your serializer:
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = ('id', 'name', 'owner', 'created_time', 'modified_time')
def get_unique_together_validators(self):
return []
Another solution, which is cleaner is to override Meta.validations of your CategorySerializer*:
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = ('id', 'name', 'owner', 'created_time', 'modified_time')
validators = []
* be aware that this will disable serializer validators unique_for_date, unique_for_month and unique_for_year that come from model
I have the model structure like below
class BaseProduct:
id = models.CharField(max_length=15)
name = models.CharField(max_length=20)
class Product
base_product = ForeigKey(BaseProduct)
name = models.CharField(max_length=20)
class Condition:
category = models.ForeignKey(Product, related_name='allowed_product')
check = models.IntegerField(default=0)
allow = models.PositiveSmallIntegerField(default=1)
The query:
Product.objects.filter(condition__allow=1, condition__check=1)
I want format something like below
Base Product and inside that list of products based on allow and check filter
[
{
"name": "BaseProduct 1",
"products": [
{
"name": "TV",
}, {}, ....
]
},
........
]
Try it, if you use django rest framework
from rest_framework import serializers
from rest_framework.fields import empty
from django.utils.functional import cached_property
class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = ('name')
class BaseProductSerializer(serializers.ModelSerializer):
products = serializers.SerializerMethodField()
class Meta:
model = BaseProduct
fields = ('name', 'products')
def __init__(self, instance=None, data=empty, **kwargs):
self._condition_allow = kwargs.pop('condition_allow', 1)
super(BaseProductSerializer, self).__init__(instance=None, data=empty, **kwargs)
#cached_property
def _request_data(self):
request = self.context.get('request')
# if POST
# return request.data if request else {}
# if GET params
return request.query_params if request else {}
#cached_property
def _condition(self):
return self._request_data.get('CONDITION_PARAM_NAME')
def get_products(self, obj):
qs = obj.product_set.filter(condition__allow=self._condition_allow, condition__check=1)
serializer = ProductSerializer(qs, many=True)
# ^^^^^
return serializer.data
in view
serialiser(qs, condition_allow=5)
Change your models to have related_name for the foreignkeys to have reverse relationship:
class BaseProduct:
id = models.CharField(max_length=15)
name = models.CharField(max_length=20)
class Product
base_product = ForeigKey(BaseProduct, related_name='products')
name = models.CharField(max_length=20)
class Condition:
category = models.ForeignKey(Product, related_name='conditions')
check = models.IntegerField(default=0)
allow = models.PositiveSmallIntegerField(default=1)
So now you can use it in your serializers:
class BaseProductSerializer:
class Meta:
model = BaseProduct
fields = ('name', 'products',)
class ProductSerializer:
class Meta:
model = Product
fields = ('conditions',)
class ConditionSerializer:
class Meta:
model = Condition
fields = '__all__'
Finally in your views, change this:
Product.objects.filter(condition__allow=1, condition__check=1)
into this:
BaseProduct.objects.filter(products__conditions__allow=1, products__conditions__allow=1)
And hopefully, this should generate JSON data in the format that you asked for.