DRF Serializer for Writable Nested JSON - django

Firstly, I would like to mention that I've already seen many SO posts about this issue, but still couldn't figure out the problem occurring here.
I would like to POST a JSON object to my Django application, which looks like this:
{
"id": "1363362773409783808",
"text": "#MsLaydeeLala Damn haha. Reminds me of my dog. If someone comes into my room while I\u2019m sleeping he gets up and growls at them if he doesn\u2019t recognize them right away.",
"created_at": "2021-02-21T05:39:49.000Z",
"author": {
"id": "112233445566778899",
"username": "Elrafa559",
"name": "EL RAFA"
},
"keywords": [
{
"name": "dog"
}
]
}
and save it to my database, where my models are:
class Keyword(models.Model):
"""
Represents keywords which we are looking for, must be unique, 20 chars max.
"""
name = models.CharField(max_length=20, unique=True)
def __str__(self):
return self.name
class TwitterAccount(models.Model):
id = models.CharField(max_length=20, primary_key=True)
username = models.CharField(max_length=80)
name = models.CharField(max_length=80)
class Tweet(models.Model):
id = models.CharField(max_length=20, primary_key=True)
text = models.CharField(max_length=380) # 280 for tweet, 100 for links
created_at = models.DateTimeField()
author = models.ForeignKey(TwitterAccount, on_delete=models.CASCADE)
keywords = models.ManyToManyField(Keyword)
def __str__(self):
return str(self.author.username) + "/" + str(self.id)
class TweetKeyword(models.Model):
# TODO is CASCADE the right choice?
tweet = models.ForeignKey(Tweet, on_delete=models.CASCADE)
keyword = models.ForeignKey(Keyword, on_delete=models.CASCADE)
for that, I've written this serializer:
class TweetSerializer(serializers.ModelSerializer):
author = TwitterAccountSerializer()
keywords = KeywordSerializer(many=True)
class Meta:
model = Tweet
fields = ["id", "text", "created_at", "author", "keywords"]
# depth = 1
def create(self, validated_data):
print(validated_data)
try:
author = validated_data.pop('author')
author, did_create = TwitterAccount.objects.update_or_create(**author)
print('644444')
tweet = Tweet.objects.update_or_create(author=author, **validated_data)
keywords_data = validated_data.pop('keywords')
for keyword_data in keywords_data:
Keyword.objects.update_or_create(**keyword_data)
return tweet
except Exception as e:
print("Exception occured in method .create in TweetSerializer")
print(e.args)
return None
the error I'm getting in the method create of TweetSerializer is:
("Field 'id' expected a number but got [OrderedDict([('name', 'dog')])].",)
('`create()` did not return an object instance.',)
could anyone explain why the field keywords is getting treated as if it is id?

One way to do what you want is to:
pop keywords data from the validated data
create the tweet object without the keywords
in a for loop create each keyword, and associate it with the tweet object
Here is the code (I have removed the author part):
def create(self, validated_data):
keywords_data = validated_data.pop("keywords")
tweet = Tweet.objects.update_or_create(**validated_data)[0]
for keyword_data in keywords_data:
keyword = Keyword.objects.update_or_create(**keyword_data)[0]
tweet.keywords.add(keyword)
return tweet
You can read the DRF documentation about that, there is not that precise example but there are others and a lot of explanation.

Related

ManyToMany django field not working, not taking any input in POST request?

I have 2 models - Module and Room. A module can have zero or multiple rooms and a room can be added into multiple modules. So, there is a simple many-to-many relationship between them.
But when I define it in my module/models.py file, it is not taking any input as rooms. here are my files -
module/models.py -
class Module(models.Model):
module_id = models.AutoField(primary_key=True)
title = models.CharField(max_length=100)
desc = models.TextField()
rooms = models.ManyToManyField(Rooms)
rooms/models.py -
class Rooms(models.Model):
room_id = models.AutoField(primary_key=True)
title = models.CharField(max_length=100)
desc = models.TextField()
level = models.CharField(max_length=100)
is_deleted = models.BooleanField(default=False)
module/serializers.py -
class ModuleSerializer(serializers.ModelSerializer):
rooms = RoomSerializer(read_only=True, many=True)
class Meta:
model = Module
fields = "__all__"
module/views.py -
class add_module(APIView):
def post(self, request, format=None):
module_serializer = ModuleSerializer(data=request.data)
if module_serializer.is_valid():
module_serializer.save()
return Response(module_serializer.data['module_id'], status = status.HTTP_201_CREATED)
return Response(module_serializer.errors, status = status.HTTP_400_BAD_REQUEST)
POST request body for creating a module in POSTMAN -
{
"rooms": [
{
"room_id": 2,
"title": "4",
"desc": "22",
"level": "2",
}
],
"title": "4",
"desc": "22",
}
With this request, module is being created, but no room is getting added in it.
Can someone tell me why my rooms are not getting added while creating modules?
You need to write an explicit serializer create method to support writable nested representations.
https://www.django-rest-framework.org/api-guide/serializers/#writable-nested-representations
Example:
Given a RoomSerializer like this:
class RoomSerializer(serializers.ModelSerializer):
room_id = serializers.IntegerField()
class Meta:
model = models.Rooms
fields = '__all__'
We explicitly set room_id in RoomSerializer, to avoid <room_id> stripped away from the input data, and a MultipleObjectsReturned raised.
class ModuleSerializer(serializers.ModelSerializer):
rooms = RoomSerializer(many=True) # remove *read_only=True*
class Meta:
model = Module
fields = "__all__"
def create(self, validated_data):
rooms_data = validated_data.pop('rooms')
module = Module.objects.create(**validated_data)
for data in rooms_data:
room, created = Rooms.objects.get_or_create(**data)
module.rooms.add(room)
return module

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', ...]

