From 46a485e80b08955f4a2296d279d9e21d997b36a0 Mon Sep 17 00:00:00 2001
From: Jonathan Weth <git@jonathanweth.de>
Date: Sun, 16 Jun 2024 17:02:18 +0200
Subject: [PATCH] Add first real queries for class register statistics

---
 .../statistics/StatisticsForPersonCard.vue    |  3 +
 .../coursebook/statistics/statistics.graphql  |  3 +-
 aleksis/apps/alsijil/model_extensions.py      | 83 ++++++++++++++++++-
 aleksis/apps/alsijil/schema/__init__.py       | 23 +++--
 aleksis/apps/alsijil/schema/statistics.py     | 32 +++----
 5 files changed, 112 insertions(+), 32 deletions(-)

diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForPersonCard.vue b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForPersonCard.vue
index 86ff28480..a961e6892 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForPersonCard.vue
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/StatisticsForPersonCard.vue
@@ -91,6 +91,9 @@ export default {
           ...term,
         };
       },
+      skip() {
+        return !this.schoolTerm;
+      },
     },
   },
   methods: {
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/statistics.graphql b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/statistics.graphql
index 821f961c3..1da470c12 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/statistics/statistics.graphql
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/statistics/statistics.graphql
@@ -1,5 +1,4 @@
 fragment statistics on StatisticsByPersonType {
-  schoolTerm
   participationCount
   absenceCount
   absenceReasons {
@@ -27,7 +26,7 @@ fragment statistics on StatisticsByPersonType {
   }
 }
 
-query statisticsByPerson($person: ID!, $term: ID) {
+query statisticsByPerson($person: ID!, $term: ID!) {
   statistics: statisticsByPerson(person: $person, term: $term) {
     ...statistics
   }
diff --git a/aleksis/apps/alsijil/model_extensions.py b/aleksis/apps/alsijil/model_extensions.py
index abbedd709..14b135321 100644
--- a/aleksis/apps/alsijil/model_extensions.py
+++ b/aleksis/apps/alsijil/model_extensions.py
@@ -10,10 +10,11 @@ from calendarweek import CalendarWeek
 
 from aleksis.apps.alsijil.managers import PersonalNoteQuerySet
 from aleksis.apps.chronos.models import Event, ExtraLesson, LessonPeriod
-from aleksis.core.models import Group, Person
+from aleksis.apps.kolego.models import AbsenceReason
+from aleksis.core.models import Group, Person, SchoolTerm
 from aleksis.core.util.core_helpers import get_site_preferences
 
-from .models import ExcuseType, ExtraMark, LessonDocumentation, PersonalNote
+from .models import Documentation, ExcuseType, ExtraMark, LessonDocumentation, PersonalNote
 
 
 def alsijil_url(
@@ -493,3 +494,81 @@ def generate_person_list_with_class_register_statistics(
         )
 
     return persons
+
+
+def annotate_person_statistics(
+    persons: QuerySet[Person], participations_filter: Q, personal_notes_filter: Q
+) -> QuerySet[Person]:
+    """Annotate a queryset of persons with class register statistics."""
+    persons = persons.annotate(
+        filtered_participation_statuses=FilteredRelation(
+            "participations",
+            condition=(participations_filter),
+        ),
+        filtered_personal_notes=FilteredRelation(
+            "new_personal_notes",
+            condition=(personal_notes_filter),
+        ),
+    ).annotate(
+        participation_count=Count(
+            "filtered_participation_statuses",
+            filter=Q(filtered_participation_statuses__absence_reason__isnull=True),
+            distinct=True,
+        ),
+        absence_count=Count(
+            "filtered_participation_statuses",
+            filter=Q(filtered_participation_statuses__absence_reason__count_as_absent=True),
+            distinct=True,
+        ),
+        # tardiness=Sum("filtered_participation_statuses__tardiness"),
+        # tardiness_count=Count(
+        #     "filtered_personal_notes",
+        #     filter=Q(filtered_personal_notes__tardiness__gt=0),
+        #     distinct=True,
+        # ),
+    )
+    persons = persons.order_by("last_name", "first_name")
+
+    for absence_reason in AbsenceReason.objects.all():
+        persons = persons.annotate(
+            **{
+                absence_reason.count_label: Count(
+                    "filtered_participation_statuses",
+                    filter=Q(
+                        filtered_participation_statuses__absence_reason=absence_reason,
+                    ),
+                    distinct=True,
+                )
+            }
+        )
+
+    for extra_mark in ExtraMark.objects.all():
+        persons = persons.annotate(
+            **{
+                extra_mark.count_label: Count(
+                    "filtered_personal_notes",
+                    filter=Q(filtered_personal_notes__extra_mark=extra_mark),
+                    distinct=True,
+                )
+            }
+        )
+
+    return persons
+
+
+def annotate_person_statistics_for_school_term(
+    persons: QuerySet[Person], school_term: SchoolTerm
+) -> QuerySet[Person]:
+    """Annotate a queryset of persons with class register statistics for a school term."""
+    documentations = Documentation.objects.filter(
+        participations__person__in=persons,
+        datetime_start__date__gte=school_term.date_start,
+        datetime_end__date__lte=school_term.date_end,
+    )
+    docs = list(documentations.values_list("pk", flat=True))
+
+    return annotate_person_statistics(
+        persons,
+        Q(participations__related_documentation__in=docs),
+        Q(new_personal_notes__documentation__in=docs),
+    )
diff --git a/aleksis/apps/alsijil/schema/__init__.py b/aleksis/apps/alsijil/schema/__init__.py
index ef274760f..986258b9f 100644
--- a/aleksis/apps/alsijil/schema/__init__.py
+++ b/aleksis/apps/alsijil/schema/__init__.py
@@ -8,11 +8,12 @@ import graphene
 from aleksis.apps.chronos.models import LessonEvent
 from aleksis.apps.cursus.models import Course
 from aleksis.apps.cursus.schema import CourseType
-from aleksis.core.models import Group, Person
+from aleksis.core.models import Group, Person, SchoolTerm
 from aleksis.core.schema.base import FilterOrderList
 from aleksis.core.schema.group import GroupType
 from aleksis.core.util.core_helpers import has_person
 
+from ..model_extensions import annotate_person_statistics_for_school_term
 from ..models import Documentation
 from .absences import (
     AbsencesBatchCreateMutation,
@@ -58,7 +59,7 @@ class Query(graphene.ObjectType):
     statistics_by_person = graphene.Field(
         StatisticsByPersonType,
         person=graphene.ID(required=True),
-        term=graphene.ID(required=False),
+        term=graphene.ID(required=True),
     )
     documentations_by_person = graphene.List(
         DocumentationByPersonType,
@@ -68,7 +69,7 @@ class Query(graphene.ObjectType):
     statistics_by_group = graphene.List(
         StatisticsByPersonType,
         group=graphene.ID(required=True),
-        term=graphene.ID(required=False),
+        term=graphene.ID(required=True),
     )
 
     def resolve_documentations_by_course_id(root, info, course_id, **kwargs):
@@ -190,9 +191,11 @@ class Query(graphene.ObjectType):
         return lessons_for_person
 
     @staticmethod
-    def resolve_statistics_by_person(root, info, person, term=None):
-        # TODO: Annotate person with necessary information for term.
-        return Person.objects.get(id=person)
+    def resolve_statistics_by_person(root, info, person, term):
+        school_term = SchoolTerm.objects.get(id=term)
+        return annotate_person_statistics_for_school_term(
+            Person.objects.filter(id=person), school_term
+        ).first()
 
     @staticmethod
     def resolve_documentations_by_person(root, info, person, term=None):
@@ -200,9 +203,11 @@ class Query(graphene.ObjectType):
         return Person.objects.get(id=person)
 
     @staticmethod
-    def resolve_statistics_by_group(root, info, group, term=None):
-        # TODO: Annotate persons with necessary information for term.
-        return Group.objects.get(id=group).members.all()
+    def resolve_statistics_by_group(root, info, group, term):
+        school_term = SchoolTerm.objects.get(id=term)
+
+        members = Group.objects.get(id=group).members.all()
+        return annotate_person_statistics_for_school_term(members, school_term)
 
 
 class Mutation(graphene.ObjectType):
diff --git a/aleksis/apps/alsijil/schema/statistics.py b/aleksis/apps/alsijil/schema/statistics.py
index 4b9188a7a..4efe5e70a 100644
--- a/aleksis/apps/alsijil/schema/statistics.py
+++ b/aleksis/apps/alsijil/schema/statistics.py
@@ -1,5 +1,6 @@
 import graphene
 
+from aleksis.apps.cursus.models import Subject
 from aleksis.apps.cursus.schema import SubjectType
 from aleksis.apps.kolego.models.absence import AbsenceReason
 from aleksis.apps.kolego.schema.absence import AbsenceReasonType
@@ -15,10 +16,10 @@ class AbsenceReasonWithCountType(graphene.ObjectType):
     count = graphene.Int()
 
     def resolve_absence_reason(root, info):
-        return root
+        return root["absence_reason"]
 
     def resolve_count(root, info):
-        return 6
+        return root["count"]
 
 
 class ExtraMarkWithCountType(graphene.ObjectType):
@@ -26,14 +27,13 @@ class ExtraMarkWithCountType(graphene.ObjectType):
     count = graphene.Int()
 
     def resolve_extra_mark(root, info):
-        return root
+        return root["extra_mark"]
 
     def resolve_count(root, info):
-        return 7
+        return root["count"]
 
 
 class StatisticsByPersonType(graphene.ObjectType):
-    school_term = graphene.Int()
     participation_count = graphene.Int()
     absence_count = graphene.Int()
     absence_reasons = graphene.List(AbsenceReasonWithCountType)
@@ -41,19 +41,11 @@ class StatisticsByPersonType(graphene.ObjectType):
     tardiness_count = graphene.Int()
     extra_marks = graphene.List(ExtraMarkWithCountType)
 
-    def resolve_school_term(root, info):
-        return 4
-
-    def resolve_participation_count(root, info):
-        return 3
-
-    def resolve_absence_count(root, info):
-        return 1
-
     def resolve_absence_reasons(root, info):
-        # TODO: Return actual AbsenceReasons
-        #       Needed by resolve_absence_count as well.
-        return AbsenceReason.objects.all()
+        return [
+            dict(absence_reason=reason, count=getattr(root, reason.count_label))
+            for reason in AbsenceReason.objects.all()
+        ]
 
     def resolve_tardiness_sum(root, info):
         return 17
@@ -62,8 +54,10 @@ class StatisticsByPersonType(graphene.ObjectType):
         return 5
 
     def resolve_extra_marks(root, info):
-        # TODO: Return actual ExtraMarks
-        return ExtraMark.objects.all()
+        return [
+            dict(extra_mark=extra_mark, count=getattr(root, extra_mark.count_label))
+            for extra_mark in ExtraMark.objects.all()
+        ]
 
 
 class DocumentationByPersonType(graphene.ObjectType):
-- 
GitLab