Create foreign key objects inside serializer - python

I've two model:
class Author(models.Model):
name = models.CharField(max_length=255)
class Books(models.Model)
author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name='books_author')
name = models.CharField(max_length=255)
Now, I need to create an author and it's related books in a single request inside create serializer. The incoming data would look something like this:
{
"author": "John Doe"
"books":[
{
"name": "Life of Pie"
},
{
"name": "Lord of the rings"
}
]
}
How can I do it without using a for loop inside the create serializer?

Nested serializers are read-only, you will need to override the create method, and the simplest method is to use a for loop.
# serializers.py
class BooksSerializer(serializers.ModelSerializer):
class Meta:
model = Books
fields = ('name',)
class AuthorSerializer(serializers.ModelSerializer):
books = BooksSerializer(source='books_author', many=True)
def create(self, validated_data):
books_data = validated_data.pop('books_author')
author = Author.objects.create(**validated_data)
for book in books_data:
Books.objects.create(author=author, **book)
return author
class Meta:
model = Author
fields = ('name', 'books')
Note that I used "name" and not "author" as it seems more logical, so you'd need to use the following body:
{
"name": "John Doe",
"books": [
{
"name": "Life of Pie"
},
{
"name": "Lord of the rings"
}
]
}
The same goes for the update method.
More details in the official documentation.
On a side note, I'd:
rename Booksto Book (models.py)
change related_name='books_author' by related_name='books' (models.py)
rename BooksSerializer to BookSerializer (serializers.py)
replace books = BooksSerializer(source='books_author', many=True) by books = BooksSerializer(many=True)(serializers.py)
This would make your code more readable.

Related

Django Rest Framework- retrieving a related field on reverse foreign key efficiently

I have the following models that represent a working group of users. Each working group has a leader and members:
class WorkingGroup(models.Model):
group_name = models.CharField(max_length=255)
leader = models.ForeignKey(User, null=True, on_delete=models.SET_NULL)
class WorkingGroupMember(models.Model):
group = models.ForeignKey(WorkingGroup, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE)
In DRF, I want to efficiently retrieve all groups (there are several hundred) as an array of the following json objects:
{
'id': <the_group_id>
'group_name': <the_group_name>
'leader': <id_of_leader>
'members': [<id_of_member_1>, <id_of_member_2>, ...]
}
To do so, I have set up the following serializer:
class WorkingGroupSerializer(serializers.ModelSerializer):
members = serializers.SerializerMethodField()
class Meta:
model = WorkingGroup
fields = ('id', 'group_name', 'leader', 'members',)
def get_members(self, obj):
return obj.workinggroupmember_set.all().values_list('user_id', flat=True)
So that in my view, I can do something like:
groups = WorkingGroup.objects.all().prefetch_related('workinggroupmember_set')
group_serializer = WorkingGroupSerializer(groups, many=True)
This works, and gives the desired result, however I am finding it does not scale well at all, as the prefetching workinggroupmember_set does not seem to be used inside of the get_members method (Silky is showing a single query to grab all WorkingGroup objects, and then a query for each workinggroupmember_set call in the get_members method). Is there a way to set up the members field in the serializer to grab a flattened/single field version of workinggroupmember_set without using a SerializerMethodField? Or some other way of doing this that lets me properly use prefetch?
Problem here that you are doing values_list on top of all which nullifies your prefetch_related. There is currently no way to do prefetch with values_list see https://code.djangoproject.com/ticket/26565. What you can do is to transition this into python code instead of SQL
class WorkingGroupSerializer(serializers.ModelSerializer):
members = serializers.SerializerMethodField()
class Meta:
model = WorkingGroup
fields = ('id', 'group_name', 'leader', 'members',)
def get_members(self, obj):
return [wgm.user_id for wgm in obj.workinggroupmember_set.all()]
In a recent project with DRF v3.9.1 and django 2.1, I needed to recursively expose all the children of an object, by having only a direct connection to the parent, which could have had multiple children.
Before, if I was to request the "tree" of an object, I was getting:
{
"uuid": "b85385c0e0a84785b6ca87ce50132659",
"name": "a",
"parent": null
}
By applying the serialization shown below I get:
{
"uuid": "b85385c0e0a84785b6ca87ce50132659",
"name": "a",
"parent": null
"children": [
{
"uuid": "efd26a820b4e4f7c8e56c812a7791fcb",
"name": "aa",
"parent": "b85385c0e0a84785b6ca87ce50132659"
"children": [
{
"uuid": "ca2441fc7abf49b6aa1f3ebbc2dae251",
"name": "aaa",
"parent": "efd26a820b4e4f7c8e56c812a7791fcb"
"children": [],
}
],
},
{
"uuid": "40e09c85775d4f1a8578bba9c812df0e",
"name": "ab",
"parent": "b85385c0e0a84785b6ca87ce50132659"
"children": [],
}
],
}
Here is the models.py of the recursive object:
class CategoryDefinition(BaseModelClass):
name = models.CharField(max_length=100)
parent = models.ForeignKey('self', related_name='children',
on_delete=models.CASCADE,
null=True, blank=True)
To get all the reverse objects in the foreign key, apply a field to the serializer class:
class DeepCategorySerializer(serializers.ModelSerializer):
children = serializers.SerializerMethodField()
class Meta:
model = models.CategoryDefinition
fields = '__all__'
def get_children(self, obj):
return [DeepCategorySerializer().to_representation(cat) for cat in obj.children.all()]
Then apply this serializer to a DRF view function or generics class, such as:
re_path(r'categories/(?P<pk>[\w\d]{32})/',
generics.RetrieveUpdateDestroyAPIView.as_view(
queryset=models.CategoryDefinition.objects.all(),
serializer_class=serializers.DeepCategorySerializer),
name='category-update'),

