Ordering objects by foreignkey field and not foreignkey count - django

I am making a request and a voting feature for the request:
class Request(models.Model):
title = models.CharField(max_length=255)
approved = models.BooleanField(default=False)
user = models.OneToOneField(User, on_delete=models.CASCADE)
class Vote(models.Model):
request = models.ForeignKey(Request, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE)
vote_type = models.BooleanField()
I would like to order requests by the total vote counts in descending order by substracting different vote_type's (True/False) for each request. Meaning order requests with highest number of votes to lowest number of votes.
I can order by the vote this way:
Request.objects.order_by('-vote')
But this just gives me which request has highest number of foreign key count, and not the actual vote count.
I can get the actual vote counts for a request like this:
def get_vote_count(obj):
return obj.vote_set.filter(vote_type=True).count() - obj.vote_set.filter(vote_type=False).count()
But I can't figure out how to achieve this when getting all the requests and ordering them in the view.

I think you can achieve it by using the conditional expressions.
Try this:
from django.db import models
Request.objects.annotate(
num_of_true_vote_type=models.Count(
models.Case(When(vote__vote_type=True, then=1), output_field=models.IntegerField())
),
num_of_false_vote_type=models.Count(
models.Case(When(vote__vote_type=False, then=1), output_field=models.IntegerField())
),
difference=models.F('num_of_true_vote_type') - models.F('num_of_false_vote_type')
).order_by('-difference')

you can use aggregation for this
r = Request.objects.filter(approved=True, vote__vote_type=True).annotate(total_vote=Count('vote'))
this will give you QuerySet of Request with approved=True. annotate part will give you extra attribut total_vote to every Request object with value count of all related Vote that have vote_type=True.
Do not forget that its QuerySet so to see how many Vote with vote_type=True for "first" Request you do r.first().total_vote

Related

Django query - how to filter sum by date?

I am struggling with a queryset in a Django view. Basically, I have three models: User, ActivityLog, & ActivityCategory.
User is the built-in.
ActivityLog looks like this:
class ActivityLog(models.Model):
activity_datetime = models.DateTimeField(default=timezone.now)
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, related_name='activity_user')
activity_category = models.ForeignKey(ActivityCategory, on_delete=models.CASCADE, null=True, related_name='activity_cat')
activity_description = models.CharField(max_length=100, default="Misc Activity")
Activity Category:
class ActivityCategory(models.Model):
activity_name = models.CharField(max_length=40)
activity_description = models.CharField(max_length=150)
pts = models.IntegerField()
My goal is to return an aggregate by user of all the points they have accumulated by participating in activities in the log. Each log references an ActivityType, different types are worth different points.
I accomplished this with the following query in the view:
class UserScoresAPIView(generics.ListAPIView):
queryset = User.objects.all()
serializer_class = UserScoresSerializer
def get_queryset(self):
queryset = User.objects.annotate(total_pts=Coalesce(Sum('activity_user__activity_category__pts'), 0)).order_by('-total_pts')
return queryset
So now I need to add to this query and restrict it based on date of the activity. I want to basically add a filter:
.filter('activity_user__activity_datetime__gte=datetime.date(2020,10,1)')
How can I add this into my current query to accomplish this? I tried to do so here:
queryset = User.objects.annotate(total_pts=Coalesce(Sum('activity_user__activity_category__pts').filter('activity_user__activity_datetime__gte=datetime.date(2020,10,1)') , 0)).order_by('-total_pts')
But that would happen after the Sum and wouldn't be helpful (or work...) so I tried to chain it where it is pulling the User objects ​
User.objects.filter('activity_user__activity_datetime__gte=datetime.date(2020,10,1)').annotate(total_pts=Coalesce(Sum('activity_user__activity_category__pts'), 0)).order_by('-total_pts')
But now I am receiving an error when trying to parse my query:
ValueError: too many values to unpack (expected 2)
I am confused at where to go next and appreciate any guidance.
Thank you.
BCBB
Aaaaaaand I got so focused on how to chain these together that I thought I was doing it wrong but in reality, I am just not sure what possessed me to enclose the filter in quotes...
Arrrrg. It's working now as listed last without the quotes...
User.objects.filter(activity_user__activity_datetime__gte=datetime.date(2020,10,1)).annotate(total_pts=Coalesce(Sum('activity_user__activity_category__pts'), 0)).order_by('-total_pts')

