Django custom queryset inside ModelSerializer - django

I have a PostSerializer that has a comments field which use CommentSerializer. I want to change the queryset of this CommentSerializer so that it won't show all comments at once. Here's the code
class PostSerializer(serializers.ModelSerializer):
comments = SimplifiedCommentSerializer(
many=True,
required=False,
)
class Meta:
model = Post
fields = ('comments')
class SimplifiedCommentSerializer(serializers.ModelSerializer):
content = serializers.TextField()
# this function doesn't seem to work
def get_queryset(self):
return Comment.objects.all()[:10]
class Meta:
model = Comment
fields = ('content')
I've tried using get_queryset inside the SimplifiedCommentSerializer, but I still get all the comments instead of the first 10.

Try to change this:
def get_queryset(self):
return Comment.objects.all()[:10]
into:
queryset = Comment.objects.all()[:10]
EDIT:
Create a viewset and outsource the line above:
class CommentViewSet(viewsets.ModelViewSet):
queryset = Comment.objects.all()[:10]
serializer_class = SimplifiedCommentSerializer
Please see this question and answer:
django REST framework - limited queryset for nested ModelSerializer?

Related

Django DRF add request.user to modelserializer

I am using django rest framework, and I have an object being created via a modelviewset, and a modelserializer. This view is only accessible by authenticated users, and the object should set its 'uploaded_by' field, to be that user.
I've read the docs, and come to the conclusion that this should work
viewset:
class FooViewset(viewsets.ModelViewSet):
permission_classes = [permissions.IsAdminUser]
queryset = Foo.objects.all()
serializer_class = FooSerializer
def get_serializer_context(self):
return {"request": self.request}
serializer:
class FooSerializer(serializers.ModelSerializer):
uploaded_by = serializers.PrimaryKeyRelatedField(
read_only=True, default=serializers.CurrentUserDefault()
)
class Meta:
model = Foo
fields = "__all__"
However, this results in the following error:
django.db.utils.IntegrityError: NOT NULL constraint failed: bar_foo.uploaded_by_id
Which suggests that "uploaded_by" is not being filled by the serializer.
Based on my understanding of the docs, this should have added the field to the validated data from the serializer, as part of the create method.
Clearly I've misunderstood something!
The problem lies in the read_only attribute on your uploaded_by field:
Read-only fields are included in the API output, but should not be
included in the input during create or update operations. Any
'read_only' fields that are incorrectly included in the serializer
input will be ignored.
Set this to True to ensure that the field is used when serializing a
representation, but is not used when creating or updating an instance
during deserialization.
Source
Basically it's used for showing representation of an object, but is excluded in any update and create-process.
Instead, you can override the create function to store the desired user by manually assigning it.
class FooSerializer(serializers.ModelSerializer):
uploaded_by = serializers.PrimaryKeyRelatedField(read_only=True)
def create(self, validated_data):
foo = Foo.objects.create(
uploaded_by=self.context['request'].user,
**validated_data
)
return foo
DRF tutorial recommend to override perform_create method in this case and then edit serializer so, that it reflect to new field
from rest_framework import generics, serializers
from .models import Post
class PostSerializer(serializers.HyperlinkedModelSerializer):
author = serializers.ReadOnlyField(source='author.username')
class Meta:
model = models.Post
fields = ['title', 'content', 'author']
class ListPost(generics.ListCreateAPIView):
queryset = Post.objects.all()
serializer_class = PostSerializer
def perform_create(self, serializer):
return serializer.save(author=self.request.user)
Cleaner way:
class PostCreateAPIView(CreateAPIView, GenericAPIView):
queryset = Post.objects.all()
serializer_class = PostCreationSerializer
def perform_create(self, serializer):
return serializer.save(author=self.request.user)
class PostCreationSerializer(serializers.ModelSerializer):
author = serializers.PrimaryKeyRelatedField(read_only=True)
class Meta:
model = Post
fields = ("content", "author")

Filter serializer field's queryset based on current instance

