Django REST framework - Getting data instead of links - python

I have taken a look around, and I didn't find an answer to this question.
Serializers.py
class PostSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Post
fields = ['title', 'body', 'comments', 'user', 'date']
class CommentSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Comment
fields = ['body', 'user', 'date']
class UserSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = User
fields = ['id', 'user']
Models.py
class Post(models.Model):
# Children
comments = models.ManyToManyField('Comment', blank=True)
# Content
title = models.TextField(default="-->Title here<--")
body = models.TextField(default="-->Body here<--")
# About
user = models.ForeignKey(User)
date = models.DateTimeField(auto_now=True)
object_id = models.PositiveIntegerField(default=0)
content_type = models.ForeignKey(ContentType, default=0)
content_object = fields.GenericForeignKey()
def __str__(self):
return str(self.title)
class Comment(models.Model):
comments = models.ManyToManyField('Comment', blank=True)
# Content
body = models.TextField(default="-->Body here<--")
# About
user = models.ForeignKey(User)
date = models.DateTimeField(auto_now=True)
object_id = models.PositiveIntegerField(default=0)
content_type = models.ForeignKey(ContentType, default=0)
content_object = fields.GenericForeignKey()
def __str__(self):
return str(self.body)
Views.py
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all()
serializer_class = PostSerializer
class CommentViewSet(viewsets.ModelViewSet):
queryset = Comment.objects.all()
serializer_class = CommentSerializer
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
def index(request):
return render(request, 'index.html', [])
The index template is a "homepage" that loads the latest posts. Under each post, I'm showing the comments. In the JSON those are links, and I found out how to load them trougth the links (so it's working).
Someone told me that instead of doing it this way, I should make it "load" the comments in the backend, and send them together with the posts (the data, not links). He told me to take a look into: http://www.django-rest-framework.org/api-guide/filtering/#overriding-the-initial-queryset
I can't really figure it out.
How do I get the data, insted of links for the ManyToManyField?

To unfold all the related data one level deep you can use depth param:
class PostSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Post
fields = ['title', 'body', 'comments', 'user', 'date']
depth = 1
This will replace post.user and post.comments ids with actual records. depth=2 will also unfold post.comments.user. If you want to selectively pull post.comments only one level deep and not post.user:
class PostSerializer(serializers.HyperlinkedModelSerializer):
comments = CommentSerializer(many=True) #has to be declared higher above
class Meta:
model = Post
fields = ['title', 'body', 'comments', 'user', 'date']
If on top you want to have post.comments.user unfolded, need to either put depth=1 into existing comment serializer or create a new serializer with unfolded users just for this view, similar to examples above.
Also make sure you are using prefetch_related on your queryset or the performance will take a serious hit, like:
Post.objects.all().prefetch_related('comments', 'comments__user')

Related

How can I get the records (of a specific model) of both request.user and a specific user?

I am not very professional in django rest...
I wrote a blog with django rest framework and There is no problem when I want to get all the records related to the Article model or get a specific article, for example
But what I want to do is to send an user id(or an user name) to the view when I click on the user's name.
and as a result display all the records of the Article model related to the request.user and all the records of the Article model related to the user whose name was clicked.
In fact, I want to click on the name of each user, in addition to getting the Articles of that user, the Articles related to the request.user will also be taken
This is what I have done so far...
#models.py
class Article(models.Model):
title = models.CharField(max_length=255)
slug = models.SlugField
author = models.ForeignKey(User , on_delete = models.CASCADE)
content = models.TextField(null = True)
publish = models.DateTimeField(default = timezone.now)
created = models.DateTimeField(auto_now_add = True)
updated = models.DateTimeField(auto_now = True)
status = models.BooleanField(default = False)
def __str__(self):
return self.title
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
name = models.CharField(max_length=100)
pic = models.ImageField(upload_to="img", blank=True, null=True)
def __str__(self):
return self.user.username
#views.py
class ArticleCreate(CreateAPIView):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
class ArticleList(ListAPIView):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
class ArticleDetail(RetrieveUpdateDestroyAPIView):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
class UserDetail(RetrieveUpdateDestroyAPIView):
queryset = get_user_model().objects.all()
serializer_class = UserSerializer
class UserProfile(RetrieveUpdateDestroyAPIView):
queryset = Profile.objects.all()
serializer_class = ProfileSerializer
#serializers.py
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = get_user_model()
fields = "__all__"
class ProfileSerializer(serializers.ModelSerializer):
class Meta:
model = Profile
fields = "__all__"
class ArticleSerializer(serializers.ModelSerializer):
class Meta:
model = Article
exclude = ['updated' , 'created']
You should directly make several modifications in get_queryset() method by using Q objects so:
class ArticleList(ListAPIView):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
def get_queryset(self):
user_id = self.kwargs.get('user_id')
if user_id:
articles = Article.objects.filter(Q(author_id=user_id) | Q(author=self.request.user))
return articles
return self.queryset
You'll also need to modify your urls.py file to include the user_id parameter in the URL so:
from django.urls import path
from .views import ArticleList
urlpatterns = [
path('articles/<int:user_id>/', ArticleList.as_view(), name='article_list'),
# ... Other routes.
]
example URL: http://example.com/api/purchases?username=denvercoder9
class ArticleList(ListAPIView):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
def get_queryset(self):
username = self.request.query_params.get('username')
if username:
return User.objects.filter(username=username).article_set.all()
user = self.request.user
return Article.objects.filter(author=user)

