DRF: values() does not return groupby object - django

Suppose I have:
# models.py
class Project(models.Model):
project = models.CharField(max_length=200)
subproject = models.CharField(max_length=200)
physical_pct = models.FloatField()
cost = models.FloatField()
# serializers.py
class ProjectSerializer(serializers.ModelSerializer):
class Meta:
model = Project
fields = '__all__'
In my viewset, I want to display a grouped by object by name that will later be annotated. I referred to this example from values.
# views.py
class ProjectsViewSet(viewsets.ModelViewSet):
serializer_class = ProjectSerializer
def get_queryset(self):
queryset = Project.objects.values('project')
print(queryset)
return queryset
When I print queryset it displays a list of all project without the other fields in the terminal. However it raises an error:
"Got KeyError when attempting to get a value for field `subproject` on serializer `ProjectSerializer`.\nThe serializer field might be named incorrectly and not match any attribute or key on the `dict` instance.\nOriginal exception text was: 'subproject'."
My desired output is a json grouped by the project field.
UPDATE 1:
It will not have an error if I put all fields in the values() arguments i.e.
.values('project', 'subproject', 'physical_pct', 'cost',)
Which now then destroy the purpose of values being grouped by.

Main cause of the problem is for your serializer defination. If you only want to return back project field response make sure you are using appropriate serializer for that.
class ProjectSerializer(serializers.ModelSerializer):
class Meta:
model = Project
fields = '__all__' // this means you are suppose to pass all model fields
So we need custom serializer for this purpose
class ProjectListSerializer(serializers.ModelSerializer):
class Meta:
model = Project
fields = ('project',)
And also we need to update get_serializer method according to our necessary need
# views.py
class ProjectsViewSet(viewsets.ModelViewSet):
def get_queryset(self):
// If you are doing so you are suppose to have only one 'project' field response
queryset = Project.objects.values('project')
return queryset
def get_serializer_class(self):
if self.action == 'list' or self.action == 'retrive':
return ProjectListSerializer
return ProjectSerializer

Try:
queryset = Project.objects.all().values('project')

You are querying only project field in Project model but trying to serialize all field in Project model. If you want to serialize one field, you don't need the serializer:
class ProjectsViewSet(viewsets.ModelViewSet):
serializer_class = ProjectSerializer
queryset = Project.objects.all()
def list(self, request, *args, **kwargs):
qs = self.get_queryset().values_list('project', flat=True)
return Response(qs)

Related

Django Rest Framework filtering against serializer method fields

In my serializers, I have added the custom field "step_type" that grabs a value from another model.
class AccountSerializer(serializers.ModelSerializer):
step_type= serializers.SerializerMethodField()
class Meta:
model = Account
fields = '__all__'
def get_step_type(self, obj):
step = Step.objects.get(step_name=obj.step_name)
return step.step_type
I want to use query parameters to filter my REST API
class AccountViewSet(viewsets.ModelViewSet):
def get_queryset(self):
queryset = Account.objects.all().order_by('-date')
query_step_type = self.request.query_params.get("type")
if query_step_type is not None:
queryset = queryset.filter(step_type=query_step_type)
return queryset
However, this won't work because step_type isn't part of the original model fields. How can I filter the queryset using the step type serializer method field?

Django Rest Framework Limiting get_queryset result to not include all fields