In a HyperlinkedModelSerializer like the one below, how can I filter the set of related instances for the other_model_objects field based on the "current" instance of the model? (Maybe by using the queryset parameter in some way?)
class MyModelSerializer(serializers.HyperlinkedModelSerializer):
other_model_objects = serializers.HyperlinkedRelatedField(
many=True,
queryset=OtherModel.objects.filter(foo=current_instance.field),
view_name='othermodel-detail'
)
class Meta:
model = MyModel
fields = ('other_model_objects',)
Have you tried subclassing HyperlinkedModelSerializer and override get_queryset as stated in the docs: CustomRelationalFields ?
Something like:
class MyFilteredHyperlinkedRelatedField(serializers.HyperlinkedRelatedField):
def get_queryset(self):
queryset = super().get_queryset()
# ... filter queryset
return queryset
class MyModelSerializer(serializers.HyperlinkedModelSerializer):
other_model_objects = MyHyperlinkedRelatedField(
many=True,
queryset=OtherModel.objects.all(),
view_name='othermodel-detail'
)
class Meta:
model = MyModel
fields = ('other_model_objects',)
I think you can access MyModelSerializer instance in get_queryset using self.parent, however I am not sure that is documented/stable.
EDIT
queryset parameter (and get_queryset method) are only used for validation during creations or updates. It is not about retrieving the values of the field, it is just about the allowed values for that field.
If you want to filter the values based on the instance I could suggest to update your model with a method returning the filtered values you would like:
class MyModel:
...
def filtered_other_model(self):
return self.other_model_relation_name.filter(....)
and then use the source parameter:
class MyModelSerializer(serializers.HyperlinkedModelSerializer):
other_model_objects = serializers.HyperlinkedRelatedField(
many=True,
queryset=OtherModel.objects.all(),
view_name='othermodel-detail',
source = 'filtered_other_model'
)
class Meta:
model = MyModel
fields = ('other_model_objects',)

Aggregate (and other annotated) fields in Django Rest Framework serializers

