How to Ordering Django SerializerMethodField with ManytoManyField Count - python

I want to make blog web , I want to order on a computed SerializerMethodField, such as likes_count. Here are my Code
models.py
class Post(models.Model):
title = models.CharField(max_length=255, unique=True)
content = models.TextField()
like = models.models.ManyToManyField(
settings.AUTH_USER_MODEL, related_name='likes', blank=True)
serializer.py
class PostSerializer(serializers.ModelSerializer):
likes_count = serializer.SerializerMethodField()
class Meta:
model = Post
fields =[ 'title, content, likes' ]
def get_likes_count(self, obj):
return obj.like.count()
views
class PostGenericAPIView(generics.GenericAPIView):
queryset = Post.objects.all()
serializer_class = PostSerializer
filter_backends = (filters.OrderingFilter)

You can just annotate the value of count and order by it. For example:
from django.db.models import Count
class PostGenericAPIView(generics.GenericAPIView):
queryset = Post.objects.annotate(likes_count=Count('like')).order_by('likes_count')
serializer_class = PostSerializer
filter_backends = (filters.OrderingFilter)
Also, to reduce DB hits, you can directly access the value of likes_count in serializer:
class PostSerializer(serializers.ModelSerializer):
likes_count = serializers.IntegerField() # No need for serializer method field

Related

Correct Way to Structure Models, Views and Serializers

I have the following structure of Parent and Child models, where the child references the parent.
class ParentModel(models.Model):
name = models.CharField(max_length=255)
class ChildModel(models.Model):
name = models.CharField(max_length=255)
parent = models.ForeignKey(
ParentModel, related_name='children', on_delete=models.CASCADE
)
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
class ParentSerializer(serializers.ModelSerializer):
class Meta:
model = ParentModel
fields = (
'name',
'children',
)
class ChildSerializer(serializers.ModelSerializer):
class Meta:
models = ChildModel
fields = (
'name'
)
class ParentViewSet(viewsets.ModelViewSet):
serializer_class = ParentSerializer
queryset = ParentModel.objects.all()
class ChildViewSet(viewsets.ModelViewSet):
serializer_class = ChildSerializer
def get_queryset(self):
user = self.request.user
return ChildModel.objects.filter(created_by=user)
I would like for ParentSerializer.children to only include the ChildModel objects with created_by as self.request.user.
What is the correct way to filter ParentSerializer.children to the current user?
I am open to changing the models as well.
First I think you got a n+1 issue with your code.
When DRF will serialize ParentModel, accessing current_parent.children.all() will produce an SQL query for each parent.
To prevent this you can use prefetch_related so:
class ParentViewSet(viewsets.ModelViewSet):
serializer_class = ParentSerializer
queryset = ParentModel.objects.prefetch_related(Prefetch("children"))
This will result in 2 SQL queries instead of N+1 (with N being the number of ParentModel row).
Additionnaly you can use prefetch_related to filter the related model:
class ParentViewSet(viewsets.ModelViewSet):
serializer_class = ParentSerializer
def get_queryset(self):
user = self.request.user
return ParentModel.objects.prefetch_related(Prefetch("children", queryset=ChildrenModel.objects.filter(created_by=user)))
Which is what you are looking for I think.

DRF Add annotated field to nested serializer