Django 'model' object is not iterable when response

I have 2 model. And the two models are connected to the ManyToManyField.
models.py
class PostModel(models.Model):
id = models.AutoField(primary_key=True, null=False)
title = models.TextField()
comments = models.ManyToManyField('CommentModel')
class CommentModel(models.Model):
id = models.AutoField(primary_key=True, null=False)
post_id = models.ForeignKey(Post, on_delete=models.CASCADE)
body = models.TextField()
and serializers.py
class CommentModel_serializer(serializers.ModelSerializer):
class Meta:
model = MainCommentModel
fields = '__all__'
class PostModel_serializer(serializers.ModelSerializer):
comment = MainCommentModel_serializer(many=True, allow_null=True, read_only=True)
class Meta:
model = MainModel
fields = '__all__'
and views.py
#api_view(['GET'])
def getPost(request, pk):
post = PostModel.objects.filter(id=pk).first()
comment_list = CommentModel.objects.filter(post_id=post.id)
for i in comments_list:
post.comments.add(i)
serializer = PostModel_serializer(post, many=True)
return Response(serializer.data)
There is an error when I make a request.
'PostModel' object is not iterable
and The trackback points here.
return Response(serializer.data)
I tried to modify a lot of code but I can't find solutions.
Please tell me where and how it went wrong.
Referring from this thread, you should remove many=True in PostModel_serializer.
Also it should be comment_list not comments_list.
#api_view(['GET'])
def getPost(request, pk):
post = PostModel.objects.filter(id=pk).first()
comment_list = CommentModel.objects.filter(post_id=post.id)
for i in comment_list:
post.comments.add(i)
serializer = PostModel_serializer(post)
return Response(serializer.data)
I think you did wrong while creating ManyToManyField().
Instead of this:
comments = models.ManyToManyField('CommentModel') #Here you made mistake. you should not add single quote to CommentModel. I think that's why you got that error
Try this:
comments = models.ManyToManyField(CommentModel)

Does using/calling method having calculating queryset in another method hits the database multiple times

