From 939b49ecc2491ef8db0ec031b146d00d838a4d5b Mon Sep 17 00:00:00 2001
From: Helge Jung <hej@c3pb.de>
Date: Mon, 23 Dec 2024 15:15:41 +0100
Subject: [PATCH] Conference.days() & ConferenceDay: ensure correct behaviour

---
 src/core/models/conference.py |  9 ++++---
 src/core/tests/conference.py  | 46 ++++++++++++++++++++++++++++++++---
 2 files changed, 48 insertions(+), 7 deletions(-)

diff --git a/src/core/models/conference.py b/src/core/models/conference.py
index 737006da6..b3e84fd1b 100644
--- a/src/core/models/conference.py
+++ b/src/core/models/conference.py
@@ -433,7 +433,7 @@ class Conference(models.Model):
             return days
         end = self.end if self.end is not None else timezone.now().replace(hour=23, minute=59, second=59)
         for i in range(self.days_count):
-            date = self.start.date() + timedelta(days=i)
+            date = self._day0 + timedelta(days=i + 1)
             days.append(ConferenceDay(i, date, self.timezone))
 
         days[0].start = self.start
@@ -677,10 +677,13 @@ class ConferenceDay:
     start: datetime
     end: datetime
 
-    def __init__(self, i: int, date: date, time_zone: tz):
+    def __init__(self, i: int, date: date | datetime, time_zone: tz):
+        if isinstance(date, datetime):
+            date = date.date()
+
         self.index = i + 1
         self.start = datetime.combine(date, time(6, 0)).astimezone(time_zone)
-        self.end = datetime.combine(date + timedelta(days=1), time(6, 0)).astimezone(time_zone)
+        self.end = datetime.combine(self.start + timedelta(days=1), time(6, 0), tzinfo=None).astimezone(time_zone)
 
     def __eq__(self, value: object) -> bool:
         if not isinstance(value, ConferenceDay):
diff --git a/src/core/tests/conference.py b/src/core/tests/conference.py
index 3ed7df0ec..0e44aec38 100644
--- a/src/core/tests/conference.py
+++ b/src/core/tests/conference.py
@@ -1,12 +1,23 @@
-from datetime import UTC, datetime, timedelta
+from datetime import UTC, date, datetime, timedelta
 from unittest.mock import patch
 
+import pytz
+
 from django.test import TestCase
 from django.utils import timezone
 
 from core.models.conference import Conference, ConferenceDay
 
 
+class ConferenceDayTests(TestCase):
+    def test_construction(self):
+        tz = pytz.timezone('Europe/Berlin')
+        day = ConferenceDay(0, date(2042, 12, 27), tz)
+        self.assertEqual(1, day.index)
+        self.assertEqual(datetime(2042, 12, 27, 6, 0).astimezone(tz), day.start)
+        self.assertEqual(datetime(2042, 12, 28, 6, 0).astimezone(tz), day.end)
+
+
 class ConferenceTests(TestCase):
     def setUp(self):
         self.conference = Conference(slug='foo', name='Foo Conference', is_public=True)
@@ -25,6 +36,33 @@ class ConferenceTests(TestCase):
         self.assertEqual(self.conference.get_conference_day(datetime(2042, 12, 31, 5, 30, tzinfo=UTC)), 4)
         self.assertEqual(self.conference.get_conference_day(datetime(2042, 12, 31, 5, 30, tzinfo=UTC)), 4)
 
+    def test_days(self):
+        self.conference.start = datetime(2042, 12, 27, 9, 00, tzinfo=UTC)
+        self.conference.end = datetime(2042, 12, 30, 15, 00, tzinfo=UTC)
+        self.conference.timezone = 'Europe/Berlin'
+        tz = pytz.timezone('Europe/Berlin')
+
+        expected_days = [
+            ConferenceDay(0, date(2042, 12, 27), tz),
+            ConferenceDay(1, date(2042, 12, 28), tz),
+            ConferenceDay(2, date(2042, 12, 29), tz),
+            ConferenceDay(3, date(2042, 12, 30), tz),
+        ]
+        expected_days[0].start = self.conference.start.astimezone(tz)
+        expected_days[-1].end = self.conference.end.astimezone(tz)
+
+        self.assertEqual(len(expected_days), self.conference.days_count)
+        actual_days = self.conference.days
+        self.assertEqual(len(expected_days), len(actual_days))
+
+        with self.subTest('timezones'):
+            self.assertTrue(all(d.start.tzinfo is not None for d in actual_days))
+            self.assertTrue(all(d.end.tzinfo is not None for d in actual_days))
+
+        for x in range(4):
+            with self.subTest(f'day {x +1}'):
+                self.assertEqual(expected_days[x], actual_days[x])
+
     def test_days_in(self):
         with patch.object(timezone, 'now', return_value=datetime(2042, 12, 27, 12, 34, 0, 0, tzinfo=UTC)):
             self.conference.start = timezone.now() - timedelta(days=1)
@@ -79,9 +117,9 @@ class ConferenceTests(TestCase):
             self.assertFalse(self.conference.has_ended)
             self.assertEqual(self.conference.days_count, 3)
             days = [
-                ConferenceDay(0, self.conference.start.astimezone(self.conference.timezone), self.conference.timezone),
-                ConferenceDay(1, self.conference.start.astimezone(self.conference.timezone) + timedelta(days=1), self.conference.timezone),
-                ConferenceDay(2, self.conference.start.astimezone(self.conference.timezone) + timedelta(days=2), self.conference.timezone),
+                ConferenceDay(0, self.conference.start.date(), self.conference.timezone),
+                ConferenceDay(1, (self.conference.start + timedelta(days=1)).date(), self.conference.timezone),
+                ConferenceDay(2, (self.conference.start + timedelta(days=2)).date(), self.conference.timezone),
             ]
             # The first day starts whenever the conference starts
             days[0].start = self.conference.start
-- 
GitLab