Show in-progress multi-day events on the calendar (#33)
Unit Tests / test (push) Successful in 5s

## Summary
Closes #32.
- Keep an event on Upcoming until its end date is over. If `end_time` is set, it drops off once that time passes on the end date, so an in-progress multi-day event stays visible.
- Render the Past tab from `past_events` instead of `future_events`.
- Show the start–end date range when the dates differ.

## Test plan
- [ ] Multi-day event that started yesterday and ends tomorrow appears under Upcoming
- [ ] Event whose end time has already passed appears under Past
- [ ] Future single-day event still appears under Upcoming
- [ ] Past tab lists finished events only
- [ ] `uv run python manage.py test schasite.tests.CalendarEventTests`

Reviewed-on: #33
This commit was merged in pull request #33.
This commit is contained in:
westfarn committed 2026-10-06 11:41:16 -07:00
1 parent 531d97f3fc
commit 783712972b
3 files changed
+81 -15

No files matched your search

+21 -6
View File
@@ -77,17 +77,32 @@ class CalendarEvent(TimeInfoBase):
def has_date(self):
return not self.start_date is None
def _has_ended(self, now=None):
"""True once the event is over. In-progress multi-day events stay open
through end_date, and through end_time on that date when one is set.
"""
if not self.has_date():
return None
if now is None:
now = datetime.datetime.now()
end_date = self.end_date or self.start_date
if self.end_time:
return now >= datetime.datetime.combine(end_date, self.end_time)
return now.date() > end_date
def past_event(self):
if self.has_date():
return self.start_date <= datetime.datetime.now().date()
else:
ended = self._has_ended()
if ended is None:
return False
return ended
def future_event(self):
if self.has_date():
return self.start_date > datetime.datetime.now().date()
else:
"""Upcoming or currently in progress."""
ended = self._has_ended()
if ended is None:
return False
return not ended
def no_date(self):
return not self.has_date()
+3 -3
View File
@@ -114,7 +114,7 @@
</div>
<div class="col-md-8">
<div class="mb-3">
<p class="mb-1"><i class="bi bi-calendar-event text-success me-2"></i><strong>Date:</strong> {{ event.start_date }}</p>
<p class="mb-1"><i class="bi bi-calendar-event text-success me-2"></i><strong>Date:</strong> {% if event.end_date and event.end_date != event.start_date %}{{ event.start_date }} – {{ event.end_date }}{% else %}{{ event.start_date }}{% endif %}</p>
{% if event.time_range_display %}
<p class="mb-1"><i class="bi bi-clock text-success me-2"></i><strong>Time:</strong> {{ event.time_range_display }}</p>
{% endif %}
@@ -144,7 +144,7 @@
<div class="tab-pane fade" id="past" role="tabpanel">
<div class="row g-4">
<!-- Past Event 1 -->
{% for event in future_events %}
{% for event in past_events %}
<div class="col-lg-6">
<div class="card h-100 shadow-sm">
<div class="card-header bg-secondary text-white">
@@ -160,7 +160,7 @@
</div>
<div class="col-md-8">
<div class="mb-3">
<p class="mb-1"><i class="bi bi-calendar-event text-secondary me-2"></i><strong>Date:</strong> {{ event.start_date }}</p>
<p class="mb-1"><i class="bi bi-calendar-event text-secondary me-2"></i><strong>Date:</strong> {% if event.end_date and event.end_date != event.start_date %}{{ event.start_date }} – {{ event.end_date }}{% else %}{{ event.start_date }}{% endif %}</p>
{% if event.time_range_display %}
<p class="mb-1"><i class="bi bi-clock text-secondary me-2"></i><strong>Time:</strong> {{ event.time_range_display }}</p>
{% endif %}
+57 -6
View File
@@ -326,6 +326,47 @@ class CalendarEventTests(TestCase):
self.assertFalse(undated.past_event())
self.assertFalse(undated.future_event())
def test_in_progress_multiday_event_counts_as_upcoming(self):
today = datetime.date.today()
ongoing = CalendarEvent.objects.create(
event_name="Block Party",
start_date=today - datetime.timedelta(days=1),
end_date=today + datetime.timedelta(days=1),
)
ended = CalendarEvent.objects.create(
event_name="Finished Sale",
start_date=today - datetime.timedelta(days=4),
end_date=today - datetime.timedelta(days=1),
)
self.assertTrue(ongoing.future_event())
self.assertFalse(ongoing.past_event())
self.assertTrue(ended.past_event())
self.assertFalse(ended.future_event())
def test_end_time_hides_event_only_after_it_finishes(self):
now = datetime.datetime.now()
still_going = now + datetime.timedelta(hours=2)
already_over = now - datetime.timedelta(hours=1)
current = CalendarEvent.objects.create(
event_name="Still Going",
start_date=(now - datetime.timedelta(days=1)).date(),
end_date=still_going.date(),
end_time=still_going.time().replace(microsecond=0),
)
finished = CalendarEvent.objects.create(
event_name="Already Over",
start_date=(already_over - datetime.timedelta(days=1)).date(),
end_date=already_over.date(),
end_time=already_over.time().replace(microsecond=0),
)
self.assertTrue(current.future_event())
self.assertFalse(current.past_event())
self.assertTrue(finished.past_event())
self.assertFalse(finished.future_event())
def test_time_range_display(self):
event = CalendarEvent.objects.create(
event_name="Timed Event",
@@ -346,23 +387,33 @@ class CalendarEventTests(TestCase):
def test_calendar_page_splits_past_and_future(self):
today = datetime.date.today()
CalendarEvent.objects.create(
event_name="Past Event",
event_name="Archived Picnic",
start_date=today - datetime.timedelta(days=3),
)
CalendarEvent.objects.create(
event_name="Future Event",
start_date=today + datetime.timedelta(days=3),
)
CalendarEvent.objects.create(
event_name="Ongoing Event",
start_date=today - datetime.timedelta(days=1),
end_date=today + datetime.timedelta(days=1),
)
response = self.client.get(reverse("calendar2"))
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.context["past_events"]), 1)
self.assertEqual(len(response.context["future_events"]), 1)
self.assertEqual(response.context["past_events"][0].event_name, "Past Event")
self.assertEqual(
response.context["future_events"][0].event_name, "Future Event"
)
self.assertEqual(len(response.context["future_events"]), 2)
self.assertEqual(response.context["past_events"][0].event_name, "Archived Picnic")
future_names = {event.event_name for event in response.context["future_events"]}
self.assertEqual(future_names, {"Future Event", "Ongoing Event"})
content = response.content.decode()
upcoming_html, past_html = content.split('id="past" role="tabpanel"', 1)
self.assertIn("Ongoing Event", upcoming_html)
self.assertNotIn("Ongoing Event", past_html)
self.assertIn("Archived Picnic", past_html)
self.assertNotIn("Archived Picnic", upcoming_html)
@override_settings(