I'm working on a DRF project to learn about ContentType models.
I created a post model and comment model(ContentType) and then added comments to the post. Everything was working fine until I added django-debug-tool and duplicated queries.
I have the following questions:
I've defined a method(children) and property(total_replies) on the comment model. Since total_replies just calling children method and count the size of queryset. Will it result in hitting the database two or more times in case I use the children method in some other methods or property?
If the database is hitting multiple times, what solution two improve performance?
After adding select_related the num of queries has been reduced drastically.
Before using select_related
After using select_related
Is it good to use select_related at all places where Foreignkey has been used?
Blog app
models.py
class Post(models.Model):
title = models.CharField(verbose_name=_("Post Title"), max_length=50)
content = models.TextField()
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='blog_posts')
category = models.ForeignKey(Category, on_delete=models.CASCADE)
def __str__(self):
return self.title
#property
def comments(self):
instance = self
#qs = Comment.objects.filter_by_instance(instance) #before
qs = Comment.objects.select_related('user').filter_by_instance(instance)
return qs
#property
def get_content_type(self):
instance = self
content_type = ContentType.objects.get_for_model(instance.__class__)
return content_type
serializers.py
class PostSerializer(serializers.ModelSerializer):
author = UserPublicSerializer(read_only=True)
status_description = serializers.ReadOnlyField(source='get_status_display')
class Meta:
model = Post
fields = (
'url', 'id', 'title', 'author',
'content', 'category', 'total_likes',
)
class PostDetailSerializer(serializers.ModelSerializer):
author = UserPublicSerializer(read_only=True)
status_description = serializers.ReadOnlyField(source='get_status_display')
comments = serializers.SerializerMethodField()
class Meta:
model = Post
fields = (
'url', 'id', 'title', 'author', 'content',
'category', 'comments', 'total_likes'
)
def get_comments(self, obj):
request = self.context.get('request')
comments_qs = Comment.objects.filter_by_instance(obj)
comments = CommentSerializer(comments_qs, many=True, context={'request':request}).data
return comments
class PostListCreateAPIView(generics.ListCreateAPIView):
serializer_class = serializers.PostSerializer
# queryset = Post.objects.all().order_by('-id') # before
queryset = Post.objects.select_related('author').order_by('-id')
name = 'post-list'
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
def perform_create(self, serializer):
serializer.save(author=self.request.user)
class PostRetrieveUpdateDestroyAPIView(generics.RetrieveUpdateDestroyAPIView):
serializer_class = serializers.PostDetailSerializer
# queryset = Post.objects.all().order_by('-id') # before
queryset = Post.objects.select_related('author').order_by('-id')
name = 'post-detail'
permission_classes = [permissions.IsAuthenticatedOrReadOnly, account_permissions.IsStaffOrAuthorOrReadOnly]
def perform_update(self, serializer):
serializer.save(author=self.request.user)
Comment app
models.py
class CommentManager(models.Manager):
def all(self):
qs = super().filter(parent=None)
return qs
def filter_by_instance(self, instance):
content_type = ContentType.objects.get_for_model(instance.__class__)
object_id = instance.id
qs = super().filter(content_type=content_type, object_id=object_id).select_related('user').filter(parent=None)
return qs
class Comment(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='comments')
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey(ct_field='content_type', fk_field='object_id')
parent = models.ForeignKey('self', null=True, blank=True, on_delete=models.CASCADE)
content = models.TextField()
objects = CommentManager()
def __str__(self):
if self.is_parent:
return f"comment {self.id} by {self.user}"
return f"reply {self.id} to comment {self.parent.id} by {self.user}"
def children(self):
return Comment.objects.select_related('user').filter(parent=self)
#property
def is_parent(self):
if self.parent is not None:
return False
return True
#property
def total_replies(self):
return self.children().count()
serializers.py
class CommentSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='comment-detail', lookup_field='pk')
user = UserPublicSerializer(read_only=True)
class Meta:
model = Comment
fields = ('url', 'user', 'id', 'content_type', 'object_id', 'parent', 'content', 'total_replies',)
class CommentChildSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='comment-detail', lookup_field='pk')
user = UserPublicSerializer(read_only=True)
class Meta:
model = Comment
fields = ('url', 'user', 'id', 'content',)
class CommentDetailSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='comment-detail', lookup_field='pk')
replies = serializers.SerializerMethodField()
class Meta:
model = Comment
fields = ('url', 'id', 'content_type', 'object_id', 'content', 'replies', 'total_replies',)
def get_replies(self, obj):
request = self.context.get('request')
if obj.is_parent:
return CommentChildSerializer(obj.children(), many=True, context={'request':request}).data
return None
views.py
class CommentListAPIView(generics.ListCreateAPIView):
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
queryset = Comment.objects.select_related('user').order_by('-id')
name = 'comment-list'
serializer_class = serializers.CommentSerializer
def perform_create(self, serializer):
serializer.save(user=self.request.user)
class CommentDetailAPIView(generics.RetrieveUpdateDestroyAPIView):
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
queryset = Comment.objects.select_related('user').all()
name = 'comment-detail'
serializer_class = serializers.CommentDetailSerializer
def perform_create(self, serializer):
serializer.save(user=self.request.user)
Thanks in advance.
That's exactly what django docs says about select_related :
"Returns a QuerySet that will “follow” foreign-key relationships, selecting additional related-object data when it executes its query. This is a performance booster which results in a single more complex query but means later use of foreign-key relationships won’t require database queries."
They describe select_related as something complex but good in term of transactional db cost.

Django Rest API- use prefetch_related with ModelSerializer

