Filtering querysets for nested models - python

I'm making a News App API and I want to create an APIView with comments for a speicific Post that also lets users posting comments for the specific post.
These are my models (simplified):
Post:
class Post(models.Model):
title = models.CharField(max_length=250)
text = models.TextField()
Comment:
class Comment(models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comments')
author = models.CharField(max_length=200)
text = models.TextField()
And view:
class CommentList(generics.ListCreateAPIView):
queryset = Comment.objects.filter(post=???)
serializer_class = CommentSerializer
EDIT: I would also like my url path to look like this (or similar):
urlpatterns = [
...
path('posts/<int:pk>/comments/', CommentList.as_view())
]
My questions:
How do I create a list of comments for an instance of Post?
Is it the correct approach or should I try something else?

If your url for comments is something like: /posts/post_id/comments/
# serializer
class CommentSerializer(serializers.ModelSerializer):
author = SomeAuthorSerializer(read_only=True)
class Meta:
model = Comment
fields = ('author', 'text')
# view
class CommentViewSet(viewsets.GenericViewSet,
mixins.CreateModelMixin,
mixins.ListModelMixin):
queryset = Comment.objects
serializer_class = CommentSerializer
def initial(self, request, *args, **kwargs):
super().initial(request, *args, **kwargs)
try:
self.post = Post.objects.get(pk=self.request.query_params.get('post_id'))
# Prefetch post object in this situation let you check permissions
# eg.:
if self.post.author != self.request.user:
raise PermissionDenied()
# remember that permission classes should be used !
except Post.DoesNotExist:
raise NotFound()
# It will filter your comments
def filter_queryset(self, queryset):
queryset = queryset.filter(post=self.post)
return super().filter_queryset(queryset)
# It will automatically bypass post object and author
# to your serializer create method
def perform_create(self, serializer):
serializer.save(post=self.post, author=self.request.user)

This is the solution that worked for me:
class CommentByPostList(generics.ListCreateAPIView):
queryset = Comment.objects.all()
serializer_class = CommentListSerializer
def get_queryset(self):
return Comment.objects.filter(post=self.kwargs['pk'])

Create a PostDetailView to GET a specific post then have the serializer return the comments for that Post.
# serializers.py
class CommentSerializer(serializers.ModelSerializer):
class Meta:
model = Comment
fields = '__all__'
class PostSerializer(serializers.ModelSerializer):
comments = CommentSerializer(many=True)
class Meta:
model = Post
fields = ('title', 'text', 'comments') # Comments field is recognized by the related_name set in your models.
# views.py
class PostDetailView(generics.RetreiveApiView):
permission_classes = (IsAuthenticated,)
serializer_class = PostSerializer
queryset = Post.objects.all()

Related

How to post with references in Django Rest Framework

I have Comment model related with User model
# models.py
class Comment(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
more fields...
....
In the serializer I want to do a create (POST) of a user comment.
But the post method is not enabled, only the put or patch method
Example: User Jon wants to create a comment
# serializers.py
class CommentSerializer(serializers.ModelSerializer):
class Meta:
model = Comment
fields = '__all__'
class UserCommentSerializer(serializers.ModelSerializer):
# id of comment
url = serializers.HyperlinkedIdentityField(
view_name="user-comments-detail",
read_only=True
)
id = serializers.CharField(read_only=True)
comment = CommentSerializer()
class Meta:
model = User
fields = ['id', 'comment']
def create(self, validated_data):
comment_data = validated_data.pop('comment')
user = User.objects.create(**validated_data)
Comment.objects.create(user=user, **comment_data)
return user
I want to new comment, referencing the user
# views.py
class CommentViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserCommentSerializer
But I have an exception, that the user has no comment RelatedObjectDoesNotExist
My url like this
http://localhost:8000/user-comments/10
10 is a user_id pk
{} object post
Example: Comment.objects.create(user=pk, {})
Currently, only put and patch is enabled, but what I want to do is post of user
{
"url": "http://localhost:8000/user-comments/10",
"id": "10",
"comment": null
}
Comment does not exist
Any idea or suggestion?
You actually need just one serializer for that.
This will create a comment for the current logged in user.
# serializers.py
class CommentSerializer(serializers.ModelSerializer):
class Meta:
model = Comment
fields = '__all__'
read_only_fields = ['user']
def create(self, validated_data):
# get the user who sent the request
user = self.context['request'].user
return Comment.objects.create(user=user, **validated_data)
# views.py
class CommentViewSet(viewsets.ModelViewSet):
queryset = Comment.objects.all()
serializer_class = CommentSerializer
def get_serializer_context(self):
# this is important since you want to pass the request object to your serializer
context = super().get_serializer_context()
context.update({"request": self.request})
return context
The exception you are getting is because ModelSerializer is linked to a specific model, in this case you linked UserCommentSerializer to model User. Variable Meta.fields specifies which fields of the model are being returned by the serializer and, so, you are getting an exception because such variable is set to ('id', 'comment') and model User doesn't have a field comment.
You can achieve what you want, this way:
class UserCommentSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id']
extra_kwargs = {'comment': {'write_only': True}}
def create(self, validated_data):
comment_data = validated_data.pop('comment')
user = User.objects.create(**validated_data)
Comment.objects.create(user=user, comment=comment_data)

how to merge models in django rest framework in one serializer class

i am trying to add CommentSerializer class to write comments with PostListSerializer , i want to display comment field bottom at every exist post
django:2.2
models.py
class Comment(models.Model):
id = models.UUIDField(default=uuid.uuid4,primary_key=True,editable=False)
author = models.ForeignKey(Account,on_delete=models.CASCADE,related_name='comments')
post= models.ForeignKey(Post,on_delete=models.CASCADE,related_name='comments')
comment_content = models.TextField(max_length=400)
commented_at = models.DateTimeField(auto_now_add=True)
edited_at = models.DateTimeField(auto_now=True)
image = models.ImageField(upload_to=random_url,blank=True,null=True)
parent = models.ForeignKey('self',null=True,blank=True,on_delete=models.CASCADE,related_name='replies')
active = models.BooleanField(default=True)
this is the serializers.py
class CommentCreateSerialzier(ModelSerializer):
class Meta:
model = Comment
fields = [
'comment_content','image'
]
class TripListSerializer(TaggitSerializer, GeoFeatureModelSerializerGIS):
tags = NewTagListSerializerField()
comments = SerializerMethodField()
detail_url = post_detail_url
user= SerializerMethodField()
class Meta:
model = Post
geo_field = 'location'
fields = [
'detail_url','user','name','city','tags','comments'
]
def get_user(self,obj):
return str(obj.user.username)
def get_comments(self,obj):
comment = Comment.objects.filter(post=obj,active=True)
comments = CommentSerialzier(comment,many=True).data
return comments
and this is my views.py
class CommentCreateSerializer(CreateAPIView):
serializer_class = CommentCreateSerialzier
message = 'you dont have permission to comment'
permission_classes = [IsAuthenticated]
def perform_create(self):
serializer.save(author=self.request.username)
how to add default post object in every single post ? and does it the same for replies as well
thanks for replying

Django REST Framework - Limit for Nested Serializer

I have the following models.py:
class Category(models.Model):
created = models.DateTimeField(auto_now_add=True)
name = models.CharField(max_length=100, blank=False)
class Movie(models.Model):
created = models.DateTimeField(auto_now_add=True)
name = models.CharField(max_length=100, blank=False)
category = models.ForeignKey(Category,related_name='movies',
on_delete=models.CASCADE,
blank=True,
null=True)
As you can see, there is a ForeignKey relationship between the two classes. A Category can have multiple movies, but a Movie belongs to only one Category.
My serializers.py looks like the following:
class CategorySerializer(serializers.HyperlinkedModelSerializer):
movies = MoviesSerializer(many=True, read_only=True)
class Meta:
model = Category
fields = ('url','id','created','name', 'movies')
class MovieSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Movie
fields = ('url','id','created','name', 'category')
So, I have nested serializers. When I deserialize the content of the categories, then it shows me also the nested movies in the JSON output as I expected.
But it shows me ALL movies belonging to a particular category in the JSON output. How can I limit this number ?
I tried this solution but it did not worked for me because I use serializers.HyperlinkedModelSerializer. In that provided solution they used serializers.ModelSerializer. I got this error when I tried that solution:
AssertionError: `HyperlinkedIdentityField` requires the request in the serializer context. Add `context={'request': request}` when instantiating the serializer.
UPDATE:
Here is my views.py:
class AllCategories(generics.ListAPIView):
'''
This class class-based view lists
all the categories created
'''
serializer_class = CategorySerializer
queryset = Category.objects.all()
def list(self, request, *args, **kwargs):
'''
standard method that we override just to
put the string 'allCategories' before the dataset
'''
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
# the only part we change
return Response({'allCategories': serializer.data})
class AllMovies(generics.ListAPIView):
'''
This class lists all the movies
'''
# define the serializer class
serializer_class = MovieSerializer
queryset = Movie.objects.all()
def list(self, request, *args, **kwargs):
'''
standard method that we override just to
put the string 'allMoviesOfUser' before the dataset
'''
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
# the only part we change
return Response({'allMoviesOfUser': serializer.data})

Django rest framework serializing many to many field

How do I serialize a many-to-many field into list of something, and return them through rest framework? In my example below, I try to return the post together with a list of tags associated with it.
models.py
class post(models.Model):
tag = models.ManyToManyField(Tag)
text = models.CharField(max_length=100)
serializers.py
class PostSerializer(serializers.ModelSerializer):
class Meta:
model = Post
fields = ("text", "tag"??)
views.py
class PostViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Post.objects.all()
serializer_class = PostSerializer
You will need a TagSerializer, whose class Meta has model = Tag. After TagSerializer is created, modify the PostSerializer with many=True for a ManyToManyField relation:
class PostSerializer(serializers.ModelSerializer):
tag = TagSerializer(read_only=True, many=True)
class Meta:
model = Post
fields = ('tag', 'text',)
Answer is for DRF 3
This is what I did, let´s suppose a Book can have more than one author and an Author can have more than one book:
On Model:
class Author(models.Model):
name = models.CharField(max_length=100, default="")
last_name = models.IntegerField(default=0)
class Book(models.Model):
authors = models.ManyToManyField(Author, related_name="book_list", blank=True)
name = models.CharField(max_length=100, default="")
published = models.BooleanField(default=True)
On Serializers:
class BookSerializer(serializers.ModelSerializer):
authors = serializers.PrimaryKeyRelatedField(queryset=Author.objects.all(), many=True)
class Meta:
model = Book
fields = ('id', 'name', 'published', 'authors')
class AuthorSerializer(serializers.ModelSerializer):
book_list = BookSerializer(many=True, read_only=True)
class Meta:
model = Author
fields = ('id', 'name', 'last_name', 'book_list')
Adding to #Brian's answer
"tags": [{"name": "tag1"}] can be simplified to "tags": ["tag1", "tag2",...] in this way:
class TagListingField(serializers.RelatedField):
def to_representation(self, value):
return value.name
class PostSerializer(serializers.ModelSerializer):
tag = TagListingField(many=True, read_only=True)
class Meta:
...
More info here: https://www.django-rest-framework.org/api-guide/relations/#custom-relational-fields
The default ModelSerializer uses primary keys for relationships. However, you can easily generate nested representations using the Meta depth attribute:
class PostSerializer(serializers.ModelSerializer):
class Meta:
model = Post
fields = ("text", "tag")
depth = 1
As mentioned in the documentation :
The depth option should be set to an integer value that indicates the depth of relationships that should be traversed before reverting to a flat representation.
This works for me.
tag = TagSerializer(source="tag", read_only=True, many=True)
Django 2.0
For many to many field, if you want specific one:
class QuestionSerializer(serializers.ModelSerializer):
topics_list = serializers.SerializerMethodField()
def get_topics_list(self, instance):
names = []
a = instance.topics.get_queryset()
for i in a:
names.append(i.desc)
return names
class Meta:
model = Question
fields = ('topics_list',)
In the serializer on init method you can pass the queryset to the field and rest_framework valide the ids on that queryset
1) first extend your serializer from serializers.ModelSerializer
class YourSerializer(serializers.ModelSerializer):
2) include the field on the meta class
class YourSerializer(serializers.ModelSerializer):
class Meta:
fields = (..., 'your_field',)
3) in the init method:
def __init__(self, *args, **kwargs):
super(YourSerializer, self).__init__(*args, **kwargs)
self.fields['your_field].queryset = <the queryset of your field>
You can limit the queryset for that field under any argument using filter or exclude like normally you do. In case that you want include all just use .objects.all()
models.py
class Tag(models.Model):
name = models.CharField(max_length=100)
# ===============
# ... rest of the fields ...
class Post(models.Model):
tag = models.ManyToManyField(Tag)
text = models.CharField(max_length=100)
serialiazers.py
class TagSerializer(serializers.ModelSerializer):
class Meta:
model = Tag
fields = '__all__'
class PostSerializer(serializers.ModelSerializer):
tags = TagSerializer(many=True, read_only=True)
class Meta:
model = Post
fields = ("text", "tag")
views.py
## FUNCTION BASED VIEW
def fbvPost_ListView(request):
# list
if request.method == "GET":
posts = Post.objects.all()
serializer = PostSerializer(instance=posts, many=True)
return JsonResponse(serializer.data, safe=False)
return JsonResponse({"success": False})
# ===========================================================
## CLASS BASED VIEW
class cbvPost_ListView(viewsets.ReadOnlyModelViewSet):
queryset = Post.objects.all()
serializer_class = PostSerializer
NB: Tag, Post are two models & we need to serialize them. Here, Post model have a dependency of Tag models, so here we explicitly mention it, [tags = TagSerializer(many=True, read_only=True)] or its return it's primary field value.
DETAILS HERE
Hi I will be showing many to many for update and create. The context is the event can have many dances and dances can have many event.
The request will be as followed.
{
"competition": 2,
"title": "the title",
"dances":[ {"id":1},{"id":2}],
"description": "the desc"
}
The Create Function will be as followed.
def create(self, validated_data):
try:
dance_ids = []
for dance in self.initial_data['dances']:
if 'id' not in dance:
raise serializers.ValidationError({'detail': 'key error'})
dance_ids.append(dance['id'])
new_event = models.Event.objects.create(**validated_data)
if dance_ids:
for dance_id in dance_ids:
new_event.dances.add(dance_id)
new_event.save()
return new_event
except Exception as e:
raise serializers.ValidationError({'detail': e})
The Update Function will be as followed.
def update(self, instance, validated_data):
# Delete all records of genres.
try:
for current_genre in instance.dances.all():
instance.dances.remove(current_genre)
# Repopulate genres into instance.
for dance in self.initial_data['dances']:
if 'id' not in dance:
raise serializers.ValidationError({'detail': 'key error'})
dance_obj = models.Dance.objects.get(pk=dance['id'])
instance.dances.add(dance_obj)
event_updated = super().update(instance, validated_data)
return event_updated
except Exception as e:
raise serializers.ValidationError({'detail': e})
If you want to just do "dances":[1,2] instead, just make some amendments to the
for dance in self.initial_data['dances']:
if 'id' not in dance:
raise serializers.ValidationError({'detail': 'key error'})
dance_ids.append(dance['id'])
part. I hope this will be able to help yall out! :)
First, Tag needs its own serializer too
class TagSerializer(serializers.ModelSerializer):
class Meta:
model = Tag
fields = '__all__'
Then in your PostSerializer, add one line
class PostSerializer(serializers.ModelSerializer):
tag = TagSerializer(read_only=True, many=True).data
class Meta:
model = Post
fields = ("text", "tag")
This will make it so your Tag field in Post is an array of tag ids. if you don't put the ".data" part, it will put all of the attributes of tag, which is probably too much in most cases
You can use serializers.SlugRelatedField() or serializers.StringRelatedField(many=True) Serializer relations
In your case :
class PostSerializer(serializers.ModelSerializer):
tag = serializers.StringRelatedField(many=True) # this will return a list
class Meta:
model = Post
fields = ('tag', 'text',)

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