diff --git a/src/core/models/conference.py b/src/core/models/conference.py index b84c50cb0048857d7e9ad66f6cc09ba484b5936f..b3e84fd1b56609fc737e21fb965f2a6370fa8b8e 100644 --- a/src/core/models/conference.py +++ b/src/core/models/conference.py @@ -433,13 +433,20 @@ 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 days[-1].end = end return days + def day(self, official_number: int) -> 'ConferenceDay': + """Returns the n-th day of the conference, starting with 1.""" + for day in self.days: + if day.index == official_number: + return day + return None + @cached_property def _day0(self) -> datetime: if self.start is None: @@ -670,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): @@ -681,7 +691,7 @@ class ConferenceDay: return self.index == value.index and self.start == value.start and self.end == value.end def __repr__(self): - return f'{self.index} ({self.start.date()} - {self.end.date()})' + return f'{self.index} ({self.start} - {self.end})' class ConferenceExportCache(models.Model): diff --git a/src/core/tests/conference.py b/src/core/tests/conference.py index 3ed7df0ec677b36116043bf74eec19f1fc60b494..0e44aec38db2a95d59ed3e69e44bee737608cf74 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 diff --git a/src/plainui/jinja2/plainui/fahrplan.html.j2 b/src/plainui/jinja2/plainui/fahrplan.html.j2 index 68955ffcb129ac117fd034e77c360748bd8f75e1..1e2b05bfc66fc9e4d9449a48933263cce1c7b34a 100644 --- a/src/plainui/jinja2/plainui/fahrplan.html.j2 +++ b/src/plainui/jinja2/plainui/fahrplan.html.j2 @@ -40,7 +40,7 @@ <form method="get" action="#now" class="hub-card mb-2"> <input type="hidden" name="mode" value="{{ mode }}"> {% if show_assembly_filters %}<input type="hidden" name="show_assembly_filters" value="y">{% endif %} - {% if day %}<input type="hidden" name="day" value="{{ day }}">{% endif %} + {% if day %}<input type="hidden" name="day" value="{{ day.index }}">{% endif %} {% if kind %}<input type="hidden" name="kind" value="{{ kind }}">{% endif %} {% if assembly %}<input type="hidden" name="assembly" value="{{ assembly.slug }}">{% endif %} {% if track %}<input type="hidden" name="track" value="{{ track.slug }}">{% endif %} @@ -78,8 +78,8 @@ </div> <div class="hub-tags mb-2"> - {% for n in range(days) %} - {{ filter_button('d' ~ (n if n != day else '') , n == day, _("Day %(n)s", n=n + 1)) }} + {% for d in days %} + {{ filter_button('d' ~ (d.index if d != day else '') , d == day, _("Day %(n)s", n=d.index)) }} {%- endfor %} <div class="hub-tag-divider"></div> diff --git a/src/plainui/views/fahrplan.py b/src/plainui/views/fahrplan.py index a1b79f94b30dbc440cf91bdf8e0af0df4ce6188d..39b247bbcc8e5b93fd031823e12b4077ed908667 100644 --- a/src/plainui/views/fahrplan.py +++ b/src/plainui/views/fahrplan.py @@ -3,7 +3,7 @@ __all__ = ( 'PublicFahrplanView', ) -from datetime import datetime, timedelta +from datetime import datetime from django.http import Http404 from django.utils import timezone @@ -37,15 +37,10 @@ class FahrplanView(ConferenceRequiredMixin, TemplateView): context['my_favorite_events'] = session_get_favorite_events(self.request.session, self.request.user) - min_date = self.conf.start - max_date = self.conf.end - if min_date is None or max_date is None: + if self.conf.days_count == 0: raise Http404 - n_days = (max_date - min_date).days - if (max_date - min_date) != timedelta(n_days): - n_days += 1 - context['days'] = n_days + context['days'] = self.conf.days mode = self.request.GET.get('mode', 'calendar') day = self.request.GET.get('day', None) kind = self.request.GET.get('kind', 'official') @@ -100,13 +95,11 @@ class FahrplanView(ConferenceRequiredMixin, TemplateView): if day: try: day = int(day) - if day < 0 or day >= n_days: - day = None except ValueError: day = None else: day = None - context['day'] = day + context['day'] = self.conf.day(day) if day else None if kind == 'all': public_fahrplan = None diff --git a/src/plainui/views/utils.py b/src/plainui/views/utils.py index df4f9decff92c37c197cd5d605069a1025863280..6e19387aeb2e8591f8bad5731c3414c4ed267e8b 100644 --- a/src/plainui/views/utils.py +++ b/src/plainui/views/utils.py @@ -198,8 +198,12 @@ def event_filter( filters = {} if day is not None: - filters['schedule_start__gte'] = min_date + timedelta(day) - filters['schedule_start__lt'] = min_date + timedelta(day + 1) + if conf_day := conf.day(day): + filters['schedule_start__gte'] = conf_day.start + filters['schedule_start__lt'] = conf_day.end + else: + # an invalid day has been specified so there can't be any events by definition + return Event.objects.none() if kinds is not None: filters['kind__in'] = kinds if assembly: