2

So, I have a Post model which contains PostVotes from users

class Post(models.Model):
    voters = models.ManyToManyField(User, through="PostVote")
    #other stuff

and the post vote can have a state of either "upvote" or "downvote" (I know I should be using enums or a bool for this before I start receiving those comments) and in many cases I need to count the total score of the object for the frontend. When I have the posts in a queryset, the following solution is working well:

posts = Post.objects.all().annotate(vote=models.Sum(
    models.Case(
        models.When(postvote__state="upvote", then=1),
        models.When(postvote__state="downvote", then=-1),
        default=0,
        output_field=models.IntegerField()
    )
))

However, there are many cases where I want to do a similar thing but instead of a queryset I have just a single instance. How do I do this? Trying the above solution gives 'Post' object has no attribute 'annotate'

2 Answers 2

2

If I understood you already have a Post instance, and you would like to get its score? I so, I think you could implement a computed property as follow:

class Post(models.Model):
    voters = models.ManyToManyField(User, through="PostVote")

    @property
    def score(self):
        return sum([-1 if vote.state == 'downvote' else 1 for vote in self.postvotes.all()])

Note that this method will trigger extra DB queries if you didn't fetch your Post instance with .prefetch_related('postvotes')

Then you can use post.score to get the result

Another way to implement the property would be by doing a query instead:

class Post(models.Model):
    voters = models.ManyToManyField(User, through="PostVote")

    @property
    def score(self):
        return PostVote.objects.filter(post=self.pk).aggregate(vote=models.Sum(
            models.Case(
                models.When(state="upvote", then=1),
                models.When(state="downvote", then=-1),
                default=0,
                output_field=models.IntegerField()
            )
        ))['vote']

Note that I didn't try any of those code, so there might be any typo/wrong variable names

Sign up to request clarification or add additional context in comments.

3 Comments

Thanks! This looks like the right solution. However, when trying the computed property I'm getting the error 'Post' object has no attribute 'postvotes'. So far I've tried also using postvote, voters, and tried adding a related_name= and using that, none of which have worked. Any idea where this is going wrong?
on the PostVote class, you should have something like post = models.ForeignKey(Post, related_name='postvotes') and use self.postvotes.all() (I think I forgot the .all() part, I'll add it through edition)
But what is about code duplication? Ie for now you have the score method implemented in 2 places: in queryset(or even manager) and in instance. In case of logic changing it will be a trick to do a change in 2 places. PS: I also face with this problem, no solution except duplication and just adding the comment in code about 2-nd place.
0

Another solution might be to add it to your query sets by default. This should make the annotation available throughout.

  1. Create a file managers.py inside of your app. And create the custom queryset/manager

    from django.db import models
    
    class PostQuerySet(models.QuerySet):
        def annotate_vote(self):
          return self.annotate(vote=models.Sum(
              models.Case(
                models.When(postvote__state="upvote", then=1),
                models.When(postvote__state="downvote", then=-1),
                default=0,
                output_field=models.IntegerField()
            )))
    
    class PostManager(models.Manager):
        def get_queryset(self):
            return PostQuerySet(self.model, using=self._db).\
                annotate_vote()
    
  1. Add the new manager as inside of your model.
from .managers import PostManager

class Post(models.Model):
    ...
    objects = PostManager()
    ...
  1. This should give you accesss to the data like so:

Post.objects.last().vote

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.