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

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")

Related

Trouble with Django annotation with multiple ForeignKey references

I'm struggling with annotations and haven't found examples that help me understand. Here are relevant parts of my models:
class Team(models.Model):
team_name = models.CharField(max_length=50)
class Match(models.Model):
match_time = models.DateTimeField()
team1 = models.ForeignKey(
Team, on_delete=models.CASCADE, related_name='match_team1')
team2 = models.ForeignKey(
Team, on_delete=models.CASCADE, related_name='match_team2')
team1_points = models.IntegerField(null=True)
team2_points = models.IntegerField(null=True)
What I'd like to end up with is an annotation on the Teams objects that would give me each team's total points. Sometimes, a team is match.team1 (so their points are in match.team1_points) and sometimes they are match.team2, with their points stored in match.team2_points.
This is as close as I've gotten, in maybe a hundred or so tries:
teams = Team.objects.annotate(total_points =
Value(
(Match.objects.filter(team1=21).aggregate(total=Sum(F('team1_points'))))['total'] or 0 +
(Match.objects.filter(team2=21).aggregate(total=Sum(F('team2_points'))))['total'] or 0,
output_field=IntegerField())
)
This works great, but (of course) annotates the total_points for the team with pk=21 to every team in the queryset. If there's a better approach for all this, I'd love to see it, but short of that, if you can show me how to turn those '21' values into a reference to the outer team's pk, I think that will work?
EDIT: I ended up using a combination of elyas' answers and annotating a raw SQL statement to solve my issues. I was not able to keep normal annotations from dropping non-unique scores from the queryset, but raw SQL seems to work.
Here's that raw annotation:
teams = Team.objects.raw('select id, sum(points) as total_points from (select team1_id as id, team1_points as points from leagueman_match union all select team2_id as id, team2_points as points from leagueman_match) group by id order by total_points desc;')
An alternative solution to annotating the QuerySet might be to make total_points a #property of the Team model (depending on the use case):
from django.db.models import Case, Q, Sum, When
class Team(models.Model):
team_name = models.CharField(max_length=50)
#property
def total_points(self):
return Match.objects.filter(Q(team1=self.id) | Q(team2=self.id)).aggregate(
total_points=Sum(Case(
When(team1=self.id, then='team1_points'),
When(team2=self.id, then='team2_points')
))
)['total_points']
The disadvantage is that it can't be used in subsequent QuerySet operations e.g. .values(), .order_by().
Django also has a #cached_property decorator which will cache the output of the attribute when it is first called.
Other solutions tried
Originally I thought you could leverage the reverse relations match_team1 and match_team2 from the Team model to generate a simple annotation:
teams = Team.objects.annotate(
total_points=(
Sum('match_team1__team1_points', distinct=True)
+ Sum('match_team2__team2_points', distinct=True)
)
)
Unfortunately this solution encounters difficulties in handling duplicates. The distinct=True argument eliminates the issue of points from the same match being summed more than once. But it introduces a different issue where different matches with the same points scored will be excluded.
Maybe I would advice to refactor your data model. Even if you find a solution for this specific problem, you may want to think little ahead.
This is a solution:
class Team(models.Model):
team_name = models.CharField(max_length=50)
class Match(models.Model):
match_time = models.DateTimeField()
team1 = models.ForeignKey(
Team, on_delete=models.CASCADE, related_name='match_team1')
team2 = models.ForeignKey(
Team, on_delete=models.CASCADE, related_name='match_team2')
class Points(models.Model):
match = models.ForeignKey(
Match, on_delete=models.CASCADE, related_name='match')
team = models.ForeignKey(
Team, on_delete=models.CASCADE, related_name='team')
points = models.IntegerField(null=True)
With this you can sum up easily the points of any team, and also filter it by matches.

How to find the highest value of a ForeignKey field for a specific user in Django

