Im working on a django project, and I have a model that looks like this:
class Comment(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, default=1,on_delete=models.CASCADE)
slug = models.SlugField(unique=True,null=True, blank=True)
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
parent = models.ForeignKey("self", null=True, blank=True,on_delete=models.CASCADE)
content = models.TextField()
timestamp = models.DateTimeField(auto_now_add=True)
objects = CommentManager()
def __str__(self):
return str(self.user.username)
I use this serializer:
class CommentDetailSerializer(ModelSerializer):
user = SerializerMethodField()
replies = SerializerMethodField()
class Meta:
model = Comment
fields = [
'user',
'content',
'replies',
'timestamp'
]
def get_user(self,obj):
return str(obj.user.username)
def get_replies(self,obj):
if obj.is_parent:
return CommentChildSerializer(obj.children(),many=True).data
for this view:
class CommentDetailApiView(RetrieveAPIView):
queryset = Comment.objects.all()
serializer_class = CommentDetailSerializer
lookup_field = 'slug'
here is the PostSerializer I use,
class PostSerializer(ModelSerializer):
user = SerializerMethodField()
comments = SerializerMethodField()
class Meta:
model = Post
fields = [
'user',
'title',
'content',
'comments'
]
def get_user(self,obj):
return str(obj.user.username)
def get_comments(self,obj):
comments_qs = Comment.objects.filter_by_instance(obj)
comments = CommentSerializer(comments_qs, many=True).data
return comments
this is what I get in the PostDetailAPIView:
{
"user": "abc",
"title": "blabla",
"content": "bla",
"comments": [
{
"user": 1,
"content": "hey",
"timestamp": "2020-02-18T00:07:29.932850Z"
}
]
}
How do I get the comment user username instead of its id?
I get the username of the comment user only in the CommentDetailApiView.
Thank you
I added the get_user method to the CommentSerializer and now it works:
class CommentSerializer(serializers.ModelSerializer):
user = SerializerMethodField()
class Meta:
model = Comment
fields = [
'user',
'content',
'timestamp'
]
def get_user(self,obj):
return str(obj.user.username)
You can add an UserSerializer to indicate what you want to show of the user:
from django.contrib.auth.models import User
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('username',)
class CommentDetailSerializer(ModelSerializer):
user = UserSerializer()
..... # the rest of your code
You need to override to_representation() function just like below:
def to_representation(self, instance):
"""Convert `username` to lowercase."""
ret = super().to_representation(instance)
ret['username'] = ret['username'].lower()
return ret
The above code is just an example. It will return username in lowercase letters. In your case just change the line
ret['username'] = ret['username'].lower()
to
ret['username'] = self.validated_data['username']
StringRelatedField may be used to represent the target of the relationship using its __str__ method.
For example, the following serializer.
class CommentDetailSerializer(serializers.ModelSerializer):
user = serializers.StringRelatedField()
...
class Meta:
model = Comment
fields = [
'user',
'content',
'replies',
'timestamp'
]
...
Related
I have a field which is ManyToMany. I would like to enter the value in POSTMAN for API post operation. But everytime It says: "This field is required." even though I provided the value.
Models:
class Day(models.Model):
day_name = models.CharField(
_("Day Name"), max_length=255, null=True, blank=True)
def __str__(self):
return self.day_name
class TutorProfile(models.Model):
user = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
tutor_availablility = models.ManyToManyField(
Day,blank=True)
Serializer:
class DaySerializer(serializers.ModelSerializer):
class Meta:
model = Day
fields = '__all__'
class TutorProfileSerializer(serializers.ModelSerializer):
user = serializers.PrimaryKeyRelatedField(
read_only=True, default=serializers.CurrentUserDefault(), source='user.username')
image_url = serializers.SerializerMethodField('get_image_url')
tutor_availablility = DaySerializer(many=True)
class Meta:
model = TutorProfile
fields = '__all__'
Viewsets:
#authentication_classes([TokenAuthentication])
#permission_classes([IsAuthenticated])
class TutorprofileViewSet(ModelViewSet):
serializer_class = TutorProfileSerializer
http_method_names = ["post", "delete", "get"]
queryset = TutorProfile.objects.all()
With the following models.py (notice that your current Day.__str__ can raise an exception if day_name does not exist):
class Day(models.Model):
day_name = models.CharField(_("Day Name"), max_length=255, blank=True, null=True)
def __str__(self):
return self.day_name if self.day_name else "Unnamed"
class TutorProfile(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
tutor_availability = models.ManyToManyField(Day, blank=True)
You do not need to explicitly add tutor_availability nor user as serializer fields:
class DaySerializer(serializers.ModelSerializer):
class Meta:
model = Day
fields = "__all__"
class TutorProfileSerializer(serializers.ModelSerializer):
# Omitting `image_url` as not reflected in `models.py`
# image_url = serializers.SerializerMethodField('get_image_url')
class Meta:
model = TutorProfile
fields = "__all__"
With this viewset:
#authentication_classes([TokenAuthentication])
#permission_classes([IsAuthenticated])
class TutorProfileViewSet(ModelViewSet):
serializer_class = TutorProfileSerializer
http_method_names = ["post", "delete", "get"]
queryset = TutorProfile.objects.all()
Then, after creating days with IDs 1 and 2 in admin, by sending the tutor_availability field as you are doing it, it should work. Request:
{
"user": 1,
"tutor_availability": [1, 2]
}
Response:
{
"id": 1,
"user": 1,
"tutor_availability": [
1,
2
]
}
Notice as well that I've changed availablility to availability and that it may be unsafe to allow authenticated users to pass the user field in the request, you may want to infer that from the user who makes the request.
In your TutorProfileSerializer you are using the DaySerializer for tutor_availablility field so when you do a post request your post action will wait for a list of dict, what you you need to do in first is to delete this line : from your TutorProfileSerializer and it will works.
tutor_availablility = DaySerializer(many=True)
If you still have the problem then you need to verify the validate method of the TutorProfileSerializer.
And if it works but you want a list of dict(of Day object) for GET request, you need to override the get_serializer_class() of your ViewSet and create two serializers one for post request and a second for get request:
#authentication_classes([TokenAuthentication])
#permission_classes([IsAuthenticated])
class TutorprofileViewSet(ModelViewSet):
serializer_class = TutorProfileSerializer
http_method_names = ["post", "delete", "get"]
queryset = TutorProfile.objects.all()
def get_serializer_class(self):
if self.action.method == 'GET':
return TutorGETProfileSerializer
return super(TutorprofileViewSet, self).get_serializer_class()
and the 2 serializers:
class TutorGETProfileSerializer(serializers.ModelSerializer):
user = serializers.PrimaryKeyRelatedField(
read_only=True, default=serializers.CurrentUserDefault(), source='user.username')
image_url = serializers.SerializerMethodField('get_image_url')
tutor_availablility = DaySerializer(many=True)
class Meta:
model = TutorProfile
fields = '__all__'
class TutorProfileSerializer(serializers.ModelSerializer):
class Meta:
model = TutorProfile
fields = '__all__'
read_only_fields = ('user',)
I have included the serializer as well as the views.py, I am new so it might be possible that i made a very silly mistake here, which I am not able to figure out, please review this code and make me understand how to resolve this issue.
serializer.py
class ServiceSerializer(serializers.RelatedField):
def to_representation(self, value):
return value.name
class Meta:
model = Service
fields = ('name')
class SaloonSerializer(serializers.Serializer):
services = ServiceSerializer(read_only = True, many=True)
class Meta:
model = Saloon
fields = (
'name', 'services'
)
Here in the field of SaloonSerializers I have tried multiple things like only name field but still if get just one output which i have attache at the end of this post.
views.py
#api_view(['GET'])
def saloon_list(request):
if request.method=="GET":
saloons = Saloon.objects.all()
serializer = SaloonSerializer(saloons, many=True)
return Response(serializer.data)
models.py
class Service(models.Model):
name = models.CharField(max_length=200, blank=False)
price = models.IntegerField(blank=False)
time = models.IntegerField(blank=False)
def __str__(self):
return self.name
class Saloon(models.Model):
name = models.CharField(max_length = 200, blank=False)
services = models.ManyToManyField(Service)
def __str__(self):
return self.name
output:
[
{
"services": [
"Cutting",
"Shaving",
"Eye Brow",
"Waxing",
"Facial massage"
]
},
{
"services": [
"Cutting",
"Shaving",
"Facial massage"
]
}
]
You need to work with a ModelSerializer: a simple serializer does not care about a Meta, and will only serialize items for which you have specified a serializer field yourself.
You thus should rewrite this to:
# ModelSerializer ↓
class SaloonSerializer(serializers.ModelSerializer):
services = ServiceSerializer(read_only = True, many=True)
class Meta:
model = Saloon
fields = (
'name', 'services'
)
I have created model with many to many relationship and I have join table when I keep additional variable for it:
class BorderStatus(models.Model):
STATUS_CHOICES = [("OP", "OPEN"), ("SEMI", "CAUTION"), ("CLOSED", "CLOSED")]
origin_country = models.ForeignKey(OriginCountry, on_delete=models.CASCADE, default="0")
destination = models.ForeignKey(Country, on_delete=models.CASCADE, default="0")
status = models.CharField(max_length=6, choices=STATUS_CHOICES, default="CLOSED")
extra = 1
class Meta:
unique_together = [("destination", "origin_country")]
verbose_name_plural = "Border Statuses"
def __str__(self):
return (
f"{self.origin_country.origin_country.name} -> {self.destination.name}"
f" ({self.status})"
)
Other models:
# Create your models here.
class Country(models.Model):
name = models.CharField(max_length=100, unique=True, verbose_name='Country')
class Meta:
verbose_name_plural = "Countries"
def __str__(self):
return self.name
class OriginCountry(models.Model):
origin_country = models.ForeignKey(
Country, related_name="origins", on_delete=models.CASCADE
)
destinations = models.ManyToManyField(
Country, related_name="destinations", through="BorderStatus"
)
class Meta:
verbose_name_plural = "Origin Countries"
def __str__(self):
return self.origin_country.name
Here is my serializer for the endpoint:
class BorderStatusEditorSerializer(serializers.ModelSerializer):
"""Create serializer for editing single connection based on origin and destination name- to change status"""
origin_country = serializers.StringRelatedField(read_only=True)
destination = serializers.StringRelatedField(read_only=True)
class Meta:
model = BorderStatus
fields = ('origin_country', 'destination', 'status')
And my endpoint:
class BorderStatusViewSet(viewsets.ModelViewSet):
queryset = BorderStatus.objects.all()
serializer_class = BorderStatusEditorSerializer
filter_backends = (DjangoFilterBackend,)
filter_fields=('origin_country','destination')
The problem Im having is that I cant create any new combination for the BorderStatus model in this serializer via post request.
If I remove the lines:
origin_country = serializers.StringRelatedField(read_only=True)
destination = serializers.StringRelatedField(read_only=True)
Then the form will work, but then I wont have the string representation of those variables, instead I get IDs.
Is there any way to allow request to accept origin_country and destination while being related fields?
EDIT:
To clarify how OriginCountry works, it is has a nested field:
[{ "id": 1
"origin_country": "Canada",
"dest_country": [
{
"id": 1,
"name": "France",
"status": "CLOSED"
},
{
"id": 2,
"name": "Canada",
"status": "OP"
}
]
},
]
You can try to override perform_create method of the viewset to make the necessary adjustments on-the-fly when new entry is posted:
class BorderStatusViewSet(viewsets.ModelViewSet):
queryset = BorderStatus.objects.all()
serializer_class = BorderStatusEditorSerializer
filter_backends = (DjangoFilterBackend,)
filter_fields=('origin_country','destination')
def perform_create(self, serializer):
origin_country, _ = models.Country.get_or_create(name=self.request.data.get('origin_country')
destination, _ = models.Country.get_or_create(name=self.request.data.get('destination')
return serializer.save(origin_country=origin_country, destination=destination)
Maybe you will also need to adjust your serializer to have:
class CountrySerializer(serializers.ModelSerializer):
class Meta:
model = Country
fields = ['name']
class BorderStatusEditorSerializer(serializers.ModelSerializer):
origin_country = CountrySerializer()
destination = CountrySerializer()
...
Yes, I will try to give this combination.
You get this error because of Incorrect Type exception. Django checks data type validation on the serializer. For example here your dest_country returns a list of dicts but in your model it is a primary key (pk)
That's why on post django says : pk value expected, list received
But you can solve this error by using two different serializers (one to post another by default)
1. Create two different serializers
class BorderStatusEditorSerializer(serializers.ModelSerializer):
"""The First serialiser by default"""
origin_country = serializers.StringRelatedField(read_only=True)
destination = serializers.StringRelatedField(read_only=True)
class Meta:
model = BorderStatus
fields = ('origin_country', 'destination', 'status')
class BorderStatusEditorCreateSerializer(serializers.ModelSerializer):
"""The Second serialiser for create"""
class Meta:
model = BorderStatus
fields = ('origin_country', 'destination', 'status')
2.Add get_serializer_class method to for Viewset
class BorderStatusViewSet(viewsets.ModelViewSet):
queryset = BorderStatus.objects.all()
filter_backends = (DjangoFilterBackend,)
filter_fields=('origin_country','destination')
serializer_classes = {
'create': BorderStatusEditorCreateSerializer, # serializer used on post
}
default_serializer_class = BorderStatusEditorSerializer # Your default serializer
def get_serializer_class(self):
return self.serializer_classes.get(self.action, self.default_serializer_class)
I have a Post Model contains tags field that have ManyToManyField to categories Model,
when i call REST ListAPIView all post tags returns in pk
I have tried to override list function in ListAPIView and map all tags_names for every post
but this takes a huge time and destroys the performance
I hope/Believe there something built in for this case
models.py
class Post(models.Model):
author = models.ForeignKey(User, on_delete=models.CASCADE)
title = models.CharField(max_length=256)
content = RichTextField()
tags = models.ManyToManyField(Categories)
def __str__(self):
return self.title
class Categories(models.Model):
tag_name = models.CharField(max_length=256, unique=True)
def __str__(self):
return self.tag_name
class Meta:
ordering = ('tag_name',)
unique_together = ('tag_name',)
views.py
from .models import Post
from .serializers import NewPostSerializer, PostSerializer
class NewPost(CreateAPIView):
serializer_class = NewPostSerializer
permission_classes = [IsAuthenticated, IsAdminUser]
class PostList(ListAPIView):
queryset = Post.objects.all()
serializer_class = PostSerializer
serializers.py
class NewPostSerializer(ModelSerializer):
class Meta:
model = Post
fields = ['title', 'content', 'tags']
read_only_fields = ['tags', 'author_id']
when i visit ListApiView link returned results would be like this:
[
{
"id": 30,
"title": "post title test",
"content": "lorem text",
"author": 3,
"tags": [
8, # should be games
3 # should be action
]
}
]
You can simply use SlugRelatedField, This will return a list of names instead of list of pks
from rest_framework import serializers
class NewPostSerializer(ModelSerializer):
tags = serializers.SlugRelatedField(
many=True,
read_only=True,
slug_field='tag_name'
)
class Meta:
model = Post
fields = ['title', 'content', 'tags']
read_only_fields = ['author_id']
To optimize performance you should use prefetch_related. This reduces the number of queries to your database to only 1 request to fetch all the related tags for all of your posts.
class PostList(ListAPIView):
queryset = Post.objects.prefetch_related('tags').all()
serializer_class = NewPostSerializer
Now for the serialization of your tags you have to create a new serializer.
class TagSerializer(ModelSerializer):
class Meta:
model = Categories
fields = ['name']
You can then use this serializer in your NewPostSerializer.
class NewPostSerializer(ModelSerializer):
tags = TagSerializer(many=True, read_only=True)
class Meta:
model = Post
fields = ['title', 'content', 'tags']
read_only_fields = ['author_id']
The results of this should be "tags": [{"name": "ABC"},{"name": "EFG"}].
Let's say that we have models like below
class Movie(models.Model):
"""Stores a single movie entry."""
title = models.CharField(max_length=200, blank=False)
class Watchlist(models.Model):
"""Stores a user watchlist."""
user = models.ForeignKey(settings.AUTH_USER_MODEL,
related_name='watchlist',
on_delete=models.CASCADE)
movie = models.ForeignKey(Movie, related_name='watchlist',
on_delete=models.CASCADE)
added = models.BooleanField(default=False)
Serializer
class CustomUserSerializer(serializers.HyperlinkedModelSerializer):
"""Serializer for a custom user model with related user action."""
url = serializers.HyperlinkedIdentityField(
view_name='customuser-detail', lookup_field='username')
watchlist = serializers.HyperlinkedRelatedField(
many=True, view_name='watchlist-detail', read_only=True)
class Meta:
model = CustomUser
fields = ('url', 'username', 'watchlist')
and the view:
class CustomUserViewSet(viewsets.ReadOnlyModelViewSet):
"""
list:
Return a list of all the existing users.
retrieve:
Return the given user with user's watchlist.
"""
queryset = CustomUser.objects.all()
permissions = (IsAdminOrReadOnly)
lookup_field = 'username'
serializer_class = CustomUserSerializer
That all will give us a user and hyperlinked filed to the particular watchlist.
{
"url": "http://127.0.0.1:8000/api/v1/users/John/",
"username": "John",
"favorites": [
"http://127.0.0.1:8000/api/v1/watchlist/2/",
"http://127.0.0.1:8000/api/v1/watchlist/1/"
]
},
but instead of that I would like to get a particular movie instance like that.
{
"url": "http://127.0.0.1:8000/api/v1/users/John/",
"username": "John",
"favorites": [
"http://127.0.0.1:8000/api/v1/movies/33/",
"http://127.0.0.1:8000/api/v1/movies/12/"
]
},
so my question is how can I achieve that? I tried with hyperlinkedrelatedfield but nothing seems to work as expected.
You could use the SerializerMethodField along with reverse.
from rest_framework.reverse import reverse
class CustomUserSerializer(serializers.HyperlinkedModelSerializer):
"""Serializer for a custom user model with related user action."""
url = serializers.HyperlinkedIdentityField(
view_name='customuser-detail', lookup_field='username')
favorites = serializers.SerializerMethodField()
def get_favorites(self, obj):
movie_urls = [
reverse("movie-view", args=[watchlist.movie.id], request=self.context['request'])
for watchlist in obj.watchlist.all()
]
return movie_urls
class Meta:
model = CustomUser
fields = ('url', 'username', 'favorites')