Join related models in django rest framework - python

Trying to create an API method for getting user profile. The problem is that there are two tables related to user: built in django User and SocialAccount from allauth framework. I guess the joining part should be in serializers so after a research I came up with this:
from rest_framework import serializers
from django.contrib.auth.models import User
from allauth.socialaccount.models import SocialAccount
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('pk', 'first_name', 'last_name')
class SocialSerializer(serializers.ModelSerializer):
user = UserSerializer(many=False, read_only=True)
class Meta:
model = SocialAccount
fields = ('uid', 'provider', 'user')
It works but it outputs it as nested objects:
{
"uid": "",
"provider": "",
"user": {
"pk": 5,
"first_name": "",
"last_name": ""
}
}
I would like it to be as one object:
{
"uid": "",
"provider": "",
"pk": 5,
"first_name": "",
"last_name": ""
}

alternatively, try
class SocialSerializer(serializers.ModelSerializer):
user = UserSerializer()
class Meta:
model = SocialAccount
fields = ('uid', 'provider', 'user')
def to_representation(self, instance):
data = super(SocialSerializer, self).to_representation(instance)
user = data.pop('user')
for key, val in user.items():
data.update({key: val})
return data

You can either try to flatten the JSON (see this link) or redefine your serializer as below:
class SocialSerializer(serializers.ModelSerializer):
pk = serializers.SerializerMethodField()
first_name = serializers.SerializerMethodField()
last_name = serializers.SerializerMethodField()
class Meta:
model = SocialAccount
fields = ('uid', 'provider', 'pk', 'first_name', 'last_name')
def get_pk(self, obj):
return obj.user.pk
def get_first_name(self, obj):
return obj.user.first_name
def get_last_name(self, obj):
return obj.user.last_name
These are serializers.SerializermethodField() fields which will look at the get_<field_name> method and call them and use the returned value.
http://www.django-rest-framework.org/api-guide/fields/#serializermethodfield

Related

Django foreign key rest framework

Is there any way to display the name of the user in the "likedBy" section of the view, instead of the user id? Using django rest framework
From view I get , ignore comments:
[
{
"id": 3,
"title": "ddsa",
"content": "dsadsa",
"created": "2021-02-10T08:07:42.758400Z",
"updated": "2021-02-10T08:07:42.758400Z",
"author": 1,
"category": [
{
"pk": 1,
"name": "Life"
}
],
"likedBy": [
1
],
"comments": [
{
"id": 2,
"content": "ghfa",
"created": "2021-02-10T08:08:02.407950Z",
"post": 3,
"author": 1,
"likedBy": [
1
]
}
]
}
]
Views.py:
class PostViewSet(FlexFieldsMixin, generics.ListAPIView):
queryset = Post.objects.all()
serializer_class = PostSerializer
permit_list_expands = ['category', 'comments']
Models.py
class Post(models.Model):
title = models.CharField(max_length=255)
content = models.TextField()
category = models.ManyToManyField(Category, related_name='posts')
author = models.ForeignKey(User, on_delete=models.CASCADE)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
likedBy = models.ManyToManyField(User, related_name='posts', blank=True)
class Meta:
ordering = ['-created']
Serializers.py:
class PostSerializer(FlexFieldsModelSerializer):
class Meta:
model = Post
fields = '__all__'
expandable_fields = {
'category': ('blogApi.CategorySerializer', {'many': True}),
'comments': ('blogApi.CommentSerializer', {'many': True}),
}
How serialize ManyToMany field to display text values
Given that you are not serializing a relation, but rather an attribute of your model which is related to your user, I believe you have to use a serializer.SerializerMethodField(). This allows you to do the following:
class PostSerializer(FlexFieldsModelSerializer):
liked_by = serializers.SerializerMethodField(method_name="get_user_likes")
class Meta:
model = Post
fields = (
"title",
"content",
"category",
"author",
"created",
"update",
"liked_by"
)
expandable_fields = {
'category': ('blogApi.CategorySerializer', {'many': True}),
'comments': ('blogApi.CommentSerializer', {'many': True}),
}
#classmethod
def get_user_likes(obj):
# do whatever you like here, but I tend to call a
# method on my model to keep my serializer file
# nice and tidy
# you'll need to define a UserSerializer
return UserSerializer(obj.get_user_likes(), many=True, read_only=True)
And in your Post model:
class Post(models.Model):
title = models.CharField(max_length=255)
content = models.TextField()
category = models.ManyToManyField(Category, related_name='posts')
author = models.ForeignKey(User, on_delete=models.CASCADE)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
likedBy = models.ManyToManyField(User, related_name='posts', blank=True)
class Meta:
ordering = ['-created']
def get_user_likes(self):
return self.likedBy.all()
You can of course define the full method in your serializer, but as I said I like to keep the serializer as clean as possible and put all methods associated with models in my models.py.
#classmethod
def get_user_likes(obj):
# you'll need to define a UserSerializer
return UserSerializer(obj.likedBy.all(), many=True, read_only=True)
So you can set
likedBy = serializers.ReadOnlyField(source='get_likedBy')
in your PostSerializer class
and define function in your Post model class like below:
#property
def get_likedBy(self):
liked_by = []
for user in self.users_liked_post.all():
liked_by.append(user.name)
return liked_by
just use correct related_name instead of users_liked_post
If you add likedBy to your expandable fields, and then add ?expand=likedBy to your url, it should give you all the information that you outline in the UserSerializer, or write a new serializer named LikedBySerializer. Also as a general rule, try not to use '__all__' it's a good way to leak data. Happy coding!
class LikedBySerializer(FlexFieldsModelSerializer):
class Meta:
model = User
fields = '__all__'
class PostSerializer(FlexFieldsModelSerializer):
class Meta:
model = Post
fields = '__all__'
expandable_fields = {
'category': ('blogApi.CategorySerializer', {'many': True}),
'comments': ('blogApi.CommentSerializer', {'many': True}),
'likedBy': ('blogApi.LikedBySerializer', {'many': True}),
}

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