I have two serializers that represent comments and their nested comments. I'm provide a queryset to viewset with annotated field likes. But my problem is that field only working in parent serializer. When i add this field to nested serializer i got error
Got AttributeError when attempting to get a value for field likes on serializer CommentChildrenSerializer.
The serializer field might be named incorrectly and not match any attribute or key on the Comment instance.
Original exception text was: 'Comment' object has no attribute 'likes'.
Here is some my code. Thanks
Models.py
class Post(models.Model):
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
title = models.CharField(max_length=200)
slug = models.SlugField(blank=True)
body = models.TextField()
tags = TaggableManager(blank=True)
pub_date = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['-pub_date']
class Comment(models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE,
related_name='comments')
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
text = models.TextField(max_length=500)
pub_date = models.DateTimeField(auto_now=True)
parent = models.ForeignKey('self', blank=True, null=True,
on_delete=models.CASCADE, related_name='children')
class Meta:
ordering = ['-pub_date']
class Vote(models.Model):
comment = models.ForeignKey(Comment, on_delete=models.CASCADE,
related_name='votes')
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
choice = models.BooleanField(null=True)
Serializers.py
class PostRetrieveSerializer(PostSerializer):
comments = CommentSerializer(read_only=True, many=True)
author = AuthorInfoSerializer(serializers.ModelSerializer)
class Meta:
model = Post
fields = ['id', 'author', 'slug', 'title', 'body', 'tags', 'pub_date', 'comments']
class CommentChildrenSerializer(serializers.ModelSerializer):
author = AuthorInfoSerializer(read_only=True)
likes = serializers.IntegerField()
class Meta:
model = Comment
fields = ['author', 'id', 'text', 'pub_date', 'parent', 'likes']
class CommentSerializer(serializers.ModelSerializer):
author = AuthorInfoSerializer(read_only=True)
children = CommentChildrenSerializer(many=True)
likes = serializers.IntegerField()
class Meta:
ordering = ['pub_date']
model = Comment
fields = ['author', 'id', 'text', 'pub_date', 'children', 'likes']
Views.py
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all().prefetch_related(
Prefetch('comments', queryset=Comment.objects.filter(parent__isnull=True)
.annotate(likes=Count('votes__choice'))))
serializer_class = PostSerializer
permission_classes = [IsOwnerOrAdminOrReadOnly]
pagination_class = PostPagination
lookup_field = 'slug'
def get_serializer_class(self):
""""
Attach related comments
when get post detail
"""
if self.action == 'retrieve':
return PostRetrieveSerializer
return self.serializer_class
def perform_create(self, serializer):
serializer.save(author=self.request.user)
maybe you can do something like this, adding the like field in each child comment.
queryset = Post.objects.all()\
.prefetch_related(
Prefetch(
'comments',
queryset=Comment.objects\
.filter(parent__isnull=True)\
.annotate(likes=Count('votes__choice'))\
.prefetch_related(
'children',
queryset=Comments.objects.all()\
.annotate(likes=Count('votes__choice'))
)
)
)
I hope this help you.
Regards!
On your model level, add a custom property like the below.
class Comment(models.Model):
...
class Meta:
ordering = ['-pub_date']
#property
def likes(self):
return self.votes.count()
On your serializer add SerializerMethodField
class CommentChildrenSerializer(serializers.ModelSerializer):
author = AuthorInfoSerializer(read_only=True)
likes = serializers.SerializerMethodField() # Change here
class Meta:
model = Comment
fields = ['author', 'id', 'text', 'pub_date', 'parent', 'likes']
# method for the SerializerMethodField
def get_likes(self, obj):
return obj.likes
Update both Comment related serializers. I believe this approach is simpler than the current implementation.

Common models in Django Rest Framework