clearing and seeding database from endpoint in django

++updated with screen shot of server error++
I'm trying to set up a rest API that can be cleared and then have the data in my postgres db re-seeded via an endpoint. I'm doing this with Django with json data in a fixtures file, executing a series of functions in my views. This has worked great so far, but now I'm trying to add data with foreign key fields.
models.py:
class Profile(models.Model):
name = models.CharField(max_length = 100)
profile_name = models.CharField(max_length = 100)
email = models.CharField(max_length = 100)
def __str__(self):
return self.name
class Post(models.Model):
title = models.CharField(max_length = 250)
body = models.TextField(max_length = 4000)
profile = models.ForeignKey(Profile, on_delete = models.CASCADE)
def __str__(self):
return self.title
class Comment(models.Model):
title = models.CharField(max_length = 200)
body = models.TextField(max_length = 1000)
profile = models.ForeignKey(Profile, on_delete = models.CASCADE)
post = models.ForeignKey(Post, on_delete = models.CASCADE)
def __str__(self):
return self.title
in views.py
def seed(request):
Profile.objects.all().delete()
reset(Profile)
for profile in all_profiles:
add_profile(profile)
Post.objects.all().delete()
reset(Post)
for post in all_posts:
add_post(post)
Comment.objects.all().delete()
reset(Comment)
for comment in all_comments:
add_comment(comment)
return HttpResponse('database cleared and seeded')
def add_profile(new_profile):
profile_instance = Profile.objects.create(**new_profile)
profile_instance.save()
def add_post(new_post):
post_instance = Post.objects.create(**new_post)
post_instance.save()
def add_comment(new_comment):
comment_instance = Comment.objects.create(**new_comment)
comment_instance.save()
def reset(table):
sequence_sql = connection.ops.sequence_reset_sql(no_style(), [table])
with connection.cursor() as cursor:
for sql in sequence_sql:
cursor.execute(sql)
some example seed objects:
all_profiles = [
{
"name": "Robert Fitzgerald Diggs",
"profile_name": "RZA",
"email": "abbotofthewu#wutang.com"
}
]
all_posts = [
{
"title": "Bring da Ruckus",
"body": "some text",
"profile": 5
}
]
all_comments = [
{
"title": "famous dart",
"body": "Ghostface catch the blast of a hype verse My Glock burst",
"profile": 6,
"post": 1
}
]
Now when I hit my endpoint I get an error like "ValueError: Cannot assign "5": "Post.profile" must be a "Profile" instance." I assume this means that the integer "5" in this case is just a number and not viewed as a reference to anything, but I'm not sure what to do about it. I thought creating the model instance would take care of this.
Here is my error from the CLI:
screenshot of server error
Any ideas?
got it figured. creating an object from the model simply saves the foreign key reference as an integer, but the database needs a full instance, so I had to query the database for the foreign object in question, variable-ize it and write over the integer with the variable before saving.
new functions from views:
def add_profile(new_profile):
profile_instance = Profile.objects.create(**new_profile)
profile_instance.save()
def add_post(new_post):
found_profile = Profile.objects.get(id=new_post['profile'])
new_post['profile'] = found_profile
post_instance = Post.objects.create(**new_post)
post_instance.save()
def add_comment(new_comment):
found_profile = Profile.objects.get(id=new_comment['profile'])
found_post = Post.objects.get(id=new_comment['post'])
new_comment['profile'] = found_profile
new_comment['post'] = found_post
comment_instance = Comment.objects.create(**new_comment)
comment_instance.save()
Perhaps try loading a string instead of an integer? As in "6" instead of 6?