Django Rest Framework - Post Foreign Key

I am new to Django Rest Framework and checked some tutorials. Now I am trying to create my own structure which is like following. I want to create a user which is OK, then create a profile seperately.
models.py
class User(models.Model):
name = models.CharField(max_length=32)
surname = models.CharField(max_length=32)
facebook_id = models.TextField(null=True)
is_sms_verified = models.BooleanField(default=False)
created = models.DateTimeField(default=timezone.now)
updated = models.DateTimeField(default=timezone.now)
status = models.BooleanField(default=1)
def __str__(self):
return self.name+" "+self.surname
class Profile(models.Model):
user = models.ForeignKey('User',on_delete=models.CASCADE)
email = models.CharField(max_length=32)
birthday = models.DateField(null=True)
bio = models.TextField(null=True)
points = models.IntegerField(default=0)
created = models.DateTimeField(default=timezone.now)
updated = models.DateTimeField(default=timezone.now)
def __str__(self):
return self.user.name+ " " + self.user.surname
serializers.py
class UserSerializer(serializers.ModelSerializer):
class Meta:
model=User
fields = ('id','name','surname','facebook_id','is_sms_verified',)
read_only_fields = ('created','updated')
class ProfileSerializer(serializers.ModelSerializer):
user = UserSerializer(read_only=True)
class Meta:
model=Profile
fields=('id','user','email','birthday','bio','points')
read_only_fields = ('created','updated')
views.py
#api_view(['POST'])
def profile_create(request):
serializer = ProfileSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return JsonResponse(serializer.data, status = status.HTTP_201_CREATED)
return JsonResponse(serializer.errors , status= status.HTTP_400_BAD_REQUEST)
data I'm trying to post
{
"user_id": {
"id": 2
},
"email": "xxx#gmail.com",
"birthday": "1991-05-28",
"bio": "qudur",
"points": 31
}
The error I get;
NOT NULL constraint failed: core_profile.user_id
Where am I doing wrong? Thanks!
Your ProfileSerializer has user as readonly. So you need to change that. I would suggest doing it like this
class ProfileSerializer(serializers.ModelSerializer):
class Meta:
model=Profile
fields=('id','user','email','birthday','gender','bio','points')
read_only_fields = ('created','updated')
def to_representation(self, instance):
self.fields['user'] = UserSerializer(read_only=True)
return super(ProfileSerializer, self).to_representation(instance)
If you do it this you could provide your user as plain id for POST
{
"user": 2,
"email": "xxx#gmail.com",
"birthday": "1991-05-28",
"bio": "qudur",
"points": 31
}
And when you will read data it will look like this
{
"user": {
"id": 2,
"name": "Name",
"surname": "Surname",
...
},
"email": "xxx#gmail.com",
"birthday": "1991-05-28",
"bio": "qudur",
"points": 31
}
I've noticed Super() throws an error the way it's mentioned above in the awnser:
return super(ProfileSerializer,self).to_representation(instance)
Error: Type error, object must be an instance or subtype of type
Try the Following:
Models.py
class Program(models.Model):
name = models.CharField(max_length=225)
cost = models.IntegerField(default=0)
description = models.TextField(default="", max_length=555)
class UserProgram(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
program = models.ForeignKey(Program, on_delete=models.CASCADE, related_name="program")
Serializers.py
class ProgramSerializers(serializers.ModelSerializer):
class Meta:
model = Program
fields = "__all__"
class UserProgramSerializers(serializers.ModelSerializer):
class Meta:
model = UserProgram
fields = "__all__"
#IMPORTANT PART
def to_representation(self, instance):
response = super().to_representation(instance)
response['program'] = ProgramSerializers(instance.program).data
return response
Views.py
class UserProgramViewset(viewsets.ModelViewSet):
permission_classes = [
permissions.IsAuthenticated
]
serializer_class = UserProgramSerializers
def get_queryset(self):
return UserProgram.objects.filter(user=self.request.user)
def perform_create(self, serializer):
serializer.save(user=self.request.user)
When you call the GET request the following should be the output:
GET Request Output
When you call the POST request you only need to pass the programID and not the whole JSON dictionary!
Hope this helped.

How can the foreign field shows the name instead of its id?

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

Custom serializer output for nested relationship field in DRF serializer class

I am in little trouble while developing a web api's using Django-Rest-Framework(DRF).
Problem Statement
I have two models User and Review.
models.py
# Consider User model as `django.contrib.auth.models.User`
from django.contrib.auth.models import User
from django.db import models
# Review model
class Review(models.Model):
head = models.CharField()
content = models.CharField()
user = models.ForeignKey(User)
is_deleted = models.BooleanField(default=False)
and, i have two endpoints like this:
/users/ - list of all users
/users/<pk> - detail of user
/review/ - list of all reviews
/review/<pk> - detail of review
I want my output like this:
# /users/
[
{
"url": "http://localhost:8000/users/1",
"fisrt_name": "Adolf",
"last_name": "Hitler",
"email": "adolfhilter#xyz.com",
"is_staff": false
........ # other fields
},
.........
.........
]
# /reviews/
[
{
"url": "http://localhost:8000/reviews/1",
"head": "Head of Review",
"content": "Content of Review",
"user": {
"url": "http://localhost:8000/users/1",
"first_name": "Adolf",
"last_name": "Hitler"
},
"is_deleted": false
},
.........
.........
]
My Solution
To achieve this form of output i created three serializers class, one is UserSerializer class , second one is ReviewSerializer, and thir one id ReviewUserSerializer.These classes are as follows:
serializers.py
from rest_framework import serializers
from .models import Review
from django.contrib.auth.models import User
class UserSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = User
fields = ('url', 'first_name', 'last_name', 'email', 'is_staff', .....)
class ReviewUserSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = User
fields = ('url', 'first_name', 'last_name')
class ReviewSerializer(serializers.HyperlinkedModelSerializer):
user = ReviewUserSerializer()
class Meta:
model = Review
fields = ('url', 'head', 'content', 'user')
So, now i want to know that ,
Is there any other way which can avoid to create an extra separate serializer class(here is ReviewUserSerializer) for these type of situations?
If yes, then suggest me a solution with proper code snippets.
Have you tried using a SerializerMethodField?
from rest_framework import serializers
from .models import Review
from django.contrib.auth.models import User
class UserSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = User
fields = ('url', 'first_name', 'last_name', 'email', 'is_staff', .....)
class ReviewSerializer(serializers.HyperlinkedModelSerializer):
user = serializers.SerializerMethodField(read_only=True)
class Meta:
model = Review
fields = ('url', 'head', 'content', 'user')
def get_user(self, obj) :
request = self.context['request']
return {
'url': reverse('user-detail',
kwargs={'pk': obj.user.id}, request=request),
'first_name': obj.user.first_name,
'last_name': obj.user.last_name,
}

Categories