I found this very useful article how to use common models in the DRF.
Common Models in Django and the DRF
So I wanted to have a create_by, created_when, updated_by and updated_when attribute for all by objects in the database. I used the viewsets.ModelViewSet together with mixins.ListModelMixin and mixins.CreateModelMixin before and it worked. I just replaced the viewsets.ModelViewSet with my new CommonViewSet class.
This is my code:
views.py
class CommonViewSet(viewsets.ModelViewSet):
"""Ensure the models are updated with the requesting user."""
def perform_create(self, serializer):
"""Ensure we have the authorized user for ownership."""
serializer.save(created_by=self.request.user, updated_by=self.request.user)
def perform_update(self, serializer):
"""Ensure we have the authorized user for ownership."""
serializer.save(updated_by=self.request.user)
class TagViewSet(CommonViewSet,
mixins.ListModelMixin,
mixins.CreateModelMixin):
"""Manage tags in the database"""
authentication_classes = (TokenAuthentication,)
permission_classes = (IsAuthenticated,)
queryset = Tag.objects.all()
serializer_class = serializers.TagSerializer
serializers.py
class CommonSerializer(serializers.ModelSerializer):
"""Ensure the fields are included in the models."""
common_fields = ['created_by', 'created_at', 'updated_by', 'updated_at']
class TagSerializer(CommonSerializer):
"""Serializer for tag objects"""
class Meta:
model = Tag
fields = (['id', 'name'] + CommonSerializer.common_fields)
read_only_fields = ('id',)
models.py
class CommonModel(models.Model):
"""Common fields that are shared among all models."""
created_by = models.ForeignKey(settings.AUTH_USER_MODEL,on_delete=models.PROTECT,
editable=False, related_name="+")
updated_by = models.ForeignKey(settings.AUTH_USER_MODEL,on_delete=models.PROTECT,
editable=False, related_name="+")
created_at = models.DateTimeField(auto_now_add=True,
editable=False)
updated_at = models.DateTimeField(auto_now=True,
editable=False)
class Tag(CommonModel):
"""Tag to be used for device type"""
name = models.CharField(max_length=255)
def __str__(self):
return self.name
But now I get this error message:
class TagViewSet(CommonViewSet,
TypeError: Cannot create a consistent method resolution
order (MRO) for bases CreateModelMixin, ListModelMixin
The DRF ModelViewseT Written as follows
class ModelViewSet(mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
mixins.ListModelMixin,
GenericViewSet):
So in TagViewSet, You don't need to import mixins again.
You can write this as follows
class TagViewSet(CommonViewSet):
...

Failing to lookup a field with a foreign key subfield in Django Rest Framework

I am using the django framework to serialize a django object. I have a profile model and the first field in the model is a foreignkey of the user object. I want to be able to pass a usename is the url patters for the profile. In the Views, I want to be able to take that username and go through the user foreignkey and find the profile with that username user. THis is what I have right now in my code:
Views.py
class ProfileListView(ListAPIView):
query_set = Profile.objects.all()
serializer_class = ProfileSerializer
class ProfileDetailView(RetrieveAPIView):
query_set = Profile.objects.all()
serializer_class = ProfileSerializer
lookup_field = 'user__username'
urls.py
path('profile/', ProfileListView.as_view()),
path('profile/<username>', ProfileDetailView.as_view()),
serializers.py
class ProfileSerializer(serializers.ModelSerializer):
class Meta:
model = Profile
fields = ('user', 'gender', 'phone', 'ip_address', 'dob_month', 'dob_day',
'dob_year', 'address_street', 'address_city', 'address_state',
'address_postal', 'address_country', 'profile_pic', 'role')
models.py
class Profile(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
gender = models.CharField(max_length=12, choices=GENDER_CHOICES) # entity type
phone = models.CharField(max_length=22)
ip_address = models.CharField(max_length=32)
dob_month = models.CharField(max_length=22, choices=MONTH_CHOICES)
dob_day = models.SmallIntegerField()
dob_year = models.SmallIntegerField()
address_street = models.CharField(max_length=140)
address_city = models.CharField(max_length=22)
address_state = USStateField()
address_postal = models.IntegerField()
address_country = models.CharField(max_length=22)
profile_pic = models.ImageField(upload_to='profile_pics/')
role = models.CharField(max_length=12, choices=ROLE_CHOICES)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.user.username + ' - ' + self.type
One way of doing is to override get_object method. Try like this:
class ProfileDetailView(RetrieveAPIView):
queryset = Profile.objects.all() # it should be queryset not query_set
serializer_class = ProfileSerializer
def get_object(self):
return self.queryset.get(user__username=self.kwargs.get('username')).first()
There are some alternate solutions as well. For that please check this answer in SO.

Cannot post entry specifying specific id if the id is specified as ForeignKey in Django

I have two models, Book and ReadBy as specified in models.py:
class Book(models.Model):
created = models.DateTimeField(auto_now_add=True)
owner = models.ForeignKey(MyUser)
class ReadBy(models.Model):
created = models.DateTimeField(auto_now_add=True)
owner = models.ForeignKey(MyUser)
book = models.ForeignKey(Book)
views.py:
class BookViewSet(viewsets.ModelViewSet):
queryset = Book.objects.all()
serializer_class = BookSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
def pre_save(self, obj):
obj.owner = self.request.user
class ReadByViewSet(viewsets.ModelViewSet):
queryset = ReadBy.objects.all()
serializer_class = ReadBySerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
def pre_save(self, obj):
obj.owner = self.request.user
serializers.py:
class BookSerializer(serializers.ModelSerializer):
readby = serializers.SerializerMethodField('get_ready_by')
def get_read_by(self, obj):
user = self.context['request'].user
book = obj
return ReadBy.objects.filter(book=book, owner=user).exists()
class Meta:
model = Book
fields = ('id', 'created', 'owner', 'readby')
class ReadBySerializer(serializers.ModelSerializer):
owner = serializers.Field(source='owner.id')
book = serializers.Field(source='book.id')
class Meta:
model = ReadBy
fields = ('id', 'created', 'owner', 'book')
Book is a ForeignKey in ReadBy. The reason for this is that I want to see if the book has been read by defining the readby field in BookSerializer to true or false.
Now when I try to POST a ReadBy item I cannot explicitly set the book_id, even if I try do I still get the same error:
"Column 'book_id' cannot be null"
What I need to know is how I can explicitly specify book_id to be able to "read" a book.
Thanks.
class ReadBy(models.Model):
... snip ...
book = models.ForeignKey(Book, null=True, blank=True)
OK I solved this.
I forgot to define the book field in the ReadBySerializer as PrimaryKeyRelatedField:
book = serializers.PrimaryKeyRelatedField(source='book.id')
In fact it seems like I can remove this line altogether since PrimaryKeyRelatedField is default if nothing is specified.

Categories