Reputation: 77
I'm currently trying to compute the score
of a Survey
in SQL side only to be able to order survey by their scores.
My current logic is:
Answer
Question
(which can have multiples Answer
so I use Sum)points
of my whole Survey
based on the sum of all Question
points (Question.point * sum(Answer.coef)) basically Survey.objects.annotate(
answerresponse__realcoef=models.Case(
models.When(answerresponse__coef__isnull=True,
then=models.F('answerresponse__answer__coef')),
models.When(answerresponse__coef__isnull=False,
then=models.F('answerresponse__coef')),
output_field=models.FloatField(),
)
).annotate(
answerresponse__realcoef_sum=models.Sum(
models.F('answerresponse__realcoef')
)
).annotate(
points=models.Sum(
models.F('answerresponse__realcoef_sum') *
models.F('answerresponse__answer__question__points'),
output_field=models.IntegerField()
),
maxpoints=models.Sum('sections__question__points')
)
The database schema is something like:
Survey > Sections > Questions (points) > Answer (coef) > AnswerResponse (coef override)
and I get the following error:
FieldError: Cannot compute Sum('<CombinedExpression: F(answerresponse__realcoef_sum) * F(answerresponse__answer__question__points)>'): '<CombinedExpression: F(answerresponse__realcoef_sum) * F(answerresponse__answer__question__points)>' is an aggregate
which I understand as
That SQL part was not executed yet so you cannot rely on it
Is it possible to achieve that by keeping in the SQL side only?
Upvotes: 2
Views: 5417
Reputation: 77
i was able to find a solution for that: i'm using a subquery to compute the total per survey then annotate into the Survey queryset, the OuterRef
method allowed me to have a reference to the parent request pk into the subrequest.
my big error was to think that my annotations were local to my nested field but it was a global aggregation
def mark_bet_scores(surveys_qs: models.QuerySet) -> models.QuerySet:
"""Annotate a bet queryset with the following fields:
- maxpoints : the amount of possibles points to get on this bet (int)
- score : the current survey score (from 0 to 100) (float)
- points : the amount of points gained on this bet (int)
"""
responses = AnswerResponse.objects \
.filter(survey__pk=models.OuterRef('pk')) \
.prefetch_related('answer', 'answer__question') \
.annotate(
realcoef=models.Case(
models.When(coef__isnull=True, then=models.F('answer__coef')),
models.When(coef__isnull=False, then=models.F('coef'))
)
) \
.annotate(
points=models.ExpressionWrapper(
models.F('realcoef') * models.F('answer__question__points'),
output_field=models.IntegerField()
)
) \
.values('survey__pk') \
.annotate(
total=models.Sum(models.F('points'))
) \
.values('total')
# now we need to make the relation between `Survey.answerresponse` and
# responses
surveys = surveys_qs \
.annotate(
maxpoints=models.Sum('sections__question__points'),
points=models.Subquery(
responses,
output_field=models.IntegerField()
)
) \
.annotate(
score=models.Case(
models.When(maxpoints=0, then=0),
models.When(maxpoints__gt=0, then=models.ExpressionWrapper(
models.F('points') / models.F('maxpoints') * 100,
output_field=models.FloatField())
)
)
)
return surveys
Upvotes: 1
Reputation: 4432
You can try to first annotate the result of answerresponse__realcoef_sum
* answerresponse__answer__question__points
with expression wrapper:
.annotate(
total=ExpressionWrapper(
F('answerresponse__realcoef_sum') * F('answerresponse__answer__question__points'), output_field=IntegerField())
)
And then aggregate the result by SUM
:
.annotate(points=models.Sum('total'))
Upvotes: 2