Аннотировать результаты из метода связанной модели на модель Queryset?

Я пытаюсь выяснить лучший / наиболее эффективный способ получить "прогресс" объекта Summary. Объект Summary имеет X объектов Grade - объект Grade is_complete когда у него есть Level выбранный и есть 1 или более связанных Evidence объектов.

Я пытаюсь привязать этот Summary "прогресс" к Person.

models.py выглядят следующим образом:

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

Моя views.py выглядит следующим образом:

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()),
        )

Я пытаюсь сделать это за как можно меньшее количество запросов (я думаю, что с prefetch возможно 3-4 запроса в общей сложности?)

В моем шаблоне я пытаюсь упростить получение этого, чтобы я мог сделать что-то простое, когда я просматриваю список людей:

<div class="progress">
    {{ student.summary_progress }}
</div>

Приведенный выше код не работает, потому что я пытаюсь аннотировать этот метод .progress() на кверисет People. Я не могу понять, как лучше это сделать.

Могло бы что-то подобное подойти вам?

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

Метод alias позволяет вычислять значения (которые не будут возвращены).
Объект Count возвращает количество связанных записей. Он может использовать Q-фильтры.
Метод annotate позволяет использовать вычисляемые значения, которые будут возвращены.
Объект F позволяет читать столбцы (даже вычисленные) для фильтрации или аннотации

Я придумал решение, похожее на ( ответ Аломбароса), но мне кажется, что я немного доработал его, и я проверил, что оно работает. Используя Django Debug Toolbar, я вижу, что эти данные могут быть получены с помощью ровно одного запроса.

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

А вот SQL, который генерируется этим запросом:

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"
Вернуться на верх