Reputation: 626
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'
Upvotes: 2
Views: 1924
Reputation: 2951
Another solution might be to add it to your query sets by default. This should make the annotation available throughout.
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()
from .managers import PostManager
class Post(models.Model):
...
objects = PostManager()
...
Post.objects.last().vote
Upvotes: 0
Reputation: 1010
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
Upvotes: 2