From d8c5206e2ed23b9577146ddf3e66922136374399 Mon Sep 17 00:00:00 2001
From: jo <ljonas@riseup.net>
Date: Sun, 19 Sep 2021 20:48:19 +0200
Subject: [PATCH] Fix cueout for overlapping starts & ends schedule

---
 api/libretimeapi/models/schedule.py           | 42 ++++++++++---
 api/libretimeapi/tests/models/__init__.py     |  0
 .../tests/models/test_schedule.py             | 62 +++++++++++++++++++
 3 files changed, 95 insertions(+), 9 deletions(-)
 create mode 100644 api/libretimeapi/tests/models/__init__.py
 create mode 100644 api/libretimeapi/tests/models/test_schedule.py

diff --git a/api/libretimeapi/models/schedule.py b/api/libretimeapi/models/schedule.py
index 314ef5ec2..f4bc7aa61 100644
--- a/api/libretimeapi/models/schedule.py
+++ b/api/libretimeapi/models/schedule.py
@@ -24,21 +24,45 @@ class Schedule(models.Model):
 
     def get_cueout(self):
         """
-        Returns a cueout that is based on the current show. Cueout of a specific
-        item can potentially overrun the show that it is scheduled in. In that
-        case, the cueout should be the end of the show. This prevents the next
-        show having overlapping items playing.
+        Returns a scheduled item cueout that is based on the current show instance.
+
+        Cueout of a specific item can potentially overrun the show that it is
+        scheduled in. In that case, the cueout should be the end of the show.
+        This prevents the next show having overlapping items playing.
+
+        Cases:
+        - When the schedule ends before the end of the show instance,
+        return the stored cueout.
+
+        - When the schedule starts before the end of the show instance
+        and ends after the show instance ends,
+        return timedelta between schedule starts and show instance ends.
+
+        - When the schedule starts after the end of the show instance,
+        return the stored cue_out even if the schedule WILL NOT BE PLAYED.
         """
-        if self.instance.ends < self.ends:
+        if self.starts < self.instance.ends and self.instance.ends < self.ends:
             return self.instance.ends - self.starts
         return self.cue_out
 
     def get_ends(self):
         """
-        Returns an item end that is based on the current show. Ends of a
-        specific item can potentially overrun the show that it is scheduled in.
-        In that case, the end should be the end of the show. This prevents the
-        next show having overlapping items playing.
+        Returns a scheduled item ends that is based on the current show instance.
+
+        End of a specific item can potentially overrun the show that it is
+        scheduled in. In that case, the end should be the end of the show.
+        This prevents the next show having overlapping items playing.
+
+        Cases:
+        - When the schedule ends before the end of the show instance,
+        return the scheduled item ends.
+
+        - When the schedule starts before the end of the show instance
+        and ends after the show instance ends,
+        return the show instance ends.
+
+        - When the schedule starts after the end of the show instance,
+        return the show instance ends.
         """
         if self.instance.ends < self.ends:
             return self.instance.ends
diff --git a/api/libretimeapi/tests/models/__init__.py b/api/libretimeapi/tests/models/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/api/libretimeapi/tests/models/test_schedule.py b/api/libretimeapi/tests/models/test_schedule.py
new file mode 100644
index 000000000..b04e25dd8
--- /dev/null
+++ b/api/libretimeapi/tests/models/test_schedule.py
@@ -0,0 +1,62 @@
+from datetime import datetime, timedelta
+
+from django.test import SimpleTestCase
+from libretimeapi.models import Schedule, Show, ShowInstance
+
+
+class TestSchedule(SimpleTestCase):
+    def test_get_cueout(self):
+        show_instance = ShowInstance(
+            created=datetime(year=2021, month=10, day=1, hour=12),
+            starts=datetime(year=2021, month=10, day=2, hour=1),
+            ends=datetime(year=2021, month=10, day=2, hour=2),
+        )
+
+        length = timedelta(minutes=10)
+        cue_in = timedelta(seconds=1)
+        cue_out = length - timedelta(seconds=4)
+
+        # No overlapping schedule datetimes, normal usecase:
+        s1_starts = datetime(year=2021, month=10, day=2, hour=1, minute=30)
+        s1_ends = s1_starts + length
+
+        s1 = Schedule(
+            starts=s1_starts,
+            ends=s1_ends,
+            cue_in=cue_in,
+            cue_out=cue_out,
+            instance=show_instance,
+        )
+
+        self.assertEqual(s1.get_cueout(), cue_out)
+        self.assertEqual(s1.get_ends(), s1_ends)
+
+        # Mixed overlapping schedule datetimes (only ends is overlapping):
+        s2_starts = datetime(year=2021, month=10, day=2, hour=1, minute=55)
+        s2_ends = s2_starts + length
+
+        s2 = Schedule(
+            starts=s2_starts,
+            ends=s2_ends,
+            cue_in=cue_in,
+            cue_out=cue_out,
+            instance=show_instance,
+        )
+
+        self.assertEqual(s2.get_cueout(), timedelta(minutes=5))
+        self.assertEqual(s2.get_ends(), show_instance.ends)
+
+        # Fully overlapping schedule datetimes (starts and ends are overlapping):
+        s3_starts = datetime(year=2021, month=10, day=2, hour=2, minute=1)
+        s3_ends = s3_starts + length
+
+        s3 = Schedule(
+            starts=s3_starts,
+            ends=s3_ends,
+            cue_in=cue_in,
+            cue_out=cue_out,
+            instance=show_instance,
+        )
+
+        self.assertEqual(s3.get_cueout(), cue_out)
+        self.assertEqual(s3.get_ends(), show_instance.ends)