I am building an app that allows users to record their workouts. First they will create an exercise, (e.g Bench Press), and then they will complete a form to show how much weight they were able to lift for that specific exercise. Their results will display below the form. There will be many workout forms, relating to many different workouts. The workouts and exercises will also be specific to each user.
Here is my models.py:
from django.contrib.auth.models import User
class Exercise(models.Model):
name = models.CharField(max_length=100)
class Workout(models.Model):
user = models.ForeignKey(Profile, on_delete=models.CASCADE)
weight = models.DecimalField(default=0.0, max_digits=5, decimal_places=1)
exercise = models.ForeignKey(Exercise, on_delete=models.CASCADE, default=None)
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
What I now want to do is be able to show the user what their max lift was for each different workout, but can't figure out how to retrieve this information. I have searched for the answer online and it seems that using aggregate or annotate might be the way to go, but I have tried a bunch of different queries and can't get it to show what I need. Hope somebody can help.
You can annotate your Exercises with the maximum weight for the Workouts of a given user with:
from django.db.models import Max
Exercise.objects.filter(
workout__user=someprofile
).annotate(
max_weight=Max('workout__weight')
)
The Exercise objects that arise from this queryset will have an extra attribute .max_weight that contains the maximum weight for that exercise for someprofile.
Given a user you need to annotate each excersise with max weight.
from django.db.models import Max
user.workout_set.values('exercise').annotate(max_weight=Max('weight'))
this should work
Instead of using aggregation, an easy way of doing it is ordering and then using distinct on exercise:
Workout.objects.filter(user=user)\
.order_by('exercise', '-weight')\
.distinct('exercise')
This will give you a list of all workouts with the biggest weight for each exercise.

Annotate returning null on non-existent values and I want to convert this to null into empty string

I have two models , Customer and Purchase. I want to return the last store that the customer purchased from and order on this.
I can successfully show this list, showing last store the Customer purchased from, and being able to order on store name, using the following models, query/subquery.
# models
class Customer(models.Model):
name = models.CharField(max_length=30)
class Purchase(models.Model):
store = models.CharField(max_length=30)
amount = models.DecimalField(decimal_places=2,max_digits=6)
customerID = models.ForeignKey(Customer, on_delete=models.CASCADE,
related_name='purchases')
order_date = models.DateField()
#viewset
#subquery
purchase_qs = Purchase.objects.filter(customerID=OuterRef("pk")).order_by("-order_date")
queryset = Customer.objects.all().annotate(last_purchase=Subquery(purchase_qs.values('store')[:1]))
ordering_fields = ['last_purchase']
My Current Output for Customers who have zero Purchases is.
"last_purchase":null
I want to have
"last_purchase":""
ForeignKey fields automatically append _id to the name of the model field, so you'd need to use customerId_id to reference the ForeignKey. Clearly this isn't what you want, so I'd recommend renaming the field to customer instead, also I think this is why your query is blank.
With that being said, you don't really need Subquery or OuterRef for this. Instead you can use the reverse relation of your Customer model, along with Max:
from django.db.models import Max
Customer.objects.select_related('Purchase').annotate(
last_purchase = Max('purchases__order_date')
)
Lastly, the null key is False by default so no need to say null=False, and it doesn't make sense to have blank=True but null=False. See this question for an excellent explanation about what the differences between null and blank are.
Update
The Coalesce function seems to be perfect for this scenario:
from django.db.models import Max, Coalesce, Value
Customer.objects.select_related('Purchase').annotate(
last_purchase = Coalesce(Max('purchases__order_date', Value(''))
)

Annotate over a field of intermediate model django

I have the following scheme:
class User(AbstractUser):
pass
class Task(models.Model):
pass
class Contest(models.Model):
tasks = models.ManyToManyField('Task',
related_name='contests',
blank=True,
through='ContestTaskRelationship')
participants = models.ManyToManyField('User',
related_name='contests_participated',
blank=True,
through='ContestParticipantRelationship')
class ContestTaskRelationship(models.Model):
contest = models.ForeignKey('Contest', on_delete=models.CASCADE)
task = models.ForeignKey('Task', on_delete=models.CASCADE)
cost = models.IntegerField()
class ContestParticipantRelationship(models.Model):
contest = models.ForeignKey('Contest', on_delete=models.CASCADE)
user = models.ForeignKey('User', on_delete=models.CASCADE)
task = models.ForeignKey('Task', on_delete=models.CASCADE, related_name='contests_participants_relationship')
is_solved = models.BooleanField()
Now I get the contest object and need to fetch all tasks over the tasks field, rach annotated with count of users solved it. So, I need to count the number of ContestParticipantRelationship with needed task, needed contest and is_solved set to True. How to make such a query?
Probably something like:
from django.db.models import IntegerField, Value, Sum
from django.db.models.functions import Cast, Coalesce
Task.objects.filter(
contests__contest=some_contest,
).annotate(
nsolved=Cast(Coalesce(
Sum('contests_participants_relationship__is_solved'), Value(0)
),IntegerField())
)
So here we first filter on the fact that the contest of the task is some_contest. And next we perform a Sum(..) over the is_solved column. Since there are corner-cases where this can be NULL (in case there is no user that made an attempt, etc.), then we convert it to a 0, and furthermore we cast it to an IntegerField, since otherwise some instances might be annotated with True, and False in case zero or one user solves it.

Ordering objects by foreignkey field and not foreignkey count

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