5

I need to count the number of children an object has and return this value in my API via the object serializer. I also need to count a subset of these children objects.

I have a Task object with children Asignees. In my API when I query the tasks I want to have the following data set returned:

[
    { label: "Cross the bridge",
      count_assigned: 5,
      count_completed: 3 },
    { label: "Build a fire",
      count_assigned: 5,
      count_completed: 2 }
]

How would I do this? I have found the .annotate() method but that result is not available in the serializer class.

models.py

class Task(models.Model):
    label         = models.CharField(max_length=255,null=False,blank=False)

class Assignee(models.model):
    task         = models.ForeignKey(Task, related_name='assignees', on_delete=models.CASCADE, blank=True, null=True) 
    person       = models.ForeignKey(Person, on_delete=models.CASCADE, blank=True, null=True)
    completed    = models.DateTimeField(null=True,blank=True)

serializers.py

from rest_framework import serializers

from .models import Task, Assignee
from people.serializers import PersonSerializer

class AssigneeSerializer(serializers.ModelSerializer):
    id = serializers.ReadOnlyField()
    person = PersonSerializer(read_only=True)

    class Meta:
        model = Assignee

        fields = ('id','task','person','completed')
        read_only_fields = ['id']


class TaskSerializer(serializers.ModelSerializer):
    id = serializers.ReadOnlyField()

    class Meta:
        model = Task

        fields = ('id', 'label')
        read_only_fields = ['id']
Maxim
  • 52,561
  • 27
  • 155
  • 209
Maverik Minett
  • 2,131
  • 3
  • 17
  • 28
  • What does your API view look like? Are you using a viewer? If so [this answer](https://stackoverflow.com/a/36697562/104349) (note, not the accepted answer on that question) will do what you want. – Daniel Roseman Sep 20 '17 at 15:45

2 Answers2

9

The proposed way

class TaskSerializer(serializers.ModelSerializer):
     id = serializers.ReadOnlyField()
     count_assigned = serializers.SerializerMethodField()
     count_completed = serializers.SerializerMethodField()

     class Meta:
         model = Task
         fields = ('id', 'label', 'count_assigned', 'count_completed')

    def get_count_assigned(self, obj):
        return obj.assignees.count()

    def get_count_completed(self, obj):
        return obj.assignees.exclude(completed__isnull=True).count()

http://www.django-rest-framework.org/api-guide/fields/#serializermethodfield

Iyvin Jose
  • 707
  • 5
  • 17
  • Ok, this worked great but raised another issue. I get the data in the format specified. Now I want to modify this javascript object on the client said, e.g. change the label, and send the same javascript object back to update the database and I get an error: `django.core.exceptions.FieldDoesNotExist: Task has no field named 'count_assigned'` – Maverik Minett Sep 21 '17 at 01:29
  • The fields(“count_assigned”, “count_completed”) are read-only fields. That means they are meant to be used only for generating serializer data representation. They can’t be used for modifying the database record. It would be wise to remove these fields from API parameters in javascript before trying to update. – Iyvin Jose Sep 21 '17 at 01:40
  • However here in case of list action the obj.assignees.count() will be called for each Task which result in N+1 queries. I'd rather suggest @brown-bear's solution which makes 1 query with all counts embedded. – Martin Faucheux Feb 04 '21 at 08:44
3

If i understand your logic correctly, you can try

in serializers

class TaskSerializer(serializers.ModelSerializer):
    count_assigned = serializers.IntegerField(read_only=True)
    count_completed = serializers.IntegerField(read_only=True)

then by queryset:

from django.db.models import Count, Case, When, IntegerField

qs = Task.objects.annotate(
        count_completed=Count(Case(
            When(assignees__completed__isnull=False, then=1),
            output_field=IntegerField(),
        ))
    ).annotate(count_assigned=Count('assignees'))

serializer = TaskSerializer(qs, many=True)

Or horribly inefficient in models:

from django.utils.functional import cached_property

class Task(models.Model):

@cached_property
def all_assignee(self):
    return self.assignees.all()

def count_assigned(self):
    return self.all_assignee.count()

def count_completed(self):
    return self.all_assignee.filter(completed__isnull=False).count()
Brown Bear
  • 19,655
  • 10
  • 58
  • 76