From 4e97b71b008d934b4d306f86ec6a395c8571f8a0 Mon Sep 17 00:00:00 2001
From: Hangzhi Yu <hangzhi@protonmail.com>
Date: Thu, 21 Nov 2024 11:34:50 +0100
Subject: [PATCH] Cut existing absences on absence creation to avoid overlaps

---
 aleksis/apps/kolego/models/absence.py | 77 +++++++++++++++++++++++++++
 1 file changed, 77 insertions(+)

diff --git a/aleksis/apps/kolego/models/absence.py b/aleksis/apps/kolego/models/absence.py
index a2c5892..7ac0443 100644
--- a/aleksis/apps/kolego/models/absence.py
+++ b/aleksis/apps/kolego/models/absence.py
@@ -1,3 +1,5 @@
+from datetime import datetime, time
+
 from django.db import models
 from django.db.models import Q, QuerySet
 from django.http import HttpRequest
@@ -145,6 +147,81 @@ class Absence(FreeBusy):
     def __str__(self):
         return f"{self.person} ({self.datetime_start} - {self.datetime_end})"
 
+    def save(self, *args, skip_overlap_handling=False, **kwargs):
+        if not skip_overlap_handling:
+            events_within = Absence.get_objects(
+                None,
+                {"person": self.person.pk},
+                start=self.datetime_start or self.date_start,
+                end=self.datetime_end or self.date_end,
+            )
+
+            # Convert dates of new event to datetimes in case dates are used
+            new_datetime_start = (
+                self.datetime_start
+                if self.datetime_start
+                else datetime.combine(self.date_start, time())
+            ).astimezone(self.timezone)
+            new_datetime_end = (
+                self.datetime_end
+                if self.datetime_end
+                else datetime.combine(self.date_end, datetime.max.time())
+            ).astimezone(self.timezone)
+
+            for event_within in events_within:
+                event_within_datetime_start = (
+                    Absence.value_start_datetime(event_within)
+                    if event_within.datetime_start
+                    else datetime.combine(Absence.value_start_datetime(event_within), time())
+                )
+                event_within_datetime_end = (
+                    Absence.value_end_datetime(event_within)
+                    if event_within.datetime_end
+                    else datetime.combine(
+                        Absence.value_end_datetime(event_within), datetime.max.time()
+                    )
+                )
+
+                if (
+                    new_datetime_start > event_within_datetime_start
+                    and new_datetime_end < event_within_datetime_end
+                ):
+                    # Cut existing event in two parts
+                    # First, cut end date of existing one
+                    event_within.datetime_end = new_datetime_start
+                    event_within.save(skip_overlap_handling=True)
+                    # Then, create new event filling up the remaining time span
+                    end_filler_event = event_within
+                    end_filler_event.pk = None
+                    end_filler_event.id = None
+                    end_filler_event._state.adding = True
+                    end_filler_event.datetime_start = new_datetime_end
+                    end_filler_event.datetime_end = event_within_datetime_end
+
+                    end_filler_event.save(skip_overlap_handling=True)
+                elif (
+                    new_datetime_start <= event_within_datetime_start
+                    and new_datetime_end >= event_within_datetime_end
+                ):
+                    # Delete existing event
+                    event_within.delete()
+                elif (
+                    new_datetime_start > event_within_datetime_start
+                    and new_datetime_end >= event_within_datetime_end
+                ):
+                    # Cut end of existing event
+                    event_within.datetime_end = new_datetime_start
+                    event_within.save(skip_overlap_handling=True)
+                elif (
+                    new_datetime_start <= event_within_datetime_start
+                    and new_datetime_end < event_within_datetime_end
+                ):
+                    # Cut start of existing event
+                    event_within.datetime_start = new_datetime_end
+                    event_within.save(skip_overlap_handling=True)
+
+        super().save(*args, **kwargs)
+
     class Meta:
         verbose_name = _("Absence")
         verbose_name_plural = _("Absences")
-- 
GitLab