I have a comment field for every blog post. I want to pass Comment.objects.all() from Views.py to ModelSerializer def get_comments(self, obj) to reduce the number of sql queries. As I am serializing a list of blog posts
Views.py
class BlogViewSet(ModelViewSet):
queryset = Blog.objects.all().annotate(
author_name=F('author__username')
)
serializer_class = BlogSerializer
permission_classes = [IsOwnerOrReadOnly]
def list(self, request):
return Response({'blogs': BlogSerializer(self.queryset, many=True).data})
Serializers.py
class BlogSerializer(ModelSerializer):
author_name = serializers.CharField(read_only=True)
comments = SerializerMethodField()
class Meta:
model = Blog
fields = ('title_text', 'main_text', 'datepublished', 'author_name', 'id', 'comments')
def get_comments(self, obj):
# filter comment
comment_object = Comment.objects.filter(post_id=obj.id)
comments = CommentSerializer(comment_object, many=True).data
return comments
You don't have to pass anything from the View.
First, you have to change the comments field in your BlogSerializer.
class CommentSerializer(ModelSerializer):
# This serializer should have all the details of your comments.
....
class Meta:
model = Comment
fields = "__all__" # Or whatever fields you want to set.
class BlogSerializer(ModelSerializer):
author_name = serializers.CharField(read_only=True)
comments = CommentSerializer(many=True, read_only=True) # I am not sure of the source of your comment reverse manager name
class Meta:
model = Blog
fields = ('title_text', 'main_text', 'datepublished', 'author_name', 'id', 'comments')
Second, you have to make a small change to your view's queryset in order to reduce the number of queries sent to the database by using prefetch_related
class BlogViewSet(ModelViewSet):
queryset = Blog.objects.prefetch_related('comments').all().annotate(
author_name=F('author__username')
)
serializer_class = BlogSerializer
permission_classes = [IsOwnerOrReadOnly]
def list(self, request):
return Response({'blogs': BlogSerializer(self.get_queryset(), many=True).data})
I assumed in the code snippets that you didn't set a related_name on your Blog ForeignKey for the Comment model, so by default, its related manager on Blog will be comment_set
Update
Your models should look like this, in order for this solution to work
class Comment(models.Model):
...
blog = models.ForeignKey('Blog', on_delete=models.CASCADE, related_name='comments')
...
class Blog(models.Model):
# comment = models.ForeignKey(Comment, on_delete=models.CASCADE, null=True)
# this foreign key shouldn't be here, remove it.
....
Do the changes on Serializer and View
# in BlogSerializer
comments = CommentSerializer(many=True, read_only=True)
# In BlogViewSet
queryset = Blog.objects.prefetch_related('comments').all().annotate(
author_name=F('author__username')
)

How can I get a Django-Rest Framework with a sub-query on child related items working?

I've got a model that looks a bit like this:
Dataset
class Dataset(ClusterableModel):
group = models.ForeignKey(DataGroup, on_delete=models.CASCADE, related_name='datasets')
is_public = models.BooleanField(default=False)
title = models.CharField(max_length=255)
name = models.CharField(max_length=255, unique=True)
DataGroup
class DataGroup(models.Model):
name = models.CharField(max_length=255, unique=True)
And I've got something working in DRF that looks like this:
class DataGroupSerializer(serializers.HyperlinkedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='datagroup-detail')
datasets = serializers.HyperlinkedRelatedField(many=True, read_only=True, view_name='dataset-detail', lookup_field='name')
class Meta:
model = DataGroup
fields = ['id', 'name', 'url', 'datasets']
class DataGroupViewSet(viewsets.ModelViewSet):
queryset = DataGroup.objects.all()
serializer_class = DataGroupSerializer
But, my issue is that the Datasets should be filtered for users.
Now, with the Datasets, they get subset with this:
def get_queryset(self):
return self.queryset.for_user(self.request.user)
But, my question is how can I do what with the DataGroupSerializer above?
EDIT: I tried something as suggested below, here:
class DatasetInDataGroupField(serializers.HyperlinkedRelatedField):
def get_queryset(self):
user = self.context['request'].user
queryset = Dataset.objects.for_user(user)
print(list(queryset))
return queryset
class DataGroupSerializer(serializers.HyperlinkedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='datagroup-detail')
datasets = DatasetInDataGroupField(many=True, view_name='dataset-detail', lookup_field='name')
class Meta:
model = DataGroup
fields = ['id', 'name', 'url', 'datasets']
And I get only the datasets that have permissions being printed, but all of them appear in the datasets list... So trying something else.
This is how I solved it in the end. Works pretty well.
class DatasetInGroupSerializer(serializers.HyperlinkedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dataset-detail', lookup_field='name')
class Meta:
model = Dataset
lookup_field = 'name'
fields = [
'name', 'url']
class DataGroupSerializer(serializers.HyperlinkedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='datagroup-detail')
datasets = serializers.SerializerMethodField()
class Meta:
model = DataGroup
fields = ['id', 'name', 'url', 'datasets']
def get_datasets(self, obj):
queryset = obj.datasets.all()
if 'request' in self.context:
queryset = queryset.for_user(self.context['request'].user)
serializer = DatasetInGroupSerializer(queryset, many=True, context=self.context)
return serializer.data
class DataGroupViewSet(viewsets.ModelViewSet):
queryset = DataGroup.objects.all()
serializer_class = DataGroupSerializer
You want something similar to https://medium.com/django-rest-framework/limit-related-data-choices-with-django-rest-framework-c54e96f5815e using HyperlinkedRelatedField instead of PrimaryKeyRelatedField

Categories

Resources