Can't get Django Rest Framework GET vs POST structure to differ

I have a model that looks like this:
class Pet(models.Model):
name = models.CharField(max_length=30)
description = models.CharField(max_length=200)
primary_contact = models.ForeignKey(Person)
My Serializer looks like this:
class PetSerializer(serializers.ModelSerializer):
class Meta:
model = Pet
fields = ('id', 'name', 'description', 'primary_contact')
My Viewset:
class PetViewSet(viewsets.ModelViewSet):
queryset = Pet.objects.all()
serializer_class = PetSerializer
My Problem:
If I do a "GET" at my endpoint, my result looks like this:
[
{
"id": 1,
"name": "Ripley",
"description": "Black / Tan Yorkie",
"primary_contact": 1
}
]
The primary_contact only brings back the ID of the Person object. This is exactly how I want the POSTing structure to look like. When I POST, I only want to supply the ID of the Person object. However, when I GET, I want the content to look like this:
[
{
"id": 1,
"name": "Ripley",
"description": "Black / Tan Yorkie",
"primary_contact": {
"id": 1,
"first_name": "MyFistName",
"last_name": "MyLastName",
"phone": "312-xxx-xxxx",
"email": "aarsan#abc123.com"
}
}
]
I can get the above structure by setting depth=2 in my serializer but then if I try to POST, it tries to create the primary_contact, which I not want to do since it already exists.
The workaround I've been using is creating a different endpoint for POST and GET when I have a foreign key, which I hope isn't the only way to do this.
You want a nested serializer. In this case you have to define a serializer for primary contact and link in with primary contact field of PetSerializer.
class ContactSerializer(serializers.ModelSerializer):
class Meta:
model = Contact
fields = ('id', 'field2', 'field3', '...')
class PetSerializer(serializers.ModelSerializer):
primary_contact = ContactSerializer()
class Meta:
model = Pet
fields = ('id', 'name', 'description', 'primary_contact')
You can have several serializers in the same ViewSet, to achieve this you need to overwrite get_serializer_classmethod.
Take a look at this: http://www.django-rest-framework.org/api-guide/generic-views/#get_serializer_classself

Django api rest filter manytomany

