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)
.....
Related
I need to create a DRF list view that shows each course along with a boolean field signifying whether the user requesting the view is subscribed to the course.
Course subscriptions are stored in the following model:
class Subscription(models.Model):
user = models.ForeignKey(
CustomUser, related_name='subscriptions', null=False,
on_delete=CASCADE)
course = models.ForeignKey(
Course, related_name='subscriptions', null=False,
on_delete=CASCADE)
class Meta:
ordering = ['course', 'user']
unique_together = [['user', 'course']]
This is the view I am writing:
class CourseListView(generics.ListAPIView):
permission_classes = [IsAuthenticated, ]
queryset = Course.objects.all()
serializer_class = CourseSerializer
def isSubscribed(self, request, course):
sub = Subscription.objects.filter(
user=request.user, course=course).first()
return True if sub else False
def list(self, request, format=None):
queryset = Course.objects.all()
serializer = CourseSerializer(queryset, many=True)
return Response(serializer.data)
I am looking for a way to modify the list method, so as to add to the response the information about whether request.user is subscribed to each of the courses.
The best solution I have for now is to construct serializer manually, which would look (at the level of pseudo-code) something like this:
serializer = []
for course in querySet:
course['sub'] = self.isSubscribed(request, course)
serializer.append(CourseSerializer(course))
I suspect there should be a better (standard, idiomatic, less convoluted) way for adding a custom field in a list view, but could not find it. In addition, I am wondering whether it is possible to avoid a database hit for every course.
You can do that easily with Exists:
just change your queryset in your view:
from django.db.models import Exists, OuterRef
class CourseListView(generics.ListAPIView):
permission_classes = [IsAuthenticated, ]
serializer_class = CourseSerializer
def get_queryset(self):
subquery = Subscription.objects.filter(user=request.user, course=OuterRef('id'))
return Course.objects.annotate(sub=Exists(subquery))
and add a field for it in your serializer:
class CourseSerializer(serializers.ModelSerializer):
sub = serializers.BooleanField(read_only=True)
class Meta:
model = Course
fields = '__all__'
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
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)
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.
In Django REST Framework API, list of database table records are not getting updated until the API restart or any code change in python files like model, serializer or view. I've tried the transaction commit but it didn't worked. Below is my view :
class ServiceViewSet(viewsets.ModelViewSet):
#authentication_classes = APIAuthentication,
queryset = Service.objects.all()
serializer_class = ServiceSerializer
def get_queryset(self):
queryset = self.queryset
parent_id = self.request.QUERY_PARAMS.get('parent_id', None)
if parent_id is not None:
queryset = queryset.filter(parent_id=parent_id)
return queryset
# Make Service readable only
def update(self, request, *args, **kwargs):
return Response(status=status.HTTP_400_BAD_REQUEST)
def destroy(self, request, *args, **kwargs):
return Response(status=status.HTTP_400_BAD_REQUEST)
Serializer looks like this :
class ServiceSerializer(serializers.ModelSerializer):
class Meta:
model = Service
fields = ('id', 'category_name', 'parent_id')
read_only_fields = ('category_name', 'parent_id')
and model looks like this :
class Service(models.Model):
class Meta:
db_table = 'service_category'
app_label = 'api'
category_name = models.CharField(max_length=100)
parent_id = models.IntegerField(default=0)
def __unicode__(self):
return '{"id":%d,"category_name":"%s"}' %(self.id,self.category_name)
This problem is occuring only with this service, rest of the APIs working perfectly fine. Any help will be appreciated.
Because you are setting up the queryset on self.queryset, which is a class attribute, it is being cached. This is why you are not getting an updated queryset for each request, and it's also why Django REST Framework calls .all() on querysets in the default get_queryset. By calling .all() on the queryset, it will no longer use the cached results and will force a new evaluation, which is what you are looking for.
class ServiceViewSet(viewsets.ModelViewSet):
queryset = Service.objects.all()
def get_queryset(self):
queryset = self.queryset.all()
parent_id = self.request.QUERY_PARAMS.get('parent_id', None)
if parent_id is not None:
queryset = queryset.filter(parent_id=parent_id)
return queryset