I am trying to figure out the best way to add annotated fields, such as any aggregated (calculated) fields to DRF (Model)Serializers. My use case is simply a situation where an endpoint returns fields that are NOT stored in a database but calculated from a database.
Let's look at the following example:
models.py
class IceCreamCompany(models.Model):
name = models.CharField(primary_key = True, max_length = 255)
class IceCreamTruck(models.Model):
company = models.ForeignKey('IceCreamCompany', related_name='trucks')
capacity = models.IntegerField()
serializers.py
class IceCreamCompanySerializer(serializers.ModelSerializer):
class Meta:
model = IceCreamCompany
desired JSON output:
[
{
"name": "Pete's Ice Cream",
"total_trucks": 20,
"total_capacity": 4000
},
...
]
I have a couple solutions that work, but each have some issues.
Option 1: add getters to model and use SerializerMethodFields
models.py
class IceCreamCompany(models.Model):
name = models.CharField(primary_key=True, max_length=255)
def get_total_trucks(self):
return self.trucks.count()
def get_total_capacity(self):
return self.trucks.aggregate(Sum('capacity'))['capacity__sum']
serializers.py
class IceCreamCompanySerializer(serializers.ModelSerializer):
def get_total_trucks(self, obj):
return obj.get_total_trucks
def get_total_capacity(self, obj):
return obj.get_total_capacity
total_trucks = SerializerMethodField()
total_capacity = SerializerMethodField()
class Meta:
model = IceCreamCompany
fields = ('name', 'total_trucks', 'total_capacity')
The above code can perhaps be refactored a bit, but it won't change the fact that this option will perform 2 extra SQL queries per IceCreamCompany which is not very efficient.
Option 2: annotate in ViewSet.get_queryset
models.py as originally described.
views.py
class IceCreamCompanyViewSet(viewsets.ModelViewSet):
queryset = IceCreamCompany.objects.all()
serializer_class = IceCreamCompanySerializer
def get_queryset(self):
return IceCreamCompany.objects.annotate(
total_trucks = Count('trucks'),
total_capacity = Sum('trucks__capacity')
)
This will get the aggregated fields in a single SQL query but I'm not sure how I would add them to the Serializer as DRF doesn't magically know that I've annotated these fields in the QuerySet. If I add total_trucks and total_capacity to the serializer, it will throw an error about these fields not being present on the Model.
Option 2 can be made work without a serializer by using a View but if the model contains a lot of fields, and only some are required to be in the JSON, it would be a somewhat ugly hack to build the endpoint without a serializer.
Possible solution:
views.py
class IceCreamCompanyViewSet(viewsets.ModelViewSet):
queryset = IceCreamCompany.objects.all()
serializer_class = IceCreamCompanySerializer
def get_queryset(self):
return IceCreamCompany.objects.annotate(
total_trucks=Count('trucks'),
total_capacity=Sum('trucks__capacity')
)
serializers.py
class IceCreamCompanySerializer(serializers.ModelSerializer):
total_trucks = serializers.IntegerField()
total_capacity = serializers.IntegerField()
class Meta:
model = IceCreamCompany
fields = ('name', 'total_trucks', 'total_capacity')
By using Serializer fields I got a small example to work. The fields must be declared as the serializer's class attributes so DRF won't throw an error about them not existing in the IceCreamCompany model.
I made a slight simplification of elnygreen's answer by annotating the queryset when I defined it. Then I don't need to override get_queryset().
# views.py
class IceCreamCompanyViewSet(viewsets.ModelViewSet):
queryset = IceCreamCompany.objects.annotate(
total_trucks=Count('trucks'),
total_capacity=Sum('trucks__capacity'))
serializer_class = IceCreamCompanySerializer
# serializers.py
class IceCreamCompanySerializer(serializers.ModelSerializer):
total_trucks = serializers.IntegerField()
total_capacity = serializers.IntegerField()
class Meta:
model = IceCreamCompany
fields = ('name', 'total_trucks', 'total_capacity')
As elnygreen said, the fields must be declared as the serializer's class attributes to avoid an error about them not existing in the IceCreamCompany model.
You can hack the ModelSerializer constructor to modify the queryset it's passed by a view or viewset.
class IceCreamCompanySerializer(serializers.ModelSerializer):
total_trucks = serializers.IntegerField(readonly=True)
total_capacity = serializers.IntegerField(readonly=True)
class Meta:
model = IceCreamCompany
fields = ('name', 'total_trucks', 'total_capacity')
def __new__(cls, *args, **kwargs):
if args and isinstance(args[0], QuerySet):
queryset = cls._build_queryset(args[0])
args = (queryset, ) + args[1:]
return super().__new__(cls, *args, **kwargs)
#classmethod
def _build_queryset(cls, queryset):
# modify the queryset here
return queryset.annotate(
total_trucks=...,
total_capacity=...,
)
There is no significance in the name _build_queryset (it's not overriding anything), it just allows us to keep the bloat out of the constructor.

Capture parameters in django-rest-framework

suppose this url:
http://localhost:8000/articles/1111/comments/
i'd like to get all comments for a given article (here the 1111).
This is how i capture this url:
url(r'^articles/(?P<uid>[-\w]+)/comments/$', comments_views.CommentList.as_view()),
The related view looks like to:
class CommentList(generics.ListAPIView):
serializer_class = CommentSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
lookup_field = "uid"
def get_queryset(self):
comments = Comment.objects.filter(article= ???)
return comments
For information, the related serializer
class CommentSerializer(serializers.ModelSerializer):
owner = UserSerializer()
class Meta:
model = Comment
fields = ('id', 'content', 'owner', 'created_at')
As you can see, I've updated my get_queryset to filter comments on the article but I don't know how to catch the "uid" parameter.
With an url ending with ?uid=value, i can use self.request.QUERY_PARAMS.get('uid') but in my case, I don't know how to do it.
Any idea?
The url parameter is stored in self.kwargs. lookup_field is the field (defaults to pk) the generic view uses inside the ORM when looking up individual model instances, lookup_url_kwarg is probably the property you want.
So try the following:
class CommentList(generics.ListAPIView):
serializer_class = CommentSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
lookup_url_kwarg = "uid"
def get_queryset(self):
uid = self.kwargs.get(self.lookup_url_kwarg)
comments = Comment.objects.filter(article=uid)
return comments

Django: running a method on all queryset objects

I want to know if the following is possible and if someone could explain how. I'm using Django REST Framework
I have a model, in that model I have a class called Product. Product has method called is_product_safe_for_user. It requires the user object and the self (product).
model.py
class Product(models.Model):
title = models.CharField(max_length=60, help_text="Title of the product.")
for_age = models.CharField(max_length=2,)
def is_product_safe_for_user(self, user):
if self.for_age > user.age
return "OK"
(ignore the syntax above, its just to give you an idea)
What I want to do is run the method for to all of the queryset objects, something like below, but I don't know how...
class ProductListWithAge(generics.ListAPIView):
permission_classes = (permissions.IsAuthenticated,)
model = Product
serializer_class = ProductSerializer
def get_queryset(self):
Product.is_product_safe_for_user(self,user)
# then somehow apply this to my queryset
return Product.objects.filter()
there will also be times when I want to run the methoud on just one object.
Or should it go into the Serializer? if so how?...
class ProductSerializer(serializers.ModelSerializer):
safe = serializers.Field(Product='is_product_safe_for_user(self,user)')
class Meta:
model = Product
fields = ('id', 'title', 'active', 'safe')
You could write a custom manager for your model. Something like this:
class OnlySafeObjects(models.Manager):
def filter_by_user(self, user):
return super(OnlySafeObjects, self).get_query_set().filter(for_age__gte=user.age)
class Product(models.Model):
# your normal stuff
onlysafeobjects = OnlySafeObjects()
Then you would use it like this:
safe_products = Product.onlysafeobjects.filter_by_user(request.user)