I am a quite New in Django and Django Api Rest FrameWork
I am trying to develop an Api with two models relationated with a ManyToMany relation
models.py
class Category(models.Model):
name = models.CharField(max_length=100)
class Apartment(models.Model):
id_2 = models.IntegerField(default= None)
neighborhood = models.CharField(max_length=100)
name = models.CharField(max_length=120)
district = models.CharField(max_length=120)
created = models.DateTimeField()
cats = models.ManyToManyField(Category)
address = models.CharField(max_length=200)
postal_code = models.IntegerField()
latitude = models.FloatField()
longitude = models.FloatField()
serializers.py
class CategorySerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Category
fields = ( 'id', 'name')
class AptSerializer(serializers.HyperlinkedModelSerializer):
cats = CategorySerializer(many=True, read_only=True)
class Meta:
model = Apartment
fields = ('id_2', 'neighborhood', 'name','district','created','cats','address','postal_code','latitude','longitude')
view.py
class AptViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows groups to be viewed or edited.
"""
queryset = Apartment.objects.all()
serializer_class = AptSerializer
filter_backends = (filters.DjangoFilterBackend,)
filter_fields = ('id_2', 'neighborhood', 'name', 'district','created','cats','address','postal_code','latitude','longitude')
routers.py
router.register(r'apartments', views.AptViewSet)
As you can see it implements DjangoFilterBackend so now i can filter with urls type
http://localhost:8000/apartments/?district=Eixample
and i get this
{
"id_2": 1044121532,
"neighborhood": "la Nova Esquerra de l'Eixample",
"name": "Hotel Exe AB Viladomat",
"district": "Eixample",
"created": "2001-02-13T00:00:00Z",
"cats": [
{
"id": 1073,
"name": "Allotjament"
},
{
"id": 1075,
"name": "Hotels"
},
{
"id": 1076,
"name": "3 estrelles"
}
],
"address": "Viladomat 197",
"postal_code": 8015,
"latitude": 41.383665702777776,
"longitude": 2.151834694444444
},
I'd like to make get request like
http://localhost:8000/apartments/?cats_name=Hotels
But it doesn't work, any idea in which will the road from now?
Write a custom Filter?
write another ViewSet for Category objects?
Thank You
The root QuerySet provided by the Manager describes all objects in the database table. Usually, though, you'll need to select only a subset of the complete set of objects.
But here your are passing the query parameters and to filter on whole queryset
views.py
class AptViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows groups to be viewed or edited.
"""
queryset = Apartment.objects.all()
serializer_class = AptSerializer
def get_queryset(self):
queryset = super(AptViewSet, self).get_queryset()
cats_name = self.request.query_params.get('cat_name', None)
if cats_name:
queryset = queryset.filter(cats__name=cats_name)
return queryset
If you can follow this post for writing the custom filters How to Pass kwarg to Filter
Check out this link on query parameter filtering.
You will overriding the get_queryset method in your serializer.

creating new object instead of assigning existing object POST in DRF

Recently we have been moving from traditional view to API Views so started implementing DRF.
so, my main problem is im having a model with a foreign key to another model
say
class Task(model.Models):
created_by=models.ForeignKey(User, null=true, blank=true)
assignees = models.ManyToManyField(User, related_name="assignees", blank=True)
class UserSerializer(serializer.ModelSerializer):
class Meta:
model = User
class TaskSerializer(serializer.ModelSerializer):
created_by = UserSerializer()
assignees = UserSerializer(many=True, required=False)
class Meta:
model=Task
now when i create a task with User(pk=10) already existing user.
A new user is created instead of assigning the existing user.
here is my input:
{
"created_by": {
"id": 10,
"email": "xyz#gmail.com",
"name": "abc def"
},
"assignees": []
}
here is the output:
{
"id":23,
"created_by": {
"id": 11,
"email": "xyz#gmail.com",
"name": "abc def"
},
"assignees": [
]
}
Got some help from Linovia: with this :- https://gist.github.com/xordoquy/78a2d0e2ec85e2d7aadf
but the test case fails.
could any one suggest me im in need of it.
Probably using PrimaryKeyRelatedField may do the work.
class TaskSerializer(serializer.ModelSerializer):
created_by = serializeres..PrimaryKeyRelatedField()
assignees = UserSerializer(many=True, required=False)
class Meta:
model=Task
And updating your json to:
{
"created_by": 10,
"assignees": []
}

Is there a better way to convert a Model to a well formatted JSON string?

I am new to Python and Django, and I just followed the tutorial on Django Book, and I created three Models according to the tutorial - Publisher, Author, Book. Now I want to get all the books, and encoded them to a JSON string.
At first, I just use the method I found on djangoproject.com. Here is the code:
from django.core import serializers
def getAllBooks(request):
book_list = Book.objects.all()
return HttpResponse(serializers.serialize("json", book_list), content_type="application/json")
It works fine, but the result is like this:
[
{
"pk": 1,
"model": "books.book",
"fields": {
"publisher": 1,
"title": "Book One",
"authors" : [3, 4],
"publication_date": "2013-07-01"
}
},
{
"pk": 2,
"model": "books.book",
"fields": {
"publisher": 3,
"title": "Book Two",
"authors" : [5],
"publication_date": "2013-07-05"
}
}
]
We can see the authors and publisher only show the id. Then I read through that article on djangoproject.com. At the end, it introduces a method called natural_key. By using that method, the authors field will look like this:
....
{
"pk": 1,
"model": "books.book",
"fields": {
"publisher": 1,
"title": "Book One",
"authors" : ["Douglas", "Adams"],
"publication_date": "2013-07-01"
}
},
....
It's better, but still not what I exactly want. What I want is this:
[
{
"publisher":{
"website":"http://www.example.com/",
"city":"SYD",
"name":"Publisher",
"country":"AU",
"state":"NSW",
"address":"1 Avenue"
},
"authors":[
{
"first_name":"Eric",
"last_name":"Qian",
"email":"eric#example.com"
},
{
"first_name":"Eric2",
"last_name":"Qian",
"email":"eric2#example.com"
}
],
"publication_date":"01/07/2013",
"title":"Book One"
}
]
The authors and publisher fields include all the data. I achieved this by adding a method called JSONEncode to all the models like this:
from django.db import models
# Create your models here.
class Publisher(models.Model):
name = models.CharField(max_length=30)
address = models.CharField(max_length=50)
city = models.CharField(max_length=60)
state_province = models.CharField(max_length=30)
country = models.CharField(max_length=50)
website = models.URLField()
def __unicode__(self):
return self.name
class Meta:
ordering = ['name']
def JSONEncode(self):
#init a dictionary
JSONData = {};
#name
JSONData['name'] = self.name
#address
JSONData['address'] = self.address
#city
JSONData['city'] = self.city
#state_province
JSONData['state'] = self.state_province
#country
JSONData['country'] = self.country
#website
JSONData['website'] = self.website
#return the json data
return JSONData
class Author(models.Model):
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=40)
email = models.EmailField()
def __unicode__(self):
return u'%s %s' % (self.first_name, self.last_name)
def JSONEncode(self):
#init a dictionary
JSONData = {};
#first_name
JSONData['first_name'] = self.first_name
#last_name
JSONData['last_name'] = self.last_name
#email
JSONData['email'] = self.email
#return the json data
return JSONData
class Book(models.Model):
title = models.CharField(max_length=100)
authors = models.ManyToManyField(Author)
publisher = models.ForeignKey(Publisher)
publication_date = models.DateField()
def __unicode__(self):
return self.title
class Meta:
ordering = ['title']
def JSONEncode(self):
#init a dictionary
JSONData = {};
#title
JSONData['title'] = self.title
#authors
authors = []
for author in self.authors.all():
authors.append(author.JSONEncode())
JSONData['authors'] = authors
#publisher
JSONData['publisher'] = self.publisher.JSONEncode()
JSONData['publication_date'] = self.publication_date.strftime('%d/%m/%Y')
#return the json data
return JSONData
Then I modify the code in books.views:
def getAllBooks(request):
book_list = Book.objects.all()
book_list_data = []
for book in book_list:
book_list_data.append(book.JSONEncode())
return HttpResponse(json.dumps(book_list_data), content_type="application/json")
It works quite good, but the draw back is obvious - I have to write a JSONEncode() function to all models. So I am wondering is Django provide a better way to do this? Thanks in advance!
Option 1 is Django-piston. By specifying model fields manually you can get nested listings and foreign key following. Probably the fastest way to get what you want, minimum errors, but not really flexible.
Option 2 if to do model serialization in naïve way by your own. For each model define a method that would convert itself to python dictionary, and then use simplejson.dumps in views.py to make it JSON. This approach gives you full control and infinite flexibility deciding about your JSON structure. Later you can replace it with more elegant solution using multiple inheritance to add as_dict and define own JsonResponse class.
Example:
# In models.py, add as_dict() method to all models
# Example for class Book
def as_dict(self):
d = {
"id": self.id,
"publisher": self.publisher.as_dict(), # avoid this
"title": self.title,
"publication_date": str(self.publication_date),
"publisher": self.publisher,
"authors": [author.as_dict() for author in self.authors.all()] # avoid this
}
# then in views.py
def getAllBooks(request):
book_list = [book.as_dict() for book in Book.objects.all().select_related()]
return HttpResponse(simplejson.dumps(book_list),
content_type="application/json")
HOWEVER
Both approaches are kind of dirty.
Your database may and probably will struggle because of this. Django ORM will produce ugly sql. Best is to avoid nested model serialization at all. And if you really need it, dont forget to select_related().
Good luck!
You could try using Tastypie or django-rest-framework. You can customise the JSON emitted. Whilst it adds another layer of complexity it will probably pay off in the long run.

Categories