Add start/end time fields to CalendarEvent (#28) (#29)
Unit Tests / test (push) Successful in 3s

## Summary
Closes #28.

- Add optional `start_time` / `end_time` (`TimeField`) on `CalendarEvent`
- Expose times in Django admin list + form
- Replace hardcoded calendar template times with `time_range_display`
- Add migration `0013` and unit coverage for time formatting

## Test plan
- [ ] `python manage.py migrate`
- [ ] `python manage.py test schasite.tests.CalendarEventTests`
- [ ] In admin, edit an event with start/end times and confirm calendar page shows them
- [ ] Confirm events without times hide the Time rowReviewed-on: #29
This commit was merged in pull request #29.
This commit is contained in:
2026-08-04 11:00:24 -07:00
parent 04c3ac913c
commit f37ff3e87b
6 changed files with 80 additions and 2 deletions
+13
View File
@@ -165,10 +165,23 @@ class CalendarEventAdmin(admin.ModelAdmin):
list_display = [ list_display = [
"event_name", "event_name",
"start_date", "start_date",
"start_time",
"end_date", "end_date",
"end_time",
"coordinator_email", "coordinator_email",
"event_link_name", "event_link_name",
] ]
fields = [
"event_name",
"start_date",
"start_time",
"end_date",
"end_time",
"location_name",
"coordinator_email",
"event_link_name",
"event_url",
]
class AddressModelAdmin(admin.ModelAdmin): class AddressModelAdmin(admin.ModelAdmin):
@@ -0,0 +1,23 @@
# Generated by Django 6.0.7 on 2026-08-04 17:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('schasite', '0012_alter_payments_date'),
]
operations = [
migrations.AddField(
model_name='calendarevent',
name='end_time',
field=models.TimeField(blank=True, null=True),
),
migrations.AddField(
model_name='calendarevent',
name='start_time',
field=models.TimeField(blank=True, null=True),
),
]
+15
View File
@@ -66,6 +66,8 @@ class CalendarEvent(TimeInfoBase):
event_name = models.CharField(max_length=256) event_name = models.CharField(max_length=256)
start_date = models.DateField(blank=True, null=True) start_date = models.DateField(blank=True, null=True)
end_date = models.DateField(blank=True, null=True) end_date = models.DateField(blank=True, null=True)
start_time = models.TimeField(blank=True, null=True)
end_time = models.TimeField(blank=True, null=True)
location_name = models.CharField(max_length=256, blank=True, null=True) location_name = models.CharField(max_length=256, blank=True, null=True)
coordinator_email = models.EmailField(max_length=256, blank=True, null=True) coordinator_email = models.EmailField(max_length=256, blank=True, null=True)
event_link_name = models.CharField(max_length=64, blank=True, null=True) event_link_name = models.CharField(max_length=64, blank=True, null=True)
@@ -90,6 +92,19 @@ class CalendarEvent(TimeInfoBase):
def no_date(self): def no_date(self):
return not self.has_date() return not self.has_date()
def time_range_display(self):
"""Format start/end times for templates, e.g. '11:00 AM - 3:00 PM'."""
from django.utils.formats import time_format
if not self.start_time and not self.end_time:
return ""
start = time_format(self.start_time, "g:i A") if self.start_time else ""
end = time_format(self.end_time, "g:i A") if self.end_time else ""
if start and end:
return f"{start} - {end}"
return start or end
class CalendarEventAddressModel(TimeInfoBase): class CalendarEventAddressModel(TimeInfoBase):
calendar_event = models.OneToOneField(CalendarEvent, on_delete=models.CASCADE) calendar_event = models.OneToOneField(CalendarEvent, on_delete=models.CASCADE)
@@ -23,6 +23,9 @@
{% else %} {% else %}
<p>Date: {{ event.start_date }} - {{ event.end_date }}</p> <p>Date: {{ event.start_date }} - {{ event.end_date }}</p>
{% endif %} {% endif %}
{% if event.time_range_display %}
<p>Time: {{ event.time_range_display }}</p>
{% endif %}
<p>Location: {{ event.location_name }} - {{ event.calendareventaddressmodel }}</p> <p>Location: {{ event.location_name }} - {{ event.calendareventaddressmodel }}</p>
{% if event.coordinator_email %} {% if event.coordinator_email %}
<p>Coordinator: {{ event.coordinator_email }}</p> <p>Coordinator: {{ event.coordinator_email }}</p>
@@ -44,6 +47,9 @@
{% else %} {% else %}
<p>Date: {{ event.start_date }} - {{ event.end_date }}</p> <p>Date: {{ event.start_date }} - {{ event.end_date }}</p>
{% endif %} {% endif %}
{% if event.time_range_display %}
<p>Time: {{ event.time_range_display }}</p>
{% endif %}
<p>Location: {{ event.location_name }} - {{ event.calendareventaddressmodel }}</p> <p>Location: {{ event.location_name }} - {{ event.calendareventaddressmodel }}</p>
{% if event.coordinator_email %} {% if event.coordinator_email %}
<p>Coordinator: {{ event.coordinator_email }}</p> <p>Coordinator: {{ event.coordinator_email }}</p>
+6 -2
View File
@@ -115,7 +115,9 @@
<div class="col-md-8"> <div class="col-md-8">
<div class="mb-3"> <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> {{ event.start_date }}</p>
<p class="mb-1"><i class="bi bi-clock text-success me-2"></i><strong>Time:</strong> 11:00 AM - 3:00 PM</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 %}
<p class="mb-1"><i class="bi bi-geo-alt text-success me-2"></i><strong>Location:</strong> {{ event.location_name }}</p> <p class="mb-1"><i class="bi bi-geo-alt text-success me-2"></i><strong>Location:</strong> {{ event.location_name }}</p>
<p class="mb-1"><i class="bi bi-house text-success me-2"></i><strong>Address:</strong> </p> <p class="mb-1"><i class="bi bi-house text-success me-2"></i><strong>Address:</strong> </p>
<p class="mb-1"><i class="bi bi-person text-success me-2"></i><strong>Coordinator:</strong> {{ event.coordinator_name }}</p> <p class="mb-1"><i class="bi bi-person text-success me-2"></i><strong>Coordinator:</strong> {{ event.coordinator_name }}</p>
@@ -159,7 +161,9 @@
<div class="col-md-8"> <div class="col-md-8">
<div class="mb-3"> <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> {{ event.start_date }}</p>
<p class="mb-1"><i class="bi bi-clock text-secondary me-2"></i><strong>Time:</strong> 9:00 AM - 12:00 PM</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 %}
<p class="mb-1"><i class="bi bi-geo-alt text-secondary me-2"></i><strong>Location:</strong> {{ event.location_name }}</p> <p class="mb-1"><i class="bi bi-geo-alt text-secondary me-2"></i><strong>Location:</strong> {{ event.location_name }}</p>
<p class="mb-1"><i class="bi bi-house text-secondary me-2"></i><strong>Address:</strong> </p> <p class="mb-1"><i class="bi bi-house text-secondary me-2"></i><strong>Address:</strong> </p>
<p class="mb-1"><i class="bi bi-person text-secondary me-2"></i><strong>Coordinator:</strong> {{ event.coordinator_name }}</p> <p class="mb-1"><i class="bi bi-person text-secondary me-2"></i><strong>Coordinator:</strong> {{ event.coordinator_name }}</p>
+17
View File
@@ -321,6 +321,23 @@ class CalendarEventTests(TestCase):
self.assertFalse(undated.past_event()) self.assertFalse(undated.past_event())
self.assertFalse(undated.future_event()) self.assertFalse(undated.future_event())
def test_time_range_display(self):
event = CalendarEvent.objects.create(
event_name="Timed Event",
start_time=datetime.time(11, 0),
end_time=datetime.time(15, 0),
)
self.assertEqual(event.time_range_display(), "11:00 AM - 3:00 PM")
start_only = CalendarEvent.objects.create(
event_name="Start Only",
start_time=datetime.time(9, 30),
)
self.assertEqual(start_only.time_range_display(), "9:30 AM")
no_time = CalendarEvent.objects.create(event_name="No Time")
self.assertEqual(no_time.time_range_display(), "")
def test_calendar_page_splits_past_and_future(self): def test_calendar_page_splits_past_and_future(self):
today = datetime.date.today() today = datetime.date.today()
CalendarEvent.objects.create( CalendarEvent.objects.create(