How to create object within nested serializer on API create call - python

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

Django RestFramework nested serializer field many=false

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

Django REST RetrieveAPIView combining querysets

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__"

Serialize ManyToManyFields with a Through Model in Django REST Framework

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

How to display the names instead of pk in the list viewset in Django Rest Framework?

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.

Django - InheritanceManager is not working

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

Categories