81

I'm making a QA site that is similar to the page you're on right now. I'm attempting to order answers by their score, but answers which have no votes are having their score set to None rather than 0. This results in answers with no votes being at the bottom of the page below negatively ranked answers. How can I make the annotated score be zero when there are no votes for an answer?

Here's my model:

from django.contrib.auth.models import User

Answer(models.Model):
    //some fields here
    pass

VOTE_CHOICES = ((-1, Down), (1, Up))

Vote(models.Model):
    user = models.ForeignKey(User)
    answer = models.ForeignKey(Answer)
    type = models.IntegerField(choices = VOTE_CHOICES)

    class Meta:
        unique_together = (user, answer)

And here's my query:

answers = Answer.objects.filter(<something here>)
                        .annotate(score=Sum('vote__type'))
                        .order_by('-score')

edit: And to be clear, I'd like to do this in the query. I know I could turn it into a list and then sort it in my python code, but I'd like to avoid that if possible.

Corey
  • 14,101
  • 7
  • 38
  • 35
Jackie
  • 811
  • 1
  • 6
  • 4
  • 2
    FYI, there is an open [Django ticket](https://code.djangoproject.com/ticket/10929) about default values for `Sum()`. – Gary Feb 07 '12 at 22:48

3 Answers3

204

You can use the Coalesce function from django.db.models.functions like:

answers = (Answer.objects
    .filter(<something here>)
    .annotate(score=Coalesce(Sum('vote__type'), 0))
    .order_by('-score'))
Dustin Wyatt
  • 4,046
  • 5
  • 31
  • 60
nelson orland
  • 2,367
  • 1
  • 12
  • 6
3

from django's documentation for Coalesce:

Prevent an aggregate Sum() from returning None. The aggregate default argument uses Coalesce() under the hood.

So instead of using Coalesce, we can use the "default" argument:

answers = Answer.objects.filter(<something here>)
                    .annotate(score=Sum('vote__type', default=0))
                    .order_by('-score')
lastmaj
  • 146
  • 2
  • 8
-5

What about you use custom Manager? For example:

AnswerManager(models.Manager):
    def all_with_score(self):
       qs = self.get_query_set().annotate(score=Sum('vote__type'))
       # Here, you can do stuff with QuerySet, for example
       # iterate over all Answers and set 'score' to zero if None.

Answer(models.Model):
    //some fields here
    objects = AnswerManager()

Then, you can use:

>>> answers = Answer.objects.all_with_score().order_by('-score')
vasco
  • 1,502
  • 1
  • 16
  • 19
  • 4
    The question asked how to replace the None values with 0 for the built in Sum aggregate function. This doesn't do that – Patrick Jun 15 '14 at 00:37