IntegrityError: null value in column "invoiceOwner_id" violates not-null constraint

In my django app, i'm having difficulties whenever i want to add a new object that uses the table paymentInvoice.
The error i'm getting from my api looks like this
IntegrityError at /api/clients/invoice/
null value in column "invoiceOwner_id" violates not-null constraint
DETAIL: Failing row contains (10, INV-0006, Lix, 2020-08-04, 1, Pending, 3000, null).
NB: I haven't created the field invoiceOwner_id, postgres automatically added it or rather is using it as a representation for my invoiceOwner field
class Purchaser(models.Model):
name = models.CharField(max_length=50)
phone = models.CharField(max_length=20, unique=True)
email = models.EmailField(max_length=255, unique=True, blank=True)
image = models.ImageField(default='default.png', upload_to='customer_photos/%Y/%m/%d/')
data_added = models.DateField(default=datetime.date.today)
def __str__(self):
return self.name
class paymentInvoice(models.Model):
invoiceNo = models.CharField(max_length=50, unique=True, default=increment_invoice_number)
invoiceOwner = models.ForeignKey(Purchaser, on_delete=models.CASCADE, related_name="invoice_detail")
product = models.CharField(max_length=50, blank=True)
date = models.DateField(default=datetime.date.today)
quantity = models.PositiveSmallIntegerField(blank=True, default=1)
payment_made = models.IntegerField(default=0)
def __str__(self):
return self.invoiceOwner.name
serilizers file
class paymentInvoiceSerializer(serializers.ModelSerializer):
invoiceOwner = serializers.SerializerMethodField()
class Meta:
model = paymentInvoice
fields = '__all__'
def get_invoiceOwner(self, instance):
return instance.invoiceOwner.name
views file
class paymentInvoiceListCreateView(ListCreateAPIView):
serializer_class = paymentInvoiceSerializer
queryset = paymentInvoice.objects.all().order_by('-date')
GET result from api call.
{
"id": 1,
"invoiceOwner": "Martin",
"invoiceNo": "INV-0001",
"product": "",
"date": "2020-08-04",
"quantity": 1,
"payment_made": 0
}
Tried passing below as POST but got the main error
{
"invoiceOwner": "Becky",
"product": "Lix",
"quantity": 1,
"payment_made": 3000
}
"invoiceOwner" in your serializers.py is a SerializerMethodField which is readonly
that's why you get an error, you have to define the create method yourself
As I said in comment: You need to explicitly override the create method in your serializer since your model has foreign key invoiceOwner, just to create that instance first as a Purchaser instance.
You can try the code below:
class paymentInvoiceSerializer(serializers.ModelSerializer):
invoiceOwner = serializers.SerializerMethodField()
class Meta:
model = paymentInvoice
fields = '__all__'
def get_invoiceOwner(self, instance):
return instance.invoiceOwner.name
def create(self, validated_data):
purchaser_name = validated_data.get("invoiceOwner")
purchaser = Purchaser(name=purchaser_name,
# you need to have phone, email, since these fields are unique,
# they can't remain null
)
purchaser.save()
return paymentInvoice.objects.create(invoiceOwner = purchaser, **validated_data)

Django REST Framework create and update nested objects