Having a unqiue counter per User for a different model in Django

I would like to know what the best way to have a model Receipt where the model has an ID field that is unique to each Django User, that won't have issues with asynchronous requests. There is still a separate main ID field added by default, but I would like to have a separate ID for the receipts so that I can display this ID for each User so that their receipt numbers start from 1 when they go to their receipt list.
class Receipt(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
employee = models.CharField(max_length=255)
date = models.DateTimeField()
txn_type = models.CharField(max_length=8)
total = models.DecimalField(max_digits=10, decimal_places=2)
correction = models.ForeignKey(
ReceiptCorrection, on_delete=models.PROTECT, default=None, null=True
I understand you can check the last number and then just increment by one, but won't that have issues if two computers are running the same user and they both make a receipt at the same time?
I appreciate any help with this. Thank you!
By the "unique counter per User" you mean the code series per User. So I would create a new model ReceiptCodeSeries
from django.db import transaction, models
class ReceiptCodeSeries(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
series_count = models.IntegerField(default=0)
# Optional fields
prefix = models.CharField(default="2021")
suffix_length = models.IntegerField(default=4)
#classmethod
def get_next_code(cls, user):
with transaction.atomic():
series = cls.objects.select_for_update().get(user=user)
series.series_count += 1
series.save()
return series.prefix + str(series.ticket_series_count).zfill(series.suffix_length)
This approach locks the code series for each get_next_code call so parallel requests will be fine.
Example usage:
>>> code_series = ReceiptCodeSeries.objects.get(user=some_user)
>>> print(code_series.series_count)
0
>>> ReceiptCodeSeries.get_next_code(some_user)
"2021-0001"
>>> code_series.refresh_from_db()
>>> print(code_series.series_count)
1
If you want to learn more about this approach, please read a great article from Haki Benita - How to Manage Concurrency in Django Models

Order Django queryset by related field (with code-value)

Simplifying my model a lot, I have the following:
class Player(models.Model):
name = models.CharField(max_length=50)
number = models.IntegerField()
class Statistic(models.Model):
'''
Known codes are:
- goals
- assists
- red_cards
'''
# Implicit ID
player = models.ForeignKey(
'Player', on_delete=models.CASCADE, related_name='statistics')
code = models.CharField(max_length=50)
value = models.CharField(max_length=50, null=True)
I'm using a code-value strategy to add different statistics in the future, without the need of adding new fields to the model.
Now, I want to list all the players, so my question is:
Is there any way to order the players by their goals scored, for example?
You need to use annotate with conditional Case statement.
from django.db.models import Case, When, F, Value, IntegerField
Statistic.objects.all().annotate(
goals=Case(
When(code="goals", then=F("value")),
default=Value(0),
output_field=IntegerField()
)).order_by("-goals")
Your queryset has now additional field goals which is equal to value only if code == goals, otherwise it is equal to 0. All i had to do from now was to order by this field.
Based on #Daniel Kusy's answer, I found the inspiration to do some small modifications for my specific scenario, where ordering Players by their scored goals would be like this:
from django.db.models import Case, When, F, Value, IntegerField
Players.objects.all().annotate(
goals=Case(
When(statistics__code="goals", then=F("statistics__value")),
default=Value(0),
output_field=IntegerField()
)).order_by("-goals")

Django Queryset Prefetch Optimization for iterating over nested results

I'm looking for a way to optimize a queryset result processing in Django by improving database access performance, taking into consideration that I need to fetch a nested relation.
Taking these models as example:
class Movie(models.Model):
name = models.CharField(max_length=50)
class Ticket(models.Model):
code = models.CharField(max_length=255, blank=True, unique=True)
movie = models.ForeignKey(Movie, related_name='tickets')
class Buyer(models.Model):
name = models.CharField(max_length=50)
class Purchase(models.Model):
tickets = models.ManyToManyField(Ticket, related_name='purchases')
buyer = models.ForeignKey(Buyer, related_name='purchases')
and this Movie QuerySet:
movies = Movie.objects.all().prefetch_related('tickets__purchases__buyer')
In case I need to retrieve all buyers from each Movie in the above QuerySet, this is one approach:
for movie in movies:
buyers = Buyer.objects.filter(purchases__tickets__in=movie.tickets.all()).distinct()
But that will hit the database once for each Movie iterated. So to this in a single transaction, I'm doing something like:
def get_movie_buyers(movie):
buyers = set()
for ticket in movie.tickets.all():
for purchase in ticket.purchases.all():
if purchase.buyer:
buyers.add(purchase.buyer)
return buyers
for movie in movies:
buyers = get_movie_buyers(movie)
This approach hits the database once due to the prefetch_related in the QuerySet, but it doesn't look optimal as I'm iterating over many nested loops, which will then increase application memory overload instead.
There might be a better approach that I couldn't figure out yet, looking for some guidance.
UPDATE
alasdair suggested to use Prefetch object, tried that:
movies = Movie.objects.prefetch_related(
Prefetch(lookup='tickets__purchases__buyer',
to_attr='buyers')
).all()
for movie in movies:
print movie.buyers
But this gives me the following error:
'Movie' object has no attribute 'buyers'
The reason why it seems too difficult is the ManyToMany relation between Purchase and Tickets.
This relation allows the same ticket to be present in multiple purchases. But this will not be the case in actual data as one ticket can be purchased only once.
The query can be simplified if you remove this ManyToMany field and add a ForeignKey field in Ticket to purchase
class Ticket(models.Model):
code = models.CharField(max_length=255, blank=True, unique=True)
movie = models.ForeignKey(Movie, related_name='tickets')
purchase = models.ForeignKey(Purchase, null=True, blank=True)
Then the query can be simplified as below.
movies = Movie.objects.all().prefetch_related('tickets__purchase__buyer')
for movie in movies:
print(set(ticket.purchase.buyer for ticket in movie.tickets if ticket.purchase))
Sure this will create additional complexity while creating Purchases as you need to update the ticket objects with purchase_id.
You need to make a call on where to keep complexity based on the frequency of both actions

Filter and count with django

Suppose I have a Post and Vote tables.
Each post can be either liked or disliked (this is the post_type).
class Post(models.Model):
author = models.ForeignKey(User)
title = models.CharField(verbose_name=_("title"), max_length=100, null=True, blank=True)
content = models.TextField(verbose_name=_("content"), unique=True)
ip = models.CharField(verbose_name=_("ip"), max_length=15)
class Vote(models.Model):
user = models.ForeignKey(User)
post = models.ForeignKey(Post)
post_type = models.PositiveSmallIntegerField(_('post_type'))
I want to get posts and annotate each post with number of likes.
What is the best way to do this?
You should make a function in Post model and call this whenever you need the count.
class Post(models.Model):
...
def likes_count(self):
return self.vote_set.filter(post_type=1).count()
Use it like this:
p = Post.objects.get(pk=1)
print p.likes_count()
One approach is to add a method to the Post class that fetches this count, as shown by #sachin-gupta. However this will generate one extra query for every post that you fetch. If you are fetching posts and their counts in bulk, this is not desirable.
You could annotate the posts in bulk but I don't think your current model structure will allow it, because you cannot filter within an annotation. You could consider changing your structure as follows:
class Vote(models.Model):
"""
An abstract vote model.
"""
user = models.ForeignKey(User)
post = models.ForeignKey(Post)
class Meta:
abstract = True
class LikeVote(Vote)
pass
class DislikeVote(Vote)
pass
i.e., instead of storing likes and dislikes in one model, you have a separate model for each. Now, you can annotate your posts in bulk, in a single query:
from django.db.models import Count
posts = Post.objects.all().annotate(Count('likevote_set'))
for post in posts:
print post.likevote__count
Of course, whether or not this is feasible depends on the architecture of the rest of your app, and how many "vote types" you are planning to have. However if you are going to be querying the vote counts of posts frequently then you will need to try and avoid a large number of database queries.