Search code examples
djangoormsubquery

Django round in subquery


hi i am getting the progress percentage of each project in a ListView with subquery with the following code

class projects(LoginRequiredMixin, ListView):
    model = Project
    template_name = 'project_list.html'
    ordering = ['project_title']
    paginate_by = 10
    queryset = Project.objects.annotate(
        todo_done=Count('todo', filter=Q(todo__state=True)) * 100 / Count('todo'),
        todo_left=Count('todo', filter=Q(todo__state=False)) * 100 / Count('todo'),
    )

in a project I have 12 tasks, 8 finished and 4 in progress. The system returns 66% completed and 33% in progress, whose sum is 99% and not 100%

{{ project.todo_done }}
{{ project.todo_left }}

enter image description here

part of the bar is blank because the 1% is missing. I try to use round as follows but it is not possible

todo_done=round(Count('todo', filter=Q(todo__state=True)) * 100 / Count('todo')),
TypeError: type CombinedExpression doesn't define __round__ method

Solution

  • You can not use Python's round, since that does not understand anything about database expressions. However you can make use of a Round expression [Django-doc]:

    from django.db.models import F
    from django.db.models.functions import Round
    
    queryset = Project.objects.annotate(
        todo_done=Round(Count('todo', filter=Q(todo__state=True)) * 100 / Count('todo')),
        todo_left=100*Count('todo') - F('todo_done'),
    )

    If the state of the Todos can only be True or False, it is probably better to subtract todo_done from 100 times Count('todo'), since then it is normally guaranteed that the two will sum up to 100, unless of course there are no related Todos.

    Some databases will use integer division if the two operands are integers, you can prevent this by casting it to a FloatField:

    from django.db.models import F, FloatField
    from django.db.models.functions import Cast, Round
    
    queryset = Project.objects.annotate(
        todo_done=Round(
            Cast(Count('todo', filter=Q(todo__state=True)), output_field=FloatField())
            * 100 / Count('todo')
        ),
        todo_left=100*Count('todo') - F('todo_done'),
    )