I am trying to create or update a nested object if the object exists, I tried to use the create_or_update method, now the create works fine, but the update failed and said the pk already existed.
My Model:
class ContentHotel(models.Model):
hotel_id = models.IntegerField(unique=True, blank=True, primary_key=True)
star = models.FloatField(blank=True, null=True)
class Meta:
managed = False
db_table = 'content_hotels'
ordering = ('hotel_id',)
def __str__(self):
return str(self.hotel_id)
class RateHotel(models.Model):
rate_hotel_id = models.IntegerField(blank=True, unique=True, primary_key=True)
content_hotel = models.ForeignKey(ContentHotel, on_delete=models.CASCADE, related_name='rate_hotels')
source_code = models.CharField(max_length=20, blank=True, null=True)
class Meta:
managed = False
db_table = 'rate_hotels'
ordering = ('rate_hotel_id',)
def __str__(self):
return str(self.rate_hotel_id)
My Serializers:
# To handle RateHotel object operations
class RateHotelSerializer(serializers.ModelSerializer):
class Meta:
model = RateHotel
fields = __all__
# To handle nested object operations
class RateHotelSerializerTwo(serializers.ModelSerializer):
class Meta:
model = RateHotel
fields = __all__
read_only_fields = ('content_hotel',)
class ContentHotelSerializer(serializers.ModelSerializer):
rate_hotels = RateHotelSerializerTwo(many=True)
class Meta:
model = ContentHotel
fields = __all__
def create(self, validated_data):
rate_hotels_data = validated_data.pop('rate_hotels')
hotel_id = validated_data.pop('hotel_id')
content_hotel, created = ContentHotel.objects.update_or_create(hotel_id=hotel_id, defaults={**validated_data})
for rate_hotel_data in rate_hotels_data:
rate_hotel_id = rate_hotel_data.pop('rate_hotel_id')
RateHotel.objects.update_or_create(rate_hotel_id=rate_hotel_id, content_hotel=content_hotel,
defaults=rate_hotel_data)
return content_hotel
def update(self, instance, validated_data):
rate_hotels_data = validated_data.pop('rate_hotels')
rate_hotels = list(instance.rate_hotels.all())
for key in validated_data:
instance.key = validated_data.get(key, instance.key)
instance.save()
for rate_hotel_data in rate_hotels_data:
rate_hotel = rate_hotels.pop(0)
for key in rate_hotel_data:
rate_hotel.key = rate_hotel_data.get(key, rate_hotel.key)
rate_hotel.save()
return instance
JSON:
# Post Request - Create:
{
"hotel_id": -1,
"star": null,
"rate_hotels": [{"rate_hotel_id": -1}]
}
# Post Response:
{
"hotel_id": -1,
"star": null,
"rate_hotels": [
{
"content_hotel": -1,
"rate_hotel_id": -1,
"source_code": null,
}
]
}
# Post Request - Update:
{
"hotel_id": -1,
"star": 2,
"rate_hotels": [{"rate_hotel_id": -1, "source_code": "-1"}]
}
{
"hotel_id": [
"content hotel with this hotel id already exists."
],
"rate_hotels": [
{
"rate_hotel_id": [
"rate hotel with this rate hotel id already exists."
]
}
],
"status_code": 400
}
If I send a put request to update the nested object, the content hotel part works correctly, but the rate hotel part still said 'rate_hotel_id' already exists.
Can anyone help me fix this issue? I cannot find any related source online, thanks in advance!
Hmmmm, this looks like you could be looking for for "update if exists else create" use case, and Django has a get_or_create() method for performing what you want, see the docs here for reference: https://docs.djangoproject.com/en/dev/ref/models/querysets/#get-or-create
However, for you it could look something like this:
id = 'some identifier that is unique to the model you wish to lookup'
content_hotel, created = RateHotel.objects.get_or_create(
rate_hotel_id=rate_hotel_id,
content_hotel=content_hotel,
defaults=rate_hotel_data
)
if created:
# means you have created a new content hotel, redirect a success here or whatever you want to do on creation!
else:
# content hotel just refers to the existing one, this is where you can update your Model
I would also advise removing the hotel_id and hotel_rate_id as primary keys:
class ContentHotel(models.Model):
hotel_id = models.IntegerField(unique=True, blank=True,)
star = models.FloatField(blank=True, null=True)
class Meta:
managed = False
db_table = 'content_hotels'
ordering = ('hotel_id',)
def __str__(self):
return str(self.hotel_id)
class RateHotel(models.Model):
rate_hotel_id = models.IntegerField(blank=True, unique=True,)
content_hotel = models.ForeignKey(ContentHotel, on_delete=models.CASCADE, related_name='rate_hotels')
source_code = models.CharField(max_length=20, blank=True, null=True)
class Meta:
managed = False
db_table = 'rate_hotels'
ordering = ('rate_hotel_id',)
def __str__(self):
return str(self.rate_hotel_id)
The update_or_create() method does not accept a primary key, but can accept other unique database fields to perform the update:
So, an update_or_create() would look like so:
obj, created = Person.objects.update_or_create(
first_name='John', last_name='Lennon',
defaults={'first_name': 'Bob'},
)
Your create code looks fine, but there are some bigger issues with your update code which you need to address before we can know where the real issue lies I think.
for key in validated_data:
instance.key = validated_data.get(key, instance.key)
instance.save()
Here you are trying to loop through the values in the validated data and assign the values of that key to that attribute on the model. What you are actually doing is setting the attribute key on the model to the value of each key in validated_data. To better illustrate the issue I'll give you an example which corrects the issue:
for key in validated_data:
setattr(instance, key, validated_data.get(key, getattr(instance, key))
instance.save()
Here rather than trying to set instance.key we use the python built-in method setattr to set the attribute with the name that matches the value of the variable key to the value which we pull from validated_data.
I'm not sure correcting this will solve your issue though. I'd make sure you're correctly sending your request to the correct url with the proper header values to get to the update method. It could be that Django Rest Framework is preventing the creation behind the scenes because it thinks you're sending the request as a create operation and it finds the primary keys already.