From e19e90462f1c4588edd30c1c755fcc95b0917681 Mon Sep 17 00:00:00 2001
From: Hangzhi Yu <hangzhi@protonmail.com>
Date: Wed, 8 Jan 2025 01:10:11 +0100
Subject: [PATCH] Add present option to AbsenceCreationDialog

---
 .../absences/AbsenceCreationDialog.vue        | 17 +++++---
 .../absences/AbsenceCreationForm.vue          |  2 +
 .../absences/absenceCreation.graphql          | 17 ++++++++
 aleksis/apps/alsijil/models.py                | 23 +++++++++++
 aleksis/apps/alsijil/schema/__init__.py       |  2 +
 aleksis/apps/alsijil/schema/absences.py       | 41 +++++++++++++++++++
 6 files changed, 97 insertions(+), 5 deletions(-)

diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationDialog.vue b/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationDialog.vue
index 690b07972..e62c2ca4a 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationDialog.vue
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationDialog.vue
@@ -96,7 +96,10 @@ import permissionsMixin from "aleksis.core/mixins/permissions.js";
 import mutateMixin from "aleksis.core/mixins/mutateMixin.js";
 import { DateTime } from "luxon";
 
-import { createAbsencesForPersons } from "./absenceCreation.graphql";
+import {
+  clearAbsencesForPersons,
+  createAbsencesForPersons,
+} from "./absenceCreation.graphql";
 
 export default {
   name: "AbsenceCreationDialog",
@@ -152,13 +155,17 @@ export default {
     confirm() {
       this.handleLoading(true);
       this.mutate(
-        createAbsencesForPersons,
+        this.absenceReason !== "present"
+          ? createAbsencesForPersons
+          : clearAbsencesForPersons,
         {
           persons: this.persons.map((p) => p.id),
           start: this.$toUTCISO(this.$parseISODate(this.startDate)),
           end: this.$toUTCISO(this.$parseISODate(this.endDate)),
-          comment: this.comment,
-          reason: this.absenceReason,
+          ...(this.absenceReason !== "present" && { comment: this.comment }),
+          ...(this.absenceReason !== "present" && {
+            reason: this.absenceReason,
+          }),
         },
         (storedDocumentations, incomingStatuses) => {
           incomingStatuses.forEach((newStatus) => {
@@ -172,7 +179,7 @@ export default {
               (part) => part.id === newStatus.id,
             );
 
-            participationStatus.absenceReason = newStatus.absenceReason;
+            participationStatus.absenceReason = newStatus?.absenceReason;
             participationStatus.isOptimistic = newStatus.isOptimistic;
           });
 
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationForm.vue b/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationForm.vue
index 902a33f25..1d392d83b 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationForm.vue
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/absences/AbsenceCreationForm.vue
@@ -55,6 +55,7 @@
         <v-text-field
           :label="$t('forms.labels.comment')"
           :value="comment"
+          :disabled="absenceReason == 'present'"
           @input="$emit('comment', $event)"
         />
       </v-row>
@@ -62,6 +63,7 @@
         <div aria-required="true">
           <absence-reason-group-select
             :rules="$rules().required.build()"
+            allow-empty
             :value="absenceReason"
             :custom-absence-reasons="absenceReasons"
             @input="$emit('absence-reason', $event)"
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/absences/absenceCreation.graphql b/aleksis/apps/alsijil/frontend/components/coursebook/absences/absenceCreation.graphql
index 5a520453f..f77c78701 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/absences/absenceCreation.graphql
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/absences/absenceCreation.graphql
@@ -59,3 +59,20 @@ mutation createAbsencesForPersons(
     }
   }
 }
+
+mutation clearAbsencesForPersons(
+  $persons: [ID]!
+  $start: DateTime!
+  $end: DateTime!
+) {
+  clearAbsencesForPersons(persons: $persons, start: $start, end: $end) {
+    ok
+    items: participationStatuses {
+      id
+      isOptimistic
+      relatedDocumentation {
+        id
+      }
+    }
+  }
+}
diff --git a/aleksis/apps/alsijil/models.py b/aleksis/apps/alsijil/models.py
index 2dd3deac4..6e8465376 100644
--- a/aleksis/apps/alsijil/models.py
+++ b/aleksis/apps/alsijil/models.py
@@ -498,6 +498,29 @@ class ParticipationStatus(CalendarEvent):
 
         return participation_statuses
 
+    @classmethod
+    def clear_absence_by_datetimes(
+        cls, 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 = None
+            participation_status.base_absence = None
+            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/__init__.py b/aleksis/apps/alsijil/schema/__init__.py
index 284d8da2d..f337be4f8 100644
--- a/aleksis/apps/alsijil/schema/__init__.py
+++ b/aleksis/apps/alsijil/schema/__init__.py
@@ -24,6 +24,7 @@ from aleksis.core.util.core_helpers import (
 from ..model_extensions import annotate_person_statistics_for_school_term
 from ..models import Documentation, ExtraMark, NewPersonalNote, ParticipationStatus
 from .absences import (
+    AbsencesForPersonsClearMutation,
     AbsencesForPersonsCreateMutation,
 )
 from .documentation import (
@@ -352,6 +353,7 @@ class Mutation(graphene.ObjectType):
     touch_documentation = TouchDocumentationMutation.Field()
     update_participation_statuses = ParticipationStatusBatchPatchMutation.Field()
     create_absences_for_persons = AbsencesForPersonsCreateMutation.Field()
+    clear_absences_for_persons = AbsencesForPersonsClearMutation.Field()
     extend_participation_statuses = ExtendParticipationStatusToAbsenceBatchMutation.Field()
 
     create_extra_marks = ExtraMarkBatchCreateMutation.Field()
diff --git a/aleksis/apps/alsijil/schema/absences.py b/aleksis/apps/alsijil/schema/absences.py
index eea8b075b..8a729f0a4 100644
--- a/aleksis/apps/alsijil/schema/absences.py
+++ b/aleksis/apps/alsijil/schema/absences.py
@@ -56,3 +56,44 @@ class AbsencesForPersonsCreateMutation(graphene.Mutation):
         return AbsencesForPersonsCreateMutation(
             ok=True, participation_statuses=participation_statuses
         )
+
+
+class AbsencesForPersonsClearMutation(graphene.Mutation):
+    class Arguments:
+        persons = graphene.List(graphene.ID, required=True)
+        start = graphene.DateTime(required=True)
+        end = graphene.DateTime(required=True)
+
+    ok = graphene.Boolean()
+    participation_statuses = graphene.List(ParticipationStatusType)
+
+    @classmethod
+    def mutate(
+        cls,
+        root,
+        info,
+        persons: list[str | int],
+        start: datetime.datetime,
+        end: datetime.datetime,
+    ):
+        participation_statuses = []
+
+        persons = Person.objects.filter(pk__in=persons)
+
+        for person in persons:
+            if not info.context.user.has_perm("alsijil.register_absence_rule", person):
+                raise PermissionDenied()
+
+            participation_statuses += ParticipationStatus.clear_absence_by_datetimes(
+                person=person, start=start, end=end
+            )
+
+            Absence.clear_or_extend_absences_in_timespan(
+                person=person,
+                datetime_start=start,
+                datetime_end=end,
+            )
+
+        return AbsencesForPersonsClearMutation(
+            ok=True, participation_statuses=participation_statuses
+        )
-- 
GitLab