view.py
class charity_totals(generics.ListAPIView):
serializer_class= CharityTotalSerializer
queryset=Transaction.objects.all()
def get_queryset(self):
queryset = super().get_queryset()
user_id = self.request.GET.get('userID')
if user_id is None:
return queryset
queryset = queryset.filter(userID=user_id)
return queryset.values('charityID').annotate(total_donation=Sum('transactionAmount'))
serializer.py
class CharityTotalSerializer(ModelSerializer):
charity_name= serializer.ReadOnlyField(source='charityID.charityName')
total_donation= serializer.DecimalField(max_digits=64,decimal_places=2)
class Meta:
model = Transaction
fields = ['charity_name','total_donation']
model
class Transaction(models.Model):
transactionAmount = models.DecimalField(max_digits=6, decimal_places=2)
userID = models.ForeignKey(User,on_delete=models.CASCADE)
charityID = models.ForeignKey(Charity,on_delete=models.CASCADE, related_name='charity_set')
processed = models.BooleanField(auto_created=True, default=False)
transactionDate = models.DateField(auto_now_add=True)
Off of a request such as this http://localhost:8000/requests/charitytotal/?userID=1 my json response is limited to just the [{"total_donation":"3.00"},{"total_donation":"17.00"}] and is not including the charity names that are specified in the serializer. From what I understand the .values should return a dict of both the charityID and the total_donation that was specified which should be able to interact with my serializer. Any Insight would be appreciated
That is because you are returning Values queryset from get_queryset method when you are getting the value of userID in request.GET. Also I assume it is important for you return that way so that you can group by and sum values of total donation. So, I think you can approach something like this:
First, change get_queryset method to return the name of the charity annotated:
from django.db.models import F, Sum
...
def get_queryset(self):
queryset = super().get_queryset()
user_id = self.request.GET.get('userID')
if user_id is not None:
queryset = queryset.filter(userID=user_id)
return queryset.values('charityID').annotate(total_donation=Sum('transactionAmount')).annotate(charity_name=F('charityID__charityName'))
Then update the serializer like this:
class CharityTotalSerializer(ModelSerializer):
charity_name= serializer.ReadOnlyField() # no need to define source
total_donation= serializer.DecimalField(max_digits=64,decimal_places=2)
class Meta:
model = Transaction
fields = ['charity_name','total_donation']
Also, better to have charityName unique(using unique=True in models), so that it does not produce confusing results.
You have to put it into Serializer and it will work fine.
charity_name = serializer.SerializerMethodField()
def get_charity_name(self, instance):
return instance.charityID.name

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")

Annotated django queryset is getting ignored during serialization

I have a ModelViewSet where I want to annotate the list() response. I've extended the queryset with an annotation and added the field to the serializer, but the serializer just ignores the new data and doesn't add the field at all in the final data.
I am using a customized get_queryset() too (show abbreviated here) which is definitely getting called and producing the right annotations. It just doesn't show up in the REST API response.
If I set default=None on the serializer field definition, it appears in the response.
class SequenceSerializer(serializers.ModelSerializer):
unread=serializers.IntegerField(read_only=True)
.....
class SequenceViewSet(viewsets.ModelViewSet,ScopedProtectedResourceView):
authentication_classes = [OAuth2Authentication]
queryset = Sequence.objects.all()
serializer_class = SequenceSerializer
.....
def get_queryset(self):
queryset = Sequence.objects.all().filter(<..... some filter>)
queryset = queryset.annotate(unread=FilteredRelation('unreadseq',
condition=Q(unreadseq__userid=self.request.user)))
print("Seq with unread",queryset.values('id','unread')) ## <<<<this shows the correct data
return queryset
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data) ##<<< this is missing the annotation
I have been banging my head against this all day and I can't for the life of me see what's going wrong.
Any ideas please?
--
more info:
class UnreadSeq(models.Model):
userid = models.ForeignKey('auth.User', on_delete=models.CASCADE)
seqid = models.ForeignKey(Sequence, on_delete=models.CASCADE)
class Meta:
unique_together=('seqid','userid')
verbose_name = "UnreadSeq"
verbose_name_plural = "UnreadSeqs"
class Sequence(models.Model):
userid = models.ForeignKey('auth.User', on_delete=models.SET_NULL,null=True)
topic = models.ForeignKey('Topic',on_delete=models.CASCADE,null=False,blank=False)
.....
class Meta:
verbose_name = "Sequence"
verbose_name_plural = "Sequences"
I think that this annotation don't return Integer. Try to annotate (you want to COUNT unreadseq) like this:
def get_queryset(self):
mytopics=getMyTopics(self.request,False)
queryset = Sequence.objects.all().filter(<..... some filter>)
count_unreadseq = Count('unreadseq', filter=Q(unreadseq__userid=self.request.user))
queryset=queryset.annotate(unread=count_unreadseq)
...
EDITED after comments to get unreadseq ids
def get_queryset(self):
mytopics=getMyTopics(self.request,False)
queryset = Sequence.objects.all().filter(<..... some filter>)
unreadseq_ids = UnreadSeq.objects.filter(seqid=OuterRef('pk'), userid=self.request.user).values('pk')
queryset=queryset.annotate(unread=Subquery(unreadseq_ids))
...
Also you need to edit serializer:
class SequenceSerializer(serializers.ModelSerializer):
unread=serializers.IntegerField(read_only=True)
.....

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.