I am building a blog app with React and Django and I am serializing model's instances saved by particular user, First I am just trying to test with .all() then I am planning to filter by specific user But when I serialize queryset with Serializer like:
class BlogSerializerApiView(viewsets.ModelViewSet):
serializer_class = BlogSerializer
def get_queryset(self, *args, **kwargs):
queryset = Blog.objects.all()
output_serializer = BlogSerializer(queryset, many=True)
print(output_serializer.data)
return "Testing"
It is showing in console:
[OrderedDict(), OrderedDict()]
and when I access it like
print(output_serializer)
Then it is showing:
BlogSerializer(<QuerySet [<Blog: user_1 - Blog_title>, <Blog: user_2 - second_blog_title>]>, many=True):
serializer.py:
class BlogSerializer(serializers.Serializer):
class Meta:
model = Blog
fields = ['title']
models.py:
class Blog(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
title = models.CharField(max_length=30, default='')
def __str__(self):
return f"{self.user} - {self.title}"
What I am trying to do:
I am trying to serialize queryset to show on page in react frontend, I will relate with specific user later.
I have tried many times by changing CBV serialization method by generics.ListAPIView instead of viewsets.ModelViewSet but still same thing.
There is a concept error here. The get_queryset function is not supposed to return serialized data. It must return a QuerySet of model objects.
To achieve what you want you can just do:
class BlogSerializerApiView(viewsets.ModelViewSet):
serializer_class = BlogSerializer
def get_queryset(self, *args, **kwargs):
return Blog.objects.all()
The Django Rest Framework will take care of serializing data.
In fact, you can even do it way more simple. Defining the view's queryset field like this:
class BlogSerializerApiView(viewsets.ModelViewSet):
queryset = Blog.objects.all()
serializer_class = BlogSerializer
Additional:
You said you will relate to current user later. You could achieve that in fact in the get_queryset method filtering aginst the user
class BlogSerializerApiView(viewsets.ModelViewSet):
serializer_class = BlogSerializer
def get_queryset(self, *args, **kwargs):
return Blog.objects.filter(user_id=USER_ID)
Hope this helps!
I was using
class BlogSerializer(serializers.Serializer):
.......
so it was showing empty results (no idea why, I think its deprecated)
After replaceing it with
class BlogSerializer(serializers.HyperlinkedModelSerializer):
It worked
Related
Is there any good way that I can make modifications to individual fields of data in a list view? For example, I have a blog post model that looks like
class Blog(models.Model):
create_time = models.DateTimeField(auto_now_add=True)
title = models.CharField(max_length=250)
document = models.TextField(blank=True)
and my list view looks like this:
class BlogPostListView(ListView):
model = Blog
template_name = 'blog/posts.html'
context_object_name = 'posts'
ordering = ['-create_time']
Obviously I want to display a list of blog posts in a table format, and I want to modify how the create_time looks (currently showing 'March 7, 2020, 10:16 a.m.', but I only want 2020-3-7 10:16); and the post document can be too long to display in a cell, I want to truncate it into only 150 words.
so what are the better ways to achieve this in a listview view?
(I read some sort-of similar questions in StackOverflow, but it is either done in the template such as reformating the time which does not solve my second requirement, or too vague to understand).
If you want to customise the representation between model and view. A faster way to do that is to use serializer and RetrieveAPIView of DRF. Check out installation link. Then, you can make the use of serializer to customise representations of model's fields.
Here is pseudo code:
from rest_framework import serializers
from rest_framework.generics import RetrieveAPIView
class BlogSerializer(serializers.ModelSerializer)
def get_create_time(self, blog):
return self.create_time..strftime("%m/%d/%Y, %H:%M)
def get_document(self, blog):
return self.document[:151]
class BlogPostListView(RetrieveAPIView):
queryset = Blog.objects.all()
serializer_class = BlogSerializer
template_name = 'blog/posts.html'
As Chouvic suggested that I should use ModelSerializer, I managed to get it work in the listView. Here is my solution:
serializer:
class BlogListSerializer(serializers.ModelSerializer):
create_time_str = serializers.SerializerMethodField()
short_document_str = serializers.SerializerMethodField()
class Meta:
model = Blog
fields = '__all__'
def get_create_time_str(self, obj):
return obj.create_time.strftime("%Y/%m/%d")
def get_short_document_str(self, obj):
return obj.document[0:150]
as for the ListView:
class BlogListView(ListView):
model = Blog
template_name = 'blog/posts.html'
context_object_name = 'posts'
ordering = ['-create_time']
queryset = Blog.objects.all()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
posts = context['posts']
mod_posts = BlogListSerializer(posts, many=True)
context['posts'] = mod_posts.data
return context
BlogListSerializer(posts, many=True) takes in posts (a list of post) and return a new list of post each with the new 'attributes' of create_time_str and short_document_str.
In the template, I just need to refer it as <td>{{ post.create_time_str }}</td>.
First, I would like to present how my managers, models, serializers and views look like upfront.
class PublishedManager(models.Manager):
"""
Only published articles. `due_date` is past.
"""
def get_queryset(self):
now = timezone.now()
return super().get_queryset().filter(due_date__lt=now)
class UnpublishedManager(models.Manager):
"""
Only unpublished articles. `due_date` is future.
"""
def announced(self):
return self.get_queryset().filter(announced=True)
def get_queryset(self):
now = timezone.now()
return super().get_queryset().filter(due_date__gt=now)
class Article(models.Model):
content = models.TextField()
due_date = models.DateTimeField()
announced = models.BooleanField()
# managers
objects = models.Manager() # standard manager
published = PublishedManager()
unpublished = UnpublishedManager()
class ArticleSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = ("content", "due_date")
class ArticleRUDView(generics.RetrieveUpdateDestroyAPIView):
serializer_class = ArticleSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
queryset = Article.objects.all()
In this code, ArticleRUDView naturally responds with all Article because of Article.objects.all(), yet this is not what I want to do. What I want to do is:
If the user is authenticated, then Article.objects.all().
If the user is anonymous,
If the entry is published (which means its due_date is less than now), then serialize all fields.
If the entry is not published (which means its due_date is greater than now), then still serialize, but content should be null in JSON.
Or, in short, how do I alter the serializer's data in a view?
Troubleshooting
This section might get updated in time. I will elaborate on what I find.
Overriding get_serializer Method from GenericAPIView
So I've found out I can get an instance of ArticleSerializer. So I did below:
def get_serializer(self, *args, **kwargs):
serializer = super().get_serializer()
if self.request.user.is_authenticated:
return serializer
obj = self.get_object() # get_object, hence the name, gets the object
due_date = obj.due_date
now = timezone.now()
if due_date > now:
serializer.data["content"] = None
return serializer
However, my tests didn't go well at all. This, oddly, returns an empty string on content field in JSON. I've tried different things but got that empty string. I do not have any single clue about what to do from here.
Environment
Python 3.7.4
Django 2.2.7
Django Rest Framework 3.10.3
I think you want to use get_serializer_class as opposed to get_serializer. You can allow the serializer class to choose what to stick in content instead of all the mucking around with managers, since you want to serialize all objects anyway. Something like this should work:
class ArticleSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = ("content", "due_date")
class AnonymousArticleSerializer(ArticleSerializer):
content = serializers.SerializerMethodField()
#staticmethod
def get_content(obj):
if obj.due_date > timezone.now():
return None
return obj.content
class ArticleRUDView(generics.RetrieveUpdateDestroyAPIView):
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
queryset = Article.objects.all()
def get_serializer_class(self):
if self.serializer_class:
return self.serializer_class
if self.request.user.is_authenticated:
self.serializer_class = ArticleSerializer
else:
self.serializer_class = AnonymousArticleSerializer
return self.serializer_class
One thing I don't like about this solution is that if you have a more complicated serializer field you're overwriting, you'd have to put the logic somewhere, but in this case (context being a text field) it's pretty simple.
So I can retrieve my data perfectly fine but when I try to post I get
{"detail":"Method \"POST\" not allowed."}
views.py
class ClubFullList(generics.ListAPIView):
serializer_class = ClubSerializer
def get_queryset(self):
return Club.objects.all()
class ClubList(generics.ListAPIView):
serializer_class = ClubSerializer
def get_queryset(self):
username = self.kwargs['username']
return Club.objects.filter(abv=username)
models.py
class Club(models.Model):
name = models.CharField(max_length=255)
abv = models.CharField(max_length=255)
serializers.py
class ClubSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Club
fields = ['name', 'abv']
How can I solve this?
You are sending POST request on an endpoint which only allows GET request.
ListAPIView is a read-only generic view. To create model objects using POST request, use CreateAPIView or ListCreateAPIView.
From the docs for ListApiView:
Used for read-only endpoints to represent a collection of model instances.
If you want to post to your endpoint, you'll need to use a different view class.
I'm using the Django Rest Framework and I'd like to be able to add extra detail to the serializer when a single object is returned, which would be left out of the list view.
In the code below I add the celery_state field to the TestModelSerializer, but I'd only like this field to be added when its returning a single object, not when it's returning the list of TestModel data.
I've looked at the list_serializer_class option but it seems to just use the original model serializer so it will always still include the field even if I try to exclude from there.
What are my options?
class TestModelSerializer(serializers.HyperlinkedModelSerializer):
celery_state = serializers.CharField(source='celery_state', read_only=True)
class Meta:
model = TestModel
class TestModelViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows TestModels to be viewed or edited.
"""
authentication_classes = (SessionAuthentication, BasicAuthentication)
permission_classes = (IsAuthenticatedOrReadOnly,)
queryset = TestModel.objects.all()
serializer_class = TestModelSerializer
Since the serializer class (used by the viewsets) passes many argument, you can use that to control the fields output:
class TestModelSerializer(serializers.HyperlinkedModelSerializer):
# ...
def __init__(self, *args, **kwargs):
super(TestModelSerializer, self).__init__(*args, **kwargs)
if kwargs.get('many', False):
self.fields.pop('celery_state')
Inspired by #mariodev answer:
The other possibility is to override many_init static method in serializer. Acording comments in thie code (https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/serializers.py#L128 ) it is suggested variant.
from rest_framework import serializers
class ExtendedSerializer(serializers.Serializer):
...
#classmethod
def many_init(cls, *args, **kwargs):
kwargs['child'] = cls()
kwargs['child'].fields.pop('extractedFiled')
return serializers.ListSerializer(*args, **kwargs)
You can have an extra serializer called ExtendedTestModelSerializer which would contain the extra fields that you want.
After that, you can use the get_serializer_class method to decide which serializer is used based on request.action -
class TestModelViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows TestModels to be viewed or edited.
"""
authentication_classes = (SessionAuthentication, BasicAuthentication)
permission_classes = (IsAuthenticatedOrReadOnly,)
queryset = TestModel.objects.all()
# serializer_class = TestModelSerializer
get_serializer_class(self):
if self.request.action == 'list':
return TestModelSerializer
return ExtendedTestModelSerializer
I have the following models:
class User(models.Model):
name = models.Charfield()
email = models.EmailField()
class Friendship(models.Model):
from_friend = models.ForeignKey(User)
to_friend = models.ForeignKey(User)
And those models are used in the following view and serializer:
class GetAllUsers(generics.ListAPIView):
authentication_classes = (SessionAuthentication, TokenAuthentication)
permission_classes = (permissions.IsAuthenticated,)
serializer_class = GetAllUsersSerializer
model = User
def get_queryset(self):
return User.objects.all()
class GetAllUsersSerializer(serializers.ModelSerializer):
is_friend_already = serializers.SerializerMethodField('get_is_friend_already')
class Meta:
model = User
fields = ('id', 'name', 'email', 'is_friend_already',)
def get_is_friend_already(self, obj):
request = self.context.get('request', None)
if request.user != obj and Friendship.objects.filter(from_friend = user):
return True
else:
return False
So basically, for each user returned by the GetAllUsers view, I want to print out whether the user is a friend with the requester (actually I should check both from_ and to_friend, but does not matter for the question in point)
What I see is that for N users in database, there is 1 query for getting all N users, and then 1xN queries in the serializer's get_is_friend_already
Is there a way to avoid this in the rest-framework way? Maybe something like passing a select_related included query to the serializer that has the relevant Friendship rows?
Django REST Framework cannot automatically optimize queries for you, in the same way that Django itself won't. There are places you can look at for tips, including the Django documentation. It has been mentioned that Django REST Framework should automatically, though there are some challenges associated with that.
This question is very specific to your case, where you are using a custom SerializerMethodField that makes a request for each object that is returned. Because you are making a new request (using the Friends.objects manager), it is very difficult to optimize the query.
You can make the problem better though, by not creating a new queryset and instead getting the friend count from other places. This will require a backwards relation to be created on the Friendship model, most likely through the related_name parameter on the field, so you can prefetch all of the Friendship objects. But this is only useful if you need the full objects, and not just a count of the objects.
This would result in a view and serializer similar to the following:
class Friendship(models.Model):
from_friend = models.ForeignKey(User, related_name="friends")
to_friend = models.ForeignKey(User)
class GetAllUsers(generics.ListAPIView):
...
def get_queryset(self):
return User.objects.all().prefetch_related("friends")
class GetAllUsersSerializer(serializers.ModelSerializer):
...
def get_is_friend_already(self, obj):
request = self.context.get('request', None)
friends = set(friend.from_friend_id for friend in obj.friends)
if request.user != obj and request.user.id in friends:
return True
else:
return False
If you just need a count of the objects (similar to using queryset.count() or queryset.exists()), you can include annotate the rows in the queryset with the counts of reverse relationships. This would be done in your get_queryset method, by adding .annotate(friends_count=Count("friends")) to the end (if the related_name was friends), which will set the friends_count attribute on each object to the number of friends.
This would result in a view and serializer similar to the following:
class Friendship(models.Model):
from_friend = models.ForeignKey(User, related_name="friends")
to_friend = models.ForeignKey(User)
class GetAllUsers(generics.ListAPIView):
...
def get_queryset(self):
from django.db.models import Count
return User.objects.all().annotate(friends_count=Count("friends"))
class GetAllUsersSerializer(serializers.ModelSerializer):
...
def get_is_friend_already(self, obj):
request = self.context.get('request', None)
if request.user != obj and obj.friends_count > 0:
return True
else:
return False
Both of these solutions will avoid N+1 queries, but the one you pick depends on what you are trying to achieve.
Described N+1 problem is a number one issue during Django REST Framework performance optimization, so from various opinions, it requires more solid approach then direct prefetch_related() or select_related() in get_queryset() view method.
Based on collected information, here's a robust solution that eliminates N+1 (using OP's code as an example). It's based on decorators and slightly less coupled for larger applications.
Serializer:
class GetAllUsersSerializer(serializers.ModelSerializer):
friends = FriendSerializer(read_only=True, many=True)
# ...
#staticmethod
def setup_eager_loading(queryset):
queryset = queryset.prefetch_related("friends")
return queryset
Here we use static class method to build the specific queryset.
Decorator:
def setup_eager_loading(get_queryset):
def decorator(self):
queryset = get_queryset(self)
queryset = self.get_serializer_class().setup_eager_loading(queryset)
return queryset
return decorator
This function modifies returned queryset in order to fetch related records for a model as defined in setup_eager_loading serializer method.
View:
class GetAllUsers(generics.ListAPIView):
serializer_class = GetAllUsersSerializer
#setup_eager_loading
def get_queryset(self):
return User.objects.all()
This pattern may look like an overkill, but it's certainly more DRY and has advantage over direct queryset modification inside views, as it allows more control over related entities and eliminates unnecessary nesting of related objects.
Using this metaclass DRF optimize ModelViewSet MetaClass
from django.utils import six
#six.add_metaclass(OptimizeRelatedModelViewSetMetaclass)
class MyModelViewSet(viewsets.ModelViewSet):
queryset = MyModel.objects.all()
serializer_class = MyModelSerializer
You can split the view into two query.
First, only get the Users list (without is_friend_already field). This only require one query.
Second, get the friends list of request.user.
Third, modify the results depending on if the user is in the request.user's friend list.
class GetAllUsersSerializer(serializers.ModelSerializer):
...
class UserListView(ListView):
def get(self, request):
friends = request.user.friends
data = []
for user in self.get_queryset():
user_data = GetAllUsersSerializer(user).data
if user in friends:
user_data['is_friend_already'] = True
else:
user_data['is_friend_already'] = False
data.append(user_data)
return Response(status=200, data=data)