I have an API with nested serializers where I overwrote the create method. My nested serializer has Foreign Keys to another model. Now I want to create objects of this other model in the same API call. This is where I am stuck.
My data looks like so:
[
{
"project": "project1",
"name": "Graph1",
"description": "testdescription",
"nodes": [
{
"id": 16,
"name": "target1",
"graph": 49
},
{
"id": 15,
"name": "Node1",
"graph": 49
}
],
"edges": [
{
"id": 14,
"name": "Edge1",
"graph": 49,
"source": 15,
"target": 16
}
]
}
]
The fields source and target are Foreign Keys to the model Node.
Now, I can send this data without a problem when the fields source and target are already existent in the database.
But what I want is, that I send the data and I create a new Node object (source) and a new Node object (target) in the same call.
So far I overwrote the create method to enable nested serialization like so:
class GraphSerializer(serializers.ModelSerializer):
nodes = NodeSerializer(many=True)
edges = EdgeSerializer(many=True)
class Meta:
model = Graph
fields = ('project',
'name',
'description',
'nodes',
'edges',
)
def create(self, validated_data):
nodes_data = validated_data.pop('nodes')
edges_data = validated_data.pop('edges')
graph = Graph.objects.create(**validated_data)
for node_data in nodes_data:
Node.objects.create(graph=graph,**node_data)
for edge_data in edges_data:
Edge.objects.create(graph=graph, **edge_data)
return graph
that works, but like I said I need to create the node objects within the edges with the same call. Is there any way to do this? I can't find any sources online on how to do this.
Any help is very very much appreciated! Thanks so much!
My models
class Graph(models.Model):
project = models.ForeignKey(Project, on_delete=models.CASCADE)
name = models.CharField(max_length=120, blank=True)
description = models.CharField(max_length=400, blank=True)
def __str__(self):
return self.name
#property
def nodes(self):
return self.node_set.all()
#property
def edges(self):
return self.edge_set.all()
class Node(models.Model):
name = models.CharField(max_length=120, blank=True)
graph = models.ForeignKey(Graph, on_delete=models.CASCADE)
def __str__(self):
return self.name
class Edge(models.Model):
name = models.CharField(max_length=120, blank=True)
graph = models.ForeignKey(Graph, on_delete=models.CASCADE)
source = models.ForeignKey(Node, on_delete=models.CASCADE, related_name='source_set')
target = models.ForeignKey(Node, on_delete=models.CASCADE, related_name='target_set')
def __str__(self):
return self.name
You can achieve the custom functionality by WritableNestedSerializers. By default nested serializers are used for read-only but in order to support write-operations to a nested serializer field you'll need to create create() and/or update() methods in order to explicitly specify how the child relationships should be saved. So yes it can be done.
class TrackSerializer(serializers.ModelSerializer):
class Meta:
model = Track
fields = ['order', 'title', 'duration']
class AlbumSerializer(serializers.ModelSerializer):
tracks = TrackSerializer(many=True)
class Meta:
model = Album
fields = ['album_name', 'artist', 'tracks']
def create(self, validated_data):
tracks_data = validated_data.pop('tracks')
album = Album.objects.create(**validated_data)
for track_data in tracks_data:
Track.objects.create(album=album, **track_data)
return album
Another way is to use drf-writable-nested package. Here you create the parent serializers and refer them in the serializer where the relation is required. Only the difference is Update/Create methods are defined in the package which you'll have to write by yourself in above method.
For example, for the following model structure:
Models.py
from django.db import models
class Site(models.Model):
url = models.CharField(max_length=100)
class User(models.Model):
username = models.CharField(max_length=100)
class AccessKey(models.Model):
key = models.CharField(max_length=100)
class Profile(models.Model):
sites = models.ManyToManyField(Site)
user = models.OneToOneField(User)
access_key = models.ForeignKey(AccessKey, null=True)
class Avatar(models.Model):
image = models.CharField(max_length=100)
profile = models.ForeignKey(Profile, related_name='avatars')
serializers.py
from rest_framework import serializers
from drf_writable_nested import WritableNestedModelSerializer
class AvatarSerializer(serializers.ModelSerializer):
image = serializers.CharField()
class Meta:
model = Avatar
fields = ('pk', 'image',)
class SiteSerializer(serializers.ModelSerializer):
url = serializers.CharField()
class Meta:
model = Site
fields = ('pk', 'url',)
class AccessKeySerializer(serializers.ModelSerializer):
class Meta:
model = AccessKey
fields = ('pk', 'key',)
class ProfileSerializer(WritableNestedModelSerializer):
# Direct ManyToMany relation
sites = SiteSerializer(many=True)
# Reverse FK relation
avatars = AvatarSerializer(many=True)
# Direct FK relation
access_key = AccessKeySerializer(allow_null=True)
class Meta:
model = Profile
fields = ('pk', 'sites', 'avatars', 'access_key',)
class UserSerializer(WritableNestedModelSerializer):
# Reverse OneToOne relation
profile = ProfileSerializer()
class Meta:
model = User
fields = ('pk', 'profile', 'username',)
Related
how can I create a nested serializer field without using (many=True)?
The following code works fine:
from music.models import Track, Album
from rest_framework import serializers
class TrackSerializer(serializers.ModelSerializer):
class Meta:
model = Track
fields = ['order', 'title', 'duration']
class AlbumSerializer(serializers.ModelSerializer):
tracks = TrackSerializer(many=True)
class Meta:
model = Album
fields = ['album_name', 'artist', 'tracks']
def create(self, validated_data):
tracks_data = validated_data.pop('tracks')
album = Album.objects.create(**validated_data)
for track_data in tracks_data:
Track.objects.create(album=album, **track_data)
return album
This json works fine:
{
"album_name": "Black Album",
"artist": "Metallica",
"tracks": [
{
"order": 1,
"title": "Enter Sandman",
"duration": 245
},
{
"order": 2,
"title": "Sad but True",
"duration": 264
},
{
"order": 3,
"title": "The Unforgiven",
"duration": 159
}
]
}
but I need to get this json working, one object, without the square brackets []:
{
"album_name": "Black Album",
"artist": "Metallica",
"tracks":
{
"order": 1,
"title": "Enter Sandman",
"duration": 245
}
}
I've tried to remove the (many=True) but I receive either the following error:
create() argument after ** must be a mapping, not str
models:
from django.db import models
class Album(models.Model):
album_name = models.CharField(max_length=100)
artist = models.CharField(max_length=100)
class Track(models.Model):
album = models.ForeignKey(Album, related_name='tracks', on_delete=models.CASCADE)
order = models.IntegerField()
title = models.CharField(max_length=100)
duration = models.IntegerField()
class Meta:
unique_together = ['album', 'order']
ordering = ['order']
def __str__(self):
return '%d: %s' % (self.order, self.title)
views.py
from rest_framework import viewsets
from music.serializers import AlbumSerializer
from music.models import Album
class STMusic(viewsets.ModelViewSet):
serializer_class = AlbumSerializer
queryset = Album.objects.all()
How to fix it?
def create(self, validated_data):
track_data = validated_data.pop('tracks')
album = Album.objects.create(**validated_data)
Track.objects.create(album=album, **track_data)
return album
Ok I found the solution based on the comments from tsantor on this other post: Django Rest Framework: AttributeError when Serializer many=False, but not when many=True
It seems if you are using a ForeignKey relationship on your model you need to add (many=True) to your serializer as DRF creates a list based on the OneToMany relationship. If you need to POST only one object, you need to use a OneToOne relationship in your model (which makes sense) so that DRF expects only one object and not a list.
So the working code is:
models.py
from django.db import models
class Album(models.Model):
album_name = models.CharField(max_length=100)
artist = models.CharField(max_length=100)
class Track(models.Model):
album = models.OneToOneField(Album, related_name='track', on_delete=models.CASCADE)
order = models.IntegerField()
title = models.CharField(max_length=100)
duration = models.IntegerField()
class Meta:
unique_together = ['album', 'order']
ordering = ['order']
def __str__(self):
return '%d: %s' % (self.order, self.title)
serializer.py
class AlbumSerializer(serializers.ModelSerializer):
track = TrackSerializer()
class Meta:
model = Album
fields = ['album_name', 'artist', 'track']
def create(self, validated_data):
track_data = validated_data.pop('tracks')
album = Album.objects.create(**validated_data)
Track.objects.create(album=album, **track_data)
return album
I'm trying to build a queryset which combines two query results, namely from Category and Course. Every Course has a Category foreign key. Is there a way to add the respective Courses to each Category?
Example:
{
"id": 61,
"name": "fgfdf",
"courses":
{
"id": 1,
"category": 61,
"title": "mytitle"
"active": true
},
{
...
}
}
Url
path('dict/<pk>/', DictView.as_view(), name='detail')
Models
class Category(models.Model):
name = models.CharField(max_length=255, blank=False, null=False)
class Course(models.Model):
category = models.ForeignKey(Category, on_delete=models.CASCADE, null=True)
title = models.CharField(max_length=255, blank=False, null=False)
active = models.BooleanField(default=True)
View
This is what I imagined but it's obviously incorrect, I've done some research but I couldn't find what I needed.
class DictView(RetrieveAPIView):
queryset = Category.objects.all()
serializer_class = CategorySerializer
def get_queryset(self):
queryset = Category.objects.all()
courses = list(Course.objects.filter(category=pk))
queryset['courses'] = courses;
return queryset
One way is defining serializers like this:
class CourseSerializer(serializers.ModelSerializer):
class Meta:
model = Course
fields = "__all__"
class CategorySerializer(serializers.ModelSerializer):
courses = CourseSerializer(source='course_set', many=True)
class Meta:
model = Category
fields = "__all__"
Then, you don't need to override get_queryset anymore.
If you wish to apply filters for courses, say you only want active courses, you can do the following:
class CategorySerializer(serializers.ModelSerializer):
courses = serializers.SerializerMethodField()
def get_courses(self, obj):
active_courses = obj.course_set.filter(active=True)
return CourseSerializer(active_courset, many=True).data
class Meta:
model = Category
fields = "__all__"
I have this M2M relation with through model as
class Person(models.Model):
name = models.CharField(max_length=128)
def __str__(self):
return self.name
class Group(models.Model):
name = models.CharField(max_length=128)
members = models.ManyToManyField(Person, through='Membership')
def __str__(self):
return self.name
class Membership(models.Model):
person = models.ForeignKey(Person, on_delete=models.CASCADE)
group = models.ForeignKey(Group, on_delete=models.CASCADE)
date_joined = models.DateField()
invite_reason = models.CharField(max_length=64)
Please note that, I have extra fields date_joined and invite_reason in the through model.
Now, I want to serialize the Group queryset using DRF and thus I choose the below serializer setup.
class PersonSerializer(serializers.ModelSerializer):
class Meta:
model = Person
fields = "__all__"
class GroupSerializer(serializers.ModelSerializer):
members = PersonSerializer(read_only=True, many=True)
class Meta:
model = Group
fields = "__all__"
and it is returning the following response,
[
{
"id": 1,
"members": [
{
"id": 1,
"name": "Jerin"
}
],
"name": "Developer"
},
{
"id": 2,
"members": [
{
"id": 1,
"name": "Jerin"
}
],
"name": "Team Lead"
}
]
Here, the members field returning the Person information, which is perfect.
But,
How can I add the date_joined and invite_reason field/info into the members field of the JSON response?
class PersonSerializer(serializers.ModelSerializer):
class Meta:
model = Person
fields = "__all__"
def serialize_membership(self, person_instance):
# simple method to serialize the through model fields
membership_instance = person_instance \
.membership_set \
.filter(group=self.context["group_instance"]) \
.first()
if membership_instance:
return MembershipSerializer(membership_instance).data
return {}
def to_representation(self, instance):
rep = super().to_representation(instance)
return {**rep, **self.serialize_membership(instance)}
class MembershipSerializer(serializers.ModelSerializer): # create new serializer to serialize the through model fields
class Meta:
model = Membership
fields = ("date_joined", "invite_reason")
class GroupSerializer(serializers.ModelSerializer):
members = serializers.SerializerMethodField() # use `SerializerMethodField`, can be used to pass context data
def get_members(self, group):
return PersonSerializer(
group.members.all(),
many=True,
context={"group_instance": group} # should pass this `group` instance as context variable for filtering
).data
class Meta:
model = Group
fields = "__all__"
Notes:
Override the to_representation(...) method of PersonSerializer to inject extra data into the members field of the JSON
We need person instance/pk and group instance/pk to identify the Membership instance to be serialized. For that, we have used the serializer context to pass essential data
I've designed an API to add songs and create a playlist.
models.py:
class Song(models.Model):
title = models.CharField(null=False, blank=False, max_length=400)
artist = models.CharField(null=False, blank=False, max_length=400)
def __str__(self):
return f"{self.title} by {self.artist}"
class Playlist(models.Model):
name = models.CharField(null=False, blank=False, max_length=100)
songs = models.ManyToManyField('Song', blank=True, related_name='playlists')
serializers.py:
class SongSerializer(serializers.ModelSerializer):
class Meta:
model = Song
fields = ['title', 'artist']
extra_kwargs = {'playlists': {'required':False}}
class PlaylistSerializer(serializers.ModelSerializer):
class Meta:
model = Playlist
fields = ['name', 'songs']
extra_kwargs = {'songs':{'required':False}}
views.py:
class SongViewSet(viewsets.ModelViewSet):
queryset = Song.objects.all()
serializer_class = SongSerializer
class PlaylistViewSet(viewsets.ModelViewSet):
queryset = Playlist.objects.all()
serializer_class = PlaylistSerializer
But when I do a get request on the Playlist, I get the results as the following:
{
"name": "Classic Hits",
"songs": [
1,
3,
4
]
}
I want the songs' names to be displayed instead of their PKs in the playlist.How do I do that?
As you did not specify how Songs are deserialized in Playlist, the primary keys are returned. You could try:
class PlaylistSerializer(serializers.ModelSerializer):
songs = SongSerializer(many=True)
class Meta:
model = Playlist
fields = ['name', 'songs']
extra_kwargs = {'songs':{'required':False}}
For create/update on nested objects:
By default nested serializers are read-only. If you want to support write-operations to a nested serializer field you'll need to create create() and/or update() methods in order to explicitly specify how the child relationships should be saved.
You can modify the PlayListSerializer's create/update method:
class PlaylistSerializer(serializers.ModelSerializer):
songs = SongSerializer(many=True)
class Meta:
model = Playlist
fields = ['name', 'songs']
extra_kwargs = {'songs':{'required':False}}
def create(self, validated_data):
songs_data = validated_data.pop('songs')
playlist = Playlist.objects.create(**validated_data)
for song_data in song_data:
Song.objects.create(**song_data)
return playlist
Refer to nested relationships in the docs.
I have the following simple models.py file:
from django.db import models
from model_utils.managers import InheritanceManager
class Clique(models.Model):
created = models.DateTimeField(auto_now_add=True)
name = models.CharField(max_length=100, blank=False)
class Post(models.Model):
created = models.DateTimeField(auto_now_add=True)
headline = models.TextField()
clique = models.ForeignKey(Clique,
on_delete=models.CASCADE,
blank=True,
null=True)
objects = InheritanceManager()
def __str__(self):
return self.headline
class VideoPost(Post):
video = models.BooleanField(default=True)
class ImagePost(Post):
image = models.BooleanField(default=True)
So, there is a Clique model which can contain multiple Post instances. The Post instances can be ImagePost or VideoPost. Therefore, ImagePost and VideoPost both inherit Post.
Now, let's say I want to retrieve the ImagePost subclass instances. So, I have the following view in my views.py file:
class PostList(generics.ListAPIView):
serializer_class = PostSerializer
def get_queryset(self):
return Post.objects.select_subclasses(ImagePost)
When I pass the endpoint posts/ in the url, then this view will be triggered and it should give me only the ImagePost instances, right ? But I also get the VideoPost instances from the database:
[
{
"clique": "http://127.0.0.1:8000/cliques/1/",
"comment_set": [],
"created": "2019-06-18T09:52:47.929623Z",
"headline": "FirstImagePost",
"id": 1,
"url": "http://127.0.0.1:8000/posts/1/"
},
{
"clique": "http://127.0.0.1:8000/cliques/1/",
"comment_set": [],
"created": "2019-06-18T09:53:20.266653Z",
"headline": "FirstVideoPost",
"id": 2,
"url": "http://127.0.0.1:8000/posts/2/"
}
]
Why is this happening ? I walked through the official doc here . Can somebody help
Just for the sake of completeness, my serializers.py file looks like the following:
from rest_framework import serializers
from posts.models import Post, VideoPost, ImagePost, Clique
class CliqueSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Clique
fields = ('id', 'name', 'post_set')
read_only_fields = ('post_set', )
class PostSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Post
fields = ('url', 'id', 'created', 'headline', 'clique', 'comment_set',)
read_only_fields = ('comment_set',)
class VideoPostSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = VideoPost
fields = '__all__'
class ImagePostSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = ImagePost
fields = '__all__'
From the documentation, it seems like select_subclasses does not filter by subclass type for you, it only converts it to the subclass if it matches what you supplied.
in your case
Post.objects.select_subclasses(ImagePost)
will convert all ImagePost to ImagePost instance, leaving the other ones as Post object, it doesn't filter it out.
from the doc here:
nearby_places = Place.objects.select_subclasses("restaurant")
# restaurants will be Restaurant instances, bars will still be Place instances
In your case you can simply do:
Post.objects.filter(imagepost__image=True).select_subclasses(ImagePost)
Though i don't think you need the select_subclasses(ImagePost) part