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
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 want to have a serializers that use two model at once (If it possible)
models.py
class Club(models.Model):
id = models.AutoField(primary_key=True)
clubname = models.CharField(max_length=50, blank=True, null=True)
location = models.CharField(max_length=50, blank=True, null=True)
scores = models.IntegerField(blank=True, null=True)
serializers.py
class ShowAllClubSerializer(serializers.ModelSerializer):
class Meta:
model = Club
fields
class ShowClubPictures(serializers.ModelSerializer):
class Meta:
model = Clubpictures
fields = ['picture']
views.py
#api_view(["GET", ])
#permission_classes((IsAuthenticated, ))
def show_all_clubs_view(request):
if request.method == "GET":
clubs = Club.objects.all()
if clubs:
for club in clubs:
pictures = Clubpictures.objects.filter(clubid=club.id)
serializer1 = ShowAllClubSerializer(club)
serializer2 = ShowClubPictures(pictures[0])
return Response(serializer1.data, status=status.HTTP_200_OK)
# return Response(serializer2.data, status=status.HTTP_200_OK)
else:
return Response(status=status.HTTP_400_BAD_REQUEST)
Now I Have These In serializers1 and serializers2 Separately:
{
"clubname": "Club Name",
"location": "Somewhere",
"scores": 5,
}
{
"picture": "/media/images/Screenshot.png"
}
How can I take something like this in result:
{
"clubname": "Club Name",
"location": "Somewhere",
"scores": 5,
"picture": "/media/images/Screenshot.png"
}
You can use nested serializers to achieve that.
Your ClubSerializer would look something like this:
class ClubSerializer(serializers.ModelSerializer):
pictures = ClubPictureSerializer(many=True)
class Meta:
model = Club
fields = ('clubname', 'location', 'scores', 'pictures')
class ClubPictures(serializers.ModelSerializer):
class Meta:
model = Clubpictures
fields = ['picture']
assuming you have a ForeignKey from the ClubPicture to your Club with a related_name of pictures.
Also, on your view, you don't need to loop through Club.objects.all() and serialize each object individually - ModelSerializer/Serializer accept a many=True parameters that handles multiple objects already.
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',)
In my models.py, there are two model, the AvailableArea has a foreign field refer to AddressRegion:
class AddressRegion(models.Model):
name = models.CharField(max_length=8)
def __str__(self):
return self.name
def __unicode__(self):
return self.name
class AvailableArea(models.Model):
name = models.CharField(max_length=8)
addressregion = models.ForeignKey(AddressRegion, default=1, related_name='availableareas', on_delete=models.CASCADE)
def __str__(self):
return self.name
def __unicode__(self):
return self.name
In the serializers.py, I serialize all the fields:
class AvailableAreaSerializer(ModelSerializer):
"""
region
"""
class Meta:
model = AvailableArea
fields = "__all__"
In the views.py:
class AddressRegionListAPIView(ListAPIView):
serializer_class = AddressRegionSerializer
permission_classes = []
queryset = AddressRegion.objects.all()
The rest framework data is like this:
[
{
"id": 13,
"name": "st-01",
"addressregion": 3
},
{
"id": 14,
"name": "tg-02",
"addressregion": 4
},
{
"id": 15,
"name": "sx-01",
"addressregion": 3
}
]
I want the addressregion not shows the addressregion's id, but shows the addressregion's name.
You can do the following:
class AvailableAreaSerializer(ModelSerializer):
addressregion_name= serializers.ReadOnlyField(source='addressregion.name')
class Meta:
model = AvailableArea
fields = ('id', 'name', 'addressregion_name')
just add the following code in your serializer:
addressregion_name = serializers.StringRelatedField()
#should be like the following
class AvailableAreaSerializer(ModelSerializer):
addressregion_name = serializers.StringRelatedField()
class Meta:
model = AvailableArea
fields = ('id', 'name', 'addressregion__name')
Your serializer should be like this:
class AvailableAreaSerializer(ModelSerializer):
"""
可用地区
"""
class Meta:
model = AvailableArea
fields = ('id', 'name', 'addressregion__name')
and queryset in View should be:
queryset = AddressRegion.objects.all().select_related('addressregion')
select_related will fetch the result by joining both the table. For more info read this