From 07d67ba7f6f8feb0ea8e5c7f41def2e078e46499 Mon Sep 17 00:00:00 2001
From: Hangzhi Yu <hangzhi@protonmail.com>
Date: Sun, 1 Dec 2024 18:38:15 +0100
Subject: [PATCH] Fix extending absences/entering of long-term absences

---
 .../absences/AbsenceCreationDialog.vue        | 11 +--
 .../absences/ManageStudentsDialog.vue         | 13 ++--
 .../absences/participationStatus.graphql      |  9 +++
 aleksis/apps/alsijil/models.py                | 23 ++++++
 aleksis/apps/alsijil/schema/absences.py       | 36 ++--------
 .../alsijil/schema/participation_status.py    | 72 +++++++++----------
 6 files changed, 88 insertions(+), 76 deletions(-)

diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationDialog.vue b/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationDialog.vue
index 7a4b17c80..690b07972 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationDialog.vue
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationDialog.vue
@@ -161,14 +161,17 @@ export default {
           reason: this.absenceReason,
         },
         (storedDocumentations, incomingStatuses) => {
-          const documentation = storedDocumentations.find(
-            (doc) => doc.id === this.documentation.id,
-          );
-
           incomingStatuses.forEach((newStatus) => {
+            const documentation = storedDocumentations.find(
+              (doc) => doc.id === newStatus.relatedDocumentation.id,
+            );
+            if (!documentation) {
+              return;
+            }
             const participationStatus = documentation.participations.find(
               (part) => part.id === newStatus.id,
             );
+
             participationStatus.absenceReason = newStatus.absenceReason;
             participationStatus.isOptimistic = newStatus.isOptimistic;
           });
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/absences/ManageStudentsDialog.vue b/aleksis/apps/alsijil/frontend/components/coursebook/absences/ManageStudentsDialog.vue
index bfd62a493..1fdbd333e 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/absences/ManageStudentsDialog.vue
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/absences/ManageStudentsDialog.vue
@@ -129,15 +129,18 @@ export default {
           input: this.markAsAbsentDay.participationIDs,
         },
         (storedDocumentations, incomingStatuses) => {
-          const documentation = storedDocumentations.find(
-            (doc) => doc.id === this.documentation.id,
-          );
-
           incomingStatuses.forEach((newStatus) => {
+            const documentation = storedDocumentations.find(
+              (doc) => doc.id === newStatus.relatedDocumentation.id,
+            );
+            if (!documentation) {
+              return;
+            }
             const participationStatus = documentation.participations.find(
               (part) => part.id === newStatus.id,
             );
-            participationStatus.baseAbsence = newStatus.baseAbsence;
+
+            participationStatus.absenceReason = newStatus.absenceReason;
             participationStatus.isOptimistic = newStatus.isOptimistic;
           });
 
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/absences/participationStatus.graphql b/aleksis/apps/alsijil/frontend/components/coursebook/absences/participationStatus.graphql
index 39eb59528..a20cb091a 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/absences/participationStatus.graphql
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/absences/participationStatus.graphql
@@ -69,6 +69,15 @@ mutation extendParticipationStatuses($input: [ID]!) {
   extendParticipationStatuses(input: $input) {
     items: participations {
       id
+      relatedDocumentation {
+        id
+      }
+      absenceReason {
+        id
+        name
+        shortName
+        colour
+      }
     }
     absences {
       id
diff --git a/aleksis/apps/alsijil/models.py b/aleksis/apps/alsijil/models.py
index 2f52a8988..8ab95cdfa 100644
--- a/aleksis/apps/alsijil/models.py
+++ b/aleksis/apps/alsijil/models.py
@@ -472,6 +472,29 @@ class ParticipationStatus(CalendarEvent):
         """Return the title of the calendar event."""
         return ""
 
+    @classmethod
+    def set_from_kolego_by_datetimes(
+        cls, kolego_absence: KolegoAbsence, person: Person, start: datetime, end: datetime
+    ) -> list["ParticipationStatus"]:
+        participation_statuses = []
+
+        events = cls.get_single_events(
+            start,
+            end,
+            None,
+            {"person": person},
+            with_reference_object=True,
+        )
+
+        for event in events:
+            participation_status = event["REFERENCE_OBJECT"]
+            participation_status.absence_reason = kolego_absence.reason
+            participation_status.base_absence = kolego_absence
+            participation_status.save()
+            participation_statuses.append(participation_status)
+
+        return participation_statuses
+
     def fill_from_kolego(self, kolego_absence: KolegoAbsence):
         """Take over data from a Kolego absence."""
         self.base_absence = kolego_absence
diff --git a/aleksis/apps/alsijil/schema/absences.py b/aleksis/apps/alsijil/schema/absences.py
index ab006e1d8..28eca0172 100644
--- a/aleksis/apps/alsijil/schema/absences.py
+++ b/aleksis/apps/alsijil/schema/absences.py
@@ -2,7 +2,6 @@ import datetime
 from typing import List
 
 from django.core.exceptions import PermissionDenied
-from django.db.models import Q
 
 import graphene
 
@@ -43,41 +42,18 @@ class AbsencesForPersonsCreateMutation(graphene.Mutation):
             if not info.context.user.has_perm("alsijil.register_absence_rule", person):
                 raise PermissionDenied()
 
-            # Check if there is an existing absence with overlapping datetime
-            absences = Absence.objects.filter(
-                Q(datetime_start__lte=start) | Q(date_start__lte=start.date()),
-                Q(datetime_end__gte=end) | Q(date_end__gte=end.date()),
+            kolego_absence = Absence.get_for_person_by_datetimes(
+                datetime_start=start,
+                datetime_end=end,
                 reason_id=reason,
                 person=person,
+                defaults={"comment": comment},
             )
 
-            if len(absences) > 0:
-                kolego_absence = absences.first()
-            else:
-                # Check for same times and create otherwise
-                kolego_absence, __ = Absence.objects.get_or_create(
-                    datetime_start=start,
-                    datetime_end=end,
-                    reason_id=reason,
-                    person=person,
-                    defaults={"comment": comment},
-                )
-
-            events = ParticipationStatus.get_single_events(
-                start,
-                end,
-                None,
-                {"person": person},
-                with_reference_object=True,
+            participation_statuses += ParticipationStatus.set_from_kolego_by_datetimes(
+                kolego_absence=kolego_absence, person=person, start=start, end=end
             )
 
-            for event in events:
-                participation_status = event["REFERENCE_OBJECT"]
-                participation_status.absence_reason_id = reason
-                participation_status.base_absence = kolego_absence
-                participation_status.save()
-                participation_statuses.append(participation_status)
-
         return AbsencesForPersonsCreateMutation(
             ok=True, participation_statuses=participation_statuses
         )
diff --git a/aleksis/apps/alsijil/schema/participation_status.py b/aleksis/apps/alsijil/schema/participation_status.py
index bb0e24e05..2f6a830e2 100644
--- a/aleksis/apps/alsijil/schema/participation_status.py
+++ b/aleksis/apps/alsijil/schema/participation_status.py
@@ -1,6 +1,7 @@
 import datetime
 
 from django.core.exceptions import PermissionDenied
+from django.utils.formats import date_format
 from django.utils.translation import gettext_lazy as _
 
 import graphene
@@ -105,60 +106,57 @@ class ExtendParticipationStatusToAbsenceBatchMutation(graphene.Mutation):
         if participation.date_end:
             end_date = participation.date_end
         else:
-            end_date = ParticipationStatus.value_end_datetime(participation).date()
+            end_date = participation.datetime_end.date()
 
         end_datetime = datetime.datetime.combine(
             end_date, datetime.time.max, participation.timezone
         )
 
-        if participation.base_absence:
-            # Update the base absence to increase length if needed
-            absence = participation.base_absence
-
-            if absence.date_end:
-                if absence.date_end < end_date:
-                    absence.date_end = end_date
-                    absence.save()
-
-                return participation, absence
-
-            # Absence uses a datetime
-            if absence.datetime_end.astimezone(absence.timezone) < end_datetime:
-                # The end date ends after the previous absence end
-                absence.datetime_end = end_datetime
-                absence.save()
-
-            return participation, absence
+        data = dict(
+            reason=participation.absence_reason if participation.absence_reason else None,
+            person=participation.person,
+        )
 
+        if participation.date_start:
+            data["date_start"] = participation.date_start
+            data["date_end"] = end_date
+            start_datetime = datetime.datetime.combine(
+                participation.date_start, datetime.time.min, participation.timezone
+            )
         else:
-            # No base absence, simply create one if absence reason is given
-            data = dict(
-                reason_id=participation.absence_reason.id if participation.absence_reason else None,
-                person=participation.person,
+            data["datetime_start"] = participation.datetime_start
+            data["datetime_end"] = end_datetime
+            start_datetime = participation.datetime_start
+
+        defaults = dict(
+            comment=_("Extended by {full_name} on {datetime}").format(
+                full_name=info.context.user.person.full_name,
+                datetime=date_format(participation.date_start or participation.datetime_start),
             )
+        )
 
-            if participation.date_start:
-                data["date_start"] = participation.date_start
-                data["date_end"] = end_date
-            else:
-                data["datetime_start"] = ParticipationStatus.value_start_datetime(participation)
-                data["datetime_end"] = end_datetime
-
-            absence, __ = Absence.objects.get_or_create(**data)
+        absence = Absence.get_for_person_by_datetimes(**data, defaults=defaults)
 
-            participation.base_absence = absence
-            participation.save()
+        participations = ParticipationStatus.set_from_kolego_by_datetimes(
+            kolego_absence=absence,
+            person=participation.person,
+            start=start_datetime,
+            end=end_datetime,
+        )
 
-            return participation, absence
+        return participations, absence
 
     @classmethod
     def mutate(cls, root, info, input):  # noqa
         with create_revision():
             set_user(info.context.user)
             set_comment(_("Extended absence reason from coursebook."))
-            participations, absences = zip(
-                *[cls.create_absence(info, participation_id) for participation_id in input]
-            )
+            participations = []
+            absences = []
+            for participation_id in input:
+                p, a = cls.create_absence(info, participation_id)
+                participations += p
+                absences.append(a)
 
         return ExtendParticipationStatusToAbsenceBatchMutation(
             participations=participations, absences=absences
-- 
GitLab