Deserialize Nested Object in Django-Rest-Framework - django

Good day,
I'm trying to do a PUT request that contains a nested-object and I can't get the province to update correctly. There didn't seem to be anything obvious in the django-rest-framework docs to help with this and I've investigated the solutions of a few other similar problems but none have helped (set many=false, change to ModelSerializer, specialized serializers, etc.).
Everything else about the address will update correctly and return a 200 response (no errors in django logs either). Am I wrong to assume that django-rest-framework handles this all for me for free? Do I have to override the update and create methods within the serializer to validate and save the nested-object?
I'm thinking it's because I have the province serializer set to read_only within the address serializer. However, if I remove the read_only modifier on the province serializer, it gives an error about the province already existing:
{
"province": {
"province": [
"valid province with this province already exists."
]
}
}
Which is behaviour I do not expect and don't know how to resolve. I am not trying to add or update the province. I just want to change the province code in the address.province field and I can't use a string "MB" because it expects an object. I effectively want this behaviour:
UPDATE agent_business_address
SET province = 'MB'
WHERE agent_id = 12345;
-- agent_business_address.province has a foreign key constraint on valid_province.province
-- valid_province is populated with all the 2-letter abbreviations for provinces(
I make this PUT request to /api/agent-business-address/
{
"address": "123 Fake St",
"agent_id": 12345,
"city": "Calgary",
"dlc": "2021-10-11 14:03:03",
"operator_id": 4,
"postal_code": "A1B 2C3",
"province": {
"description": "Manitoba",
"province": "MB"
},
"valid_address": "N"
}
That is received by this ViewSet:
class AgentBusinessAddressViewSet(viewsets.ModelViewSet):
queryset = AgentBusinessAddress.objects.all()
serializer_class = AgentBusinessAddressSerializer
Relevant serializers:
class AgentBusinessAddressSerializer(serializers.HyperlinkedModelSerializer):
province = ValidProvinceSerializer(read_only=True) # Removing the read_only causes the error above.
class Meta:
model = AgentBusinessAddress
fields = ('agent_id', 'operator_id', 'dlc', 'address', 'city', 'province', 'postal_code', 'valid_address')
class ValidProvinceSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = ValidProvince
read_only_fields = ('operator_id', 'dlc')
fields = ('province', 'description')
Relevant models:
class AgentBusinessAddress(models.Model):
agent = models.OneToOneField(Agent, models.DO_NOTHING, primary_key=True)
operator_id = models.SmallIntegerField()
dlc = models.DateTimeField()
address = models.CharField(max_length=100)
city = models.CharField(max_length=80)
province = models.ForeignKey('ValidProvince', models.DO_NOTHING, db_column='province')
postal_code = models.CharField(max_length=7)
valid_address = models.CharField(max_length=1)
class Meta:
managed = False
db_table = 'agent_business_address'
class ValidProvince(models.Model):
province = models.CharField(primary_key=True, max_length=2)
operator_id = models.SmallIntegerField()
dlc = models.DateTimeField()
description = models.CharField(max_length=30, blank=True, null=True)
class Meta:
managed = False
db_table = 'valid_province'
Any help would be appreciated.

Solved it with the help of the specialized serializers post after a few re-reads.
I updated the serializers to this:
# This gets called for non-GET requests.
class AgentBusinessAddressSerializer(serializers.ModelSerializer):
class Meta:
model = AgentBusinessAddress
fields = ('__all__')
# This get called for GET requests.
class AgentBusinessAddressReadSerializer(AgentBusinessAddressSerializer):
province = ValidProvinceSerializer(read_only=True)
class Meta:
model = AgentBusinessAddress
fields = ('__all__')
I updated the viewset to this:
class AgentBusinessAddressViewSet(viewsets.ModelViewSet):
queryset = AgentBusinessAddress.objects.all()
def get_serializer_class(self):
if self.request.method in ['GET']:
return AgentBusinessAddressReadSerializer
return AgentBusinessAddressSerializer
Now I just send the primary key for the province in the PUT request:
{
"address": "123 Fake St",
"agent": 10000003,
"city": "Calgary",
"dlc": "2021-10-11 19:47:38",
"operator_id": 4,
"postal_code": "A1B 2C3",
"province": "NS",
"valid_address": "N"
}
I get a PUT 200 response back and verify in the DB that the province is now 'NS'.
{
"agent": 10000003,
"operator_id": 4,
"dlc": "2021-10-11T19:47:38",
"address": "123 Fake St",
"city": "Calgary",
"postal_code": "A1B 2C3",
"valid_address": "N",
"province": "NS",
}

Related

Why does my "id" field disappear in my serializer (manytomany)?

I have a "PriceTable" object that can contain several "PriceLine" objects with a manytomany relationship.
I use django rest framework to publish an api and I would like that when I use PUT or PATCH on PriceTable, I can also modify the content of PriceLine.
The goal is to have a unique UPDATE method to be able to modify the instances of the 2 objects.
My models:
class PriceTable(models.Model):
name = models.CharField(_('Name'), max_length=255)
client = models.ForeignKey(
"client.Client",
verbose_name=_('client'),
related_name="price_table_client",
on_delete=models.CASCADE
)
lines = models.ManyToManyField(
'price.PriceLine',
verbose_name=_('lines'),
related_name="price_table_lines",
blank=True,
)
standard = models.BooleanField(_('Standard'), default=False)
class Meta:
verbose_name = _("price table")
verbose_name_plural = _("price tables")
def __str__(self):
return self.name
class PriceLine(models.Model):
price = models.FloatField(_('Price'))
product = models.ForeignKey(
"client.Product",
verbose_name=_('product'),
related_name="price_line_product",
on_delete=models.CASCADE,
)
class Meta:
verbose_name = _("price line")
verbose_name_plural = _("price line")
def __str__(self):
return f"{self.product.name} : {self.price} €"
I want to be able to send a JSON of this format to modify both the table and its lines:
{
"id": 16,
"lines": [
{
"id": 1,
"price": 20.0,
"product": 1
},
{
"id": 2,
"price": 45.0,
"product": 2
}
],
"name": "test"
}
For this, I try to override the update method of my serializer:
class PriceTableSerializer(serializers.ModelSerializer):
"""
PriceTableSerializer
"""
lines = PriceLineSerializerTest(many=True)
class Meta:
model = PriceTable
exclude = ['standard', 'client',]
def update(self, instance, validated_data):
instance.name = validated_data.get('name', instance.name)
lines = validated_data.get('lines')
print(lines)
# not python code
# for target_line in lines:
# if instance.id == target_line.id
# instance.price = target_line.price
# ...
return instance
The 5 commented lines, are the logic I would like to implement.
I want to browse the received array of rows and if the id of this row is equal to the id of a row in my instance, I change the values of this row.
The problem is that the id disappears. When I print the lines variable, I get this:
[OrderedDict([('price', 20.0), ('product', <Product: Test import2>)])]
OrderedDict([('price', 20.0), ('product', <Product: Test import2>)])
What happened to the id?
As docs says in https://www.django-rest-framework.org/api-guide/serializers/#customizing-multiple-update:
You will need to add an explicit id field to the instance serializer. The default implicitly-generated id field is marked as read_only. This causes it to be removed on updates. Once you declare it explicitly, it will be available in the list serializer's update method.
So you should declare id yourself for being able to use that:
class PriceLineSerializerTest(serializers.ModelSerializer):
id = serializers.IntegerField()
class Meta:
model = PriceLine
exclude = ['id', ...]

Post Foreign Key As Id but show Foreign Key Details in GET method

I am working in a drf & react app . Where i need to show details of a foreign key field but when in POST method i need post only ID.But its not norking for nested serilizer. Giving an error from frontend named "Expected Dictonary but got int".
output:
{
"id": 1,
"Hospital": {
"id": 1,
"User_Info": "bnsb#gmail.com",
"Manager_Name": "Jalal Uddin",
"Address": "Dhaka"
},
"FullName": "s",
"Age": "s",
Models.py:
class Hospital(models.Model):
User_Info = models.ForeignKey(User , on_delete=models.CASCADE)
Manager_Name = models.CharField(max_length= 40 , blank=True, null=True)
Address = models.CharField(max_length=40 , blank=True, null=True)
class Patient(models.Model):
Hospital = models.ForeignKey(Hospital , on_delete= models.CASCADE , default = 1)
FullName = models.CharField(max_length = 50 , blank = False , null = False , default = "")
Age = models.CharField(max_length=12 , blank=True, null=True)
def __str__(self):
return str(self.User_Info.Hospital_Name)
Serilizers.py:
class HospitalDetailsForPatientSerializer(serializers.ModelSerializer):
User_Info = CustomUserSerializer(read_only= True)
class Meta:
model = Hospital
fields = "__all__"
extra_kwargs = {"Address":{'read_only': True} ,"Manager_Name":{'read_only' : True}}
I got the expected output, Now I need to POST the only ID of Hospital Model when I save the patient but it's not working.
I solved the problem by defining the to_representation method in the parent serializer. Now I can see the foreign key details in the 'GET' method but when I want to POST foreign it needs only ID.
Serializers.py :
class HospitalDetailsForPatientSerializer(serializers.ModelSerializer):
User_Info = CustomUserSerializer()
class Meta:
model = Hospital
fields = ['User_Info']
class PatientSerializer(serializers.ModelSerializer):
class Meta:
model = Patient
fields = "__all__"
def to_representation(self, instance):
response = super().to_representation(instance)
response['Hospital'] = HospitalDetailsForPatientSerializer(instance.Hospital).data
return response

Different write and read operations with DRF

I am using Django Rest Framework for a project and I am running into a problem. When the frontend creates a Team they want to reference all relationships with an ID, but when getting the Team, they want the data from the relationship. How can I achieve this?
models:
class Team(models.Model):
class Meta:
db_table = "team"
team_id = models.AutoField(primary_key=True)
name = models.CharField(max_length=100)
organization = models.ForeignKey(Organization, on_delete=models.CASCADE)
class Organization(models.Model):
class Meta:
db_table = "organization"
organization_id = models.AutoField(primary_key=True)
name = models.CharField(max_length=100)
class Position(models.Model):
class Meta:
db_table = "position"
position_id = models.AutoField(primary_key=True)
team = models.ForeignKey(Team, on_delete=models.CASCADE, related_name="positions")
class Player(model.Model):
class Meta:
db_table = "player"
player_id = models.AutoField(primary_key=True)
name = models.CharField(max_length=100)
positions = models.ManyToManyField(Position, related_name="players")
serializers:
class TeamSerializer(serializers.ModelSerializer):
class Meta:
model = Team
fields = ["team_id", "name", "organization", "positions"]
positions = PositionSerializer(many=True) # This is merely for output. There is no need to create a position when a team is created.
organization = OrganizationSerializer() # Since an organization already exists I'd like to just give an organization_id when creating/editing a team.
# I don't think the other serializers matter here but can add them on request.
So when doing POST or PATCH on a team, I'd like the front end to be able to pass this payload
{
"name": "My Team",
"organization": 1
}
but when doing a GET on a team, I'd like the front end to receive this response.
{
"team_id": 1,
"name": "My Team",
"organization": {
"organization_id": 1,
"name": "My Organization"
},
"positions": [{
"position_id": 1,
"players": [{
"player_id": 1,
"name": "Member 1"
}
]
}
Is there a a way to achieve this?
In such situations define two serializers, one is for read operations and one is for write operations.
class TeamWriteSerializer(serializers.ModelSerializer):
# see, here no nested relationships...
class Meta:
model = Team
fields = ["name", "organization"]
class TeamReadSerializer(serializers.ModelSerializer):
class Meta:
model = Team
fields = ["team_id", "name", "organization", "positions"]
positions = PositionSerializer(many=True)
organization = OrganizationSerializer()
and now, use these two serializers properly in your views. For example, I hope you are using the ModelViewSet in views,
class TeamModelViewSet(viewsets.ModelViewSet):
def get_serializer_class(self):
if self.request.method.lower() == 'get':
return TeamReadSerializer
else:
return TeamWriteSerializer

Add related ForeignKey fields with serializer in Django REST Framework

I'm using Django 2.2 and Django REST Framework
I have three models like
class Plan(models.Model):
name = models.CharField(_('Plan Name'), max_length=100)
default = models.NullBooleanField(default=None, unique=True)
created = models.DateTimeField(_('created'), db_index=True)
quotas = models.ManyToManyField('Quota', through='PlanQuota')
class Quota(models.Model):
codename = models.CharField(max_length=50, unique=True)
name = models.CharFieldmax_length=100)
unit = models.CharField(max_length=100, blank=True)
class PlanQuota(models.Model):
plan = models.ForeignKey('Plan', on_delete=models.CASCADE)
quota = models.ForeignKey('Quota', on_delete=models.CASCADE)
value = models.IntegerField(default=1, null=True, blank=True)
I have to get all quota and their value from PlanQuota in the plan serializer while getting a list of plans.
I have the following serializer
class PlanQuotaSerialier(serializers.ModelSerializer):
class Meta:
model = PlanQuota
depth = 1
fields = ['quota', 'value']
class PlanListSerializer(serializers.ModelSerializer):
plan_quota = PlanQuotaSerialier(read_only=True, many=True)
class Meta:
model = Plan
depth = 1
fields = ['name', 'default', 'created', 'plan_quota']
But there is no plan_quota in the response.
How can I add all Quota and their value for each plan in a single query (SQL JOIN)?
Edit 2:
Adding source to the serializer field worked
plan_quota = PlanQuotaSerialier(source='planquota_set', many=True)
And the result is like
"results": [
{
"name": "Test Plan 1",
"default": true,
"plan_quotas": [
{
"quota": {
"id": 1,
"order": 0,
"codename": "TEST",
"name": "Test Domain",
"unit": "count",
"description": "",
"is_boolean": false,
"url": ""
},
"value": 10
},
]
}
]
Can I club all fields from quota with value field in the plan_quotas list?
class PlanQuota(models.Model):
plan = models.ForeignKey('Plan', on_delete=models.CASCADE, related_name='plan_quotas')
quota = models.ForeignKey('Quota', on_delete=models.CASCADE)
value = models.IntegerField(default=1, null=True, blank=True)
class PlanListSerializer(serializers.ModelSerializer):
class Meta:
model = Plan
depth = 1
fields = ['name', 'default', 'created', 'plan_quotas']
This is how I got it solved.
For the first query, added source
plan_quota = PlanQuotaSerialier(source='planquota_set', many=True)
For removing quota key, added to_presentation() in the PlanQuotaSerializer
def to_representation(self, instance):
representation = super().to_representation(instance)
if 'quota' in representation:
representation['quota']['quota_id'] = representation['quota'].pop('id')
representation.update(representation.pop('quota'))
return representation

Using reverse relationships with django-rest-framework's serializer

My model looks like this:
class User(TimestampedModel):
name = models.CharField(max_length=30, null=False, blank=False)
device = models.CharField(max_length=255, null=False, blank=False)
class Comment(TimestampedModel):
user = models.ForeignKey(User, on_delete=models.PROTECT, blank=True, null=True)
contents = models.CharField(max_length=510)
rating = models.IntegerField(blank=False, null=False)
And my serializer looks like this:
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('name',)
class CommentListItemSerializer(serializers.ModelSerializer):
user = UserSerializer()
class Meta:
model = Comment
fields = ('user', 'contents', 'rating')
And the view:
class CommentsList(generics.ListAPIView):
serializer_class = CommentListItemSerializer
queryset = Comment.objects.all()
It's almost getting the job done ;). The response I'm getting looks like this:
"results": [
{
"user": {
"name": "Ania"
},
"contents": "Very good",
"rating": 6
},
{
"user": {
"name": "Anuk"
},
"contents": "Not very good",
"rating": 1
}
]
There are two problems with that response.
I don't want to have this nested object "user.name". I'd like to receive that as a simple string field, for example "username".
Serializer makes a database query (not a join, but a separate query) for each user, to get his/her name. Since that's unacceptable, how to fix that?
Serializer makes a database query (not a join, but a separate query)
for each user, to get his/her name.
You can use select_related() on the queryset attribute of your view. Then accessing user.name will not result in further database queries.
class CommentsList(generics.ListAPIView):
serializer_class = CommentListItemSerializer
queryset = Comment.objects.all().select_related('user') # use select_related
I don't want to have this nested object "user.name". I'd like to
receive that as a simple string field, for example "username"
You can define a read-only username field in your serializer with source argument. This will return a username field in response.
class CommentListItemSerializer(serializers.ModelSerializer):
# define read-only username field
username = serializers.CharField(source='user.name', read_only=True)
class Meta:
model = Comment
fields = ('username', 'contents', 'rating')
You can add custom functions as fields
class Comment(models.Model):
user = models.ForeignKey(User, on_delete=models.PROTECT, blank=True, null=True)
contents = models.CharField(max_length=510)
rating = models.IntegerField(blank=False, null=False)
def username(self):
return self.user.name
class CommentListItemSerializer(serializers.ModelSerializer):
class Meta:
model = Comment
fields = ('username', 'contents', 'rating')