I'm trying to figure out the best / most efficient way to get the 'progress' of a Summary
object. A Summary
object has X Grade
objects - a Grade
object is_complete
when it has a Level
chosen and has 1 or more related Evidence
objects.
I am trying to tie that Summary
'progress' to a Person
.
The models.py
look like this:
class Summary(models.Model):
id = models.BigAutoField(primary_key=True)
person = models.ForeignKey(
Person, on_delete=models.PROTECT, related_name="summaries"
)
finalized = models.BooleanField(default=False)
class Meta:
verbose_name = "Summary"
verbose_name_plural = "Summaries"
def progress(self):
"""Return the progress of the summary."""
grades = self.grades.all()
finished_grades = (
Grade.complete.all().filter(summary=self).count()
)
try:
progress = (finished_grades / grades.count()) * 100
class Grade(models.Model):
id = models.BigAutoField(primary_key=True)
summary = models.ForeignKey(
Summary, on_delete=models.PROTECT, related_name="%(class)ss"
)
level = models.ForeignKey(
Level,
on_delete=models.PROTECT,
null=True,
blank=True,
related_name="%(class)ss",
)
class Meta:
verbose_name = "Grade"
verbose_name_plural = "Grades"
@property
def is_complete(self):
if 0 < self.evidences.count() and self.level:
return True
return False
class Evidence(models.Model):
id = models.BigAutoField(primary_key=True)
grade = models.ForeignKey(
Grade, on_delete=models.PROTECT, related_name="%(class)ss"
)
comment = models.TextField()
My views.py
looks like this:
class PersonListView(ListView):
model = Person
template_name = "app/person_list.html"
context_object_name = "person_list"
def get_queryset(self):
people = Person.objects.all().prefetch_related("summaries", "summaries__grades", "summaries__grades__evidences")
# There should only be one non-finalized summary
# or there will be None
first_summary = Summary.objects.filter(
person__id=OuterRef("id"), finalized=False
)
return people.annotate(
summary_progress=Subquery(first_summary[:1].progress()),
)
I'm trying to do this in as few queries as possible (I think with prefetch maybe 3-4 queries would be possible in total?)
In my template I'm trying to make it simple to get that so I can do something simple as I'm looping through the list of people:
<div >
{{ student.summary_progress }}
</div>
The code above doesn't work because I'm trying to annotate that .progress()
method onto the People
queryset. I can't seem to figure out the best way to accomplish it.
CodePudding user response:
Would something like that work for you ?
Summary.objects.alias(total_grades=Count("grade"), finished_grades=Count("grade", filter=Q(grade__evidences__isnull=False,level__isnull=False))).annotate(progress=F("finished_grades") / F("total_grades")).
The alias method allows for calculated values (that won't be returned).
The Count object will return the number of related entries. It can use Q filters.
The annotate method allows for calculated values that will be returned.
The F object allows you to read colums (even calculated ones) for filtering or annotation
CodePudding user response:
I came up with a similar solution to (Alombaros's answer), but I believe I've fleshed it out a bit more, and I've tested that it works. Using Django Debug Toolbar, I can see that this data can be fetched with exactly one query.
from django.db.models import (
Count,
ExpressionWrapper,
F,
FloatField,
OuterRef,
Q,
Subquery,
Value,
)
class PersonListView(ListView):
model = Person
template_name = "app/person_list.html"
context_object_name = "person_list"
def get_queryset(self):
qs = super().get_queryset()
# Build up the queryset for the subquery
summaries_subquery_qs = Summary.objects.filter(person_id=OuterRef("id"), finalized=False)
summaries_subquery_qs = summaries_subquery_qs.alias(
total_grades=Count("grades"),
finished_grades=Count("grades", filter=Q(
grades__evidences__isnull=False, grades__level__isnull=False
))).annotate(
# ExpressionWrapper allows you to output using a FloatField.
summary_progress=ExpressionWrapper(
# Multiplying by 100 in order to get a percentage value between 0-100
(F("finished_grades") * Value(100) / F("total_grades")),
output_field=FloatField())
)
qs = qs.annotate(
# Now you can annotate that subquery, and "pluck" out the value you want
summary_progress=Subquery(summaries_subquery_qs.values('summary_progress')[:1])
)
return qs
And here's the SQL that is generated by that query:
SELECT "myapp_person"."id",
"myapp_person"."name",
(SELECT ((COUNT(U2."id") FILTER (WHERE (U3."id" IS NOT NULL AND U2."level_id" IS NOT NULL)) * 100) /
COUNT(U2."id")) AS "finished_percentage"
FROM "myapp_summary" U0
LEFT OUTER JOIN "myapp_grade" U2
ON (U0."id" = U2."summary_id")
LEFT OUTER JOIN "myapp_evidence" U3
ON (U2."id" = U3."grade_id")
WHERE (NOT U0."finalized" AND U0."person_id" = "myapp_person"."id")
GROUP BY U0."id"
LIMIT 1) AS "finished_percentage"
FROM "myapp_person"