From 1b214dae091607ec09218dcdc3674603c94602df Mon Sep 17 00:00:00 2001 From: Austin Riba Date: Fri, 27 Feb 2026 13:58:49 -0800 Subject: [PATCH 01/39] Basic calendar template in tom_common --- tom_common/calendar.py | 61 +++++++++++++++++ .../templates/tom_common/calendar_page.html | 6 ++ .../tom_common/partials/calendar.html | 65 +++++++++++++++++++ tom_common/urls.py | 2 + 4 files changed, 134 insertions(+) create mode 100644 tom_common/calendar.py create mode 100644 tom_common/templates/tom_common/calendar_page.html create mode 100644 tom_common/templates/tom_common/partials/calendar.html diff --git a/tom_common/calendar.py b/tom_common/calendar.py new file mode 100644 index 000000000..58f4fd2e6 --- /dev/null +++ b/tom_common/calendar.py @@ -0,0 +1,61 @@ +import calendar as cal_module +from datetime import date + +from django.urls import path +from django.utils import timezone +from django.shortcuts import render + +DAY_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] + + +def render_calendar(request): + now = timezone.now() + today = now.date() + + month = int(request.GET.get("month", now.month)) + year = int(request.GET.get("year", now.year)) + + month = max(1, min(12, month)) + + # Sunday is 6 in python calendar for some reason + calendar = cal_module.Calendar(firstweekday=6) + weeks = calendar.monthdatescalendar(year, month) + + # Previous month/year + if month == 1: + prev_month, prev_year = 12, year - 1 + else: + prev_month, prev_year = month - 1, year + + # Next month/year + if month == 12: + next_month, next_year = 1, year + 1 + else: + next_month, next_year = month + 1, year + + month_name = date(year, month, 1).strftime("%B %Y") + + context = { + "month": month, + "year": year, + "month_name": month_name, + "weeks": weeks, + "day_names": DAY_NAMES, + "today": today, + "prev_month": prev_month, + "prev_year": prev_year, + "next_month": next_month, + "next_year": next_year, + } + + if request.htmx: + template = "tom_common/partials/calendar.html" + else: + template = "tom_common/calendar_page.html" + + return render(request, template, context) + + +urlpatterns = [ + path("", render_calendar, name="calendar"), +] diff --git a/tom_common/templates/tom_common/calendar_page.html b/tom_common/templates/tom_common/calendar_page.html new file mode 100644 index 000000000..b37bf649b --- /dev/null +++ b/tom_common/templates/tom_common/calendar_page.html @@ -0,0 +1,6 @@ +{% extends 'tom_common/base.html' %} +{% block title %}Calendar{% endblock %} +{% block content %} +

Calendar

+ {% include 'tom_common/partials/calendar.html' %} +{% endblock %} diff --git a/tom_common/templates/tom_common/partials/calendar.html b/tom_common/templates/tom_common/partials/calendar.html new file mode 100644 index 000000000..20bbcfe7b --- /dev/null +++ b/tom_common/templates/tom_common/partials/calendar.html @@ -0,0 +1,65 @@ + + +
+
+ +

{{ month_name }}

+ +
+ +
+ {% for name in day_names %} +
{{ name }}
+ {% endfor %} + + {% for week in weeks %} + {% for day in week %} + {% if day.month != month %} +
+ {{ day.day }} +
+ {% else %} +
+ {{ day.day }} +
+ {% endif %} + {% endfor %} + {% endfor %} + +
+
diff --git a/tom_common/urls.py b/tom_common/urls.py index b53b728f2..c1a3b04c4 100644 --- a/tom_common/urls.py +++ b/tom_common/urls.py @@ -27,6 +27,7 @@ from tom_common.api_views import GroupViewSet from tom_common.views import UserListView, UserPasswordChangeView, UserCreateView, UserDeleteView, UserUpdateView from tom_common.views import CommentDeleteView, GroupCreateView, GroupUpdateView, GroupDeleteView, UserProfileView +from tom_common.calendar import urlpatterns as calendar_urlpatterns from tom_common.views import robots_txt from .api_router import collect_api_urls, SharedAPIRootRouter # DRF routers are setup in each INSTALL_APPS url.py @@ -49,6 +50,7 @@ path('robots.txt', robots_txt, name='robots_txt'), path('targets/', include('tom_targets.urls', namespace='targets')), path('alerts/', include('tom_alerts.urls', namespace='alerts')), + path('calendar/', include((calendar_urlpatterns, 'calendar'), namespace='calendar')), path('comments/', include('django_comments.urls')), path('catalogs/', include('tom_catalogs.urls')), path('observations/', include('tom_observations.urls', namespace='observations')), From 84ede3446da078654ae8e73337a0d402dcdb05bb Mon Sep 17 00:00:00 2001 From: Austin Riba Date: Fri, 27 Feb 2026 14:16:01 -0800 Subject: [PATCH 02/39] Move calendar to own app --- tom_base/settings.py | 1 + tom_calendar/__init__.py | 0 tom_calendar/admin.py | 3 +++ tom_calendar/apps.py | 5 +++++ tom_calendar/migrations/__init__.py | 0 tom_calendar/models.py | 3 +++ .../templates/tom_calendar}/calendar_page.html | 2 +- .../templates/tom_calendar}/partials/calendar.html | 0 tom_calendar/tests.py | 3 +++ tom_calendar/urls.py | 9 +++++++++ tom_common/calendar.py => tom_calendar/views.py | 10 ++-------- tom_common/urls.py | 3 +-- 12 files changed, 28 insertions(+), 11 deletions(-) create mode 100644 tom_calendar/__init__.py create mode 100644 tom_calendar/admin.py create mode 100644 tom_calendar/apps.py create mode 100644 tom_calendar/migrations/__init__.py create mode 100644 tom_calendar/models.py rename {tom_common/templates/tom_common => tom_calendar/templates/tom_calendar}/calendar_page.html (70%) rename {tom_common/templates/tom_common => tom_calendar/templates/tom_calendar}/partials/calendar.html (100%) create mode 100644 tom_calendar/tests.py create mode 100644 tom_calendar/urls.py rename tom_common/calendar.py => tom_calendar/views.py (86%) diff --git a/tom_base/settings.py b/tom_base/settings.py index 4d5bc6e82..faa3c8295 100644 --- a/tom_base/settings.py +++ b/tom_base/settings.py @@ -62,6 +62,7 @@ 'tom_observations', 'tom_dataproducts', 'tom_dataservices', + 'tom_calendar', ] SITE_ID = 1 diff --git a/tom_calendar/__init__.py b/tom_calendar/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tom_calendar/admin.py b/tom_calendar/admin.py new file mode 100644 index 000000000..4185d360e --- /dev/null +++ b/tom_calendar/admin.py @@ -0,0 +1,3 @@ +# from django.contrib import admin + +# Register your models here. diff --git a/tom_calendar/apps.py b/tom_calendar/apps.py new file mode 100644 index 000000000..b18b30bb7 --- /dev/null +++ b/tom_calendar/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class TomCalendarConfig(AppConfig): + name = 'tom_calendar' diff --git a/tom_calendar/migrations/__init__.py b/tom_calendar/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tom_calendar/models.py b/tom_calendar/models.py new file mode 100644 index 000000000..0b4331b36 --- /dev/null +++ b/tom_calendar/models.py @@ -0,0 +1,3 @@ +# from django.db import models + +# Create your models here. diff --git a/tom_common/templates/tom_common/calendar_page.html b/tom_calendar/templates/tom_calendar/calendar_page.html similarity index 70% rename from tom_common/templates/tom_common/calendar_page.html rename to tom_calendar/templates/tom_calendar/calendar_page.html index b37bf649b..df015078d 100644 --- a/tom_common/templates/tom_common/calendar_page.html +++ b/tom_calendar/templates/tom_calendar/calendar_page.html @@ -2,5 +2,5 @@ {% block title %}Calendar{% endblock %} {% block content %}

Calendar

- {% include 'tom_common/partials/calendar.html' %} + {% include 'tom_calendar/partials/calendar.html' %} {% endblock %} diff --git a/tom_common/templates/tom_common/partials/calendar.html b/tom_calendar/templates/tom_calendar/partials/calendar.html similarity index 100% rename from tom_common/templates/tom_common/partials/calendar.html rename to tom_calendar/templates/tom_calendar/partials/calendar.html diff --git a/tom_calendar/tests.py b/tom_calendar/tests.py new file mode 100644 index 000000000..a79ca8be5 --- /dev/null +++ b/tom_calendar/tests.py @@ -0,0 +1,3 @@ +# from django.test import TestCase + +# Create your tests here. diff --git a/tom_calendar/urls.py b/tom_calendar/urls.py new file mode 100644 index 000000000..096f7d374 --- /dev/null +++ b/tom_calendar/urls.py @@ -0,0 +1,9 @@ +from django.urls import path + +from .views import render_calendar + +app_name = 'tom_calendar' + +urlpatterns = [ + path("", render_calendar, name="calendar"), +] diff --git a/tom_common/calendar.py b/tom_calendar/views.py similarity index 86% rename from tom_common/calendar.py rename to tom_calendar/views.py index 58f4fd2e6..315f3759d 100644 --- a/tom_common/calendar.py +++ b/tom_calendar/views.py @@ -1,7 +1,6 @@ import calendar as cal_module from datetime import date -from django.urls import path from django.utils import timezone from django.shortcuts import render @@ -49,13 +48,8 @@ def render_calendar(request): } if request.htmx: - template = "tom_common/partials/calendar.html" + template = "tom_calendar/partials/calendar.html" else: - template = "tom_common/calendar_page.html" + template = "tom_calendar/calendar_page.html" return render(request, template, context) - - -urlpatterns = [ - path("", render_calendar, name="calendar"), -] diff --git a/tom_common/urls.py b/tom_common/urls.py index c1a3b04c4..24f8d9cc4 100644 --- a/tom_common/urls.py +++ b/tom_common/urls.py @@ -27,7 +27,6 @@ from tom_common.api_views import GroupViewSet from tom_common.views import UserListView, UserPasswordChangeView, UserCreateView, UserDeleteView, UserUpdateView from tom_common.views import CommentDeleteView, GroupCreateView, GroupUpdateView, GroupDeleteView, UserProfileView -from tom_common.calendar import urlpatterns as calendar_urlpatterns from tom_common.views import robots_txt from .api_router import collect_api_urls, SharedAPIRootRouter # DRF routers are setup in each INSTALL_APPS url.py @@ -50,7 +49,7 @@ path('robots.txt', robots_txt, name='robots_txt'), path('targets/', include('tom_targets.urls', namespace='targets')), path('alerts/', include('tom_alerts.urls', namespace='alerts')), - path('calendar/', include((calendar_urlpatterns, 'calendar'), namespace='calendar')), + path('calendar/', include('tom_calendar.urls', namespace='calendar')), path('comments/', include('django_comments.urls')), path('catalogs/', include('tom_catalogs.urls')), path('observations/', include('tom_observations.urls', namespace='observations')), From f80a670bc0cfc4fa967715e4a7df5710d2718391 Mon Sep 17 00:00:00 2001 From: Austin Riba Date: Fri, 27 Feb 2026 15:01:48 -0800 Subject: [PATCH 03/39] Add calendarEvent model --- tom_calendar/admin.py | 6 ++++-- tom_calendar/migrations/0001_initial.py | 27 +++++++++++++++++++++++++ tom_calendar/models.py | 22 ++++++++++++++++++-- tom_calendar/views.py | 8 ++++++++ 4 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 tom_calendar/migrations/0001_initial.py diff --git a/tom_calendar/admin.py b/tom_calendar/admin.py index 4185d360e..b1e15c98b 100644 --- a/tom_calendar/admin.py +++ b/tom_calendar/admin.py @@ -1,3 +1,5 @@ -# from django.contrib import admin +from django.contrib import admin -# Register your models here. +from .models import CalendarEvent + +admin.site.register(CalendarEvent) diff --git a/tom_calendar/migrations/0001_initial.py b/tom_calendar/migrations/0001_initial.py new file mode 100644 index 000000000..076cbea55 --- /dev/null +++ b/tom_calendar/migrations/0001_initial.py @@ -0,0 +1,27 @@ +# Generated by Django 4.2.27 on 2026-02-27 22:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='CalendarEvent', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=200)), + ('description', models.TextField(blank=True, default='')), + ('start_time', models.DateTimeField()), + ('end_time', models.DateTimeField()), + ('url', models.URLField(blank=True, default='')), + ('created', models.DateTimeField(auto_now_add=True)), + ('modified', models.DateTimeField(auto_now=True)), + ], + ), + ] diff --git a/tom_calendar/models.py b/tom_calendar/models.py index 0b4331b36..660788848 100644 --- a/tom_calendar/models.py +++ b/tom_calendar/models.py @@ -1,3 +1,21 @@ -# from django.db import models +from django.db import models -# Create your models here. + +class CalendarEvent(models.Model): + """ + Class representing an event in the calendar. + + Other applications can create calendar events by creating instances of this class. + + """ + title = models.CharField(max_length=200) + description = models.TextField(blank=True, default="") + start_time = models.DateTimeField() + end_time = models.DateTimeField() + url = models.URLField(blank=True, default="") + """The URL a user can visit for more information or associated object.""" + created = models.DateTimeField(auto_now_add=True) + modified = models.DateTimeField(auto_now=True) + + def __str__(self): + return self.title diff --git a/tom_calendar/views.py b/tom_calendar/views.py index 315f3759d..2f19e88ef 100644 --- a/tom_calendar/views.py +++ b/tom_calendar/views.py @@ -4,6 +4,8 @@ from django.utils import timezone from django.shortcuts import render +from .models import CalendarEvent + DAY_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] @@ -34,6 +36,11 @@ def render_calendar(request): month_name = date(year, month, 1).strftime("%B %Y") + events = CalendarEvent.objects.filter( + start_time__date__lte=weeks[-1][-1], + end_time__date__gte=weeks[0][0], + ) + context = { "month": month, "year": year, @@ -45,6 +52,7 @@ def render_calendar(request): "prev_year": prev_year, "next_month": next_month, "next_year": next_year, + "events": events, } if request.htmx: From 6700bc41fdfff46c1fcd6b9ce31015b0217a0a89 Mon Sep 17 00:00:00 2001 From: Austin Riba Date: Fri, 27 Feb 2026 15:19:58 -0800 Subject: [PATCH 04/39] Add events to calendar --- .../tom_calendar/partials/calendar.html | 30 ++++++++++++++++--- tom_calendar/views.py | 21 +++++++++---- 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/tom_calendar/templates/tom_calendar/partials/calendar.html b/tom_calendar/templates/tom_calendar/partials/calendar.html index 20bbcfe7b..e56d9d9f0 100644 --- a/tom_calendar/templates/tom_calendar/partials/calendar.html +++ b/tom_calendar/templates/tom_calendar/partials/calendar.html @@ -23,6 +23,10 @@ border-radius: 50%; margin-bottom: 0.15rem; } + .cal-event { + font-size: 0.75rem; + line-height: 1.3; + }
@@ -49,13 +53,31 @@

{{ month_name }}

{% for week in weeks %} {% for day in week %} - {% if day.month != month %} + {% if day.date.month != month %}
- {{ day.day }} + {{ day.date.day }} + {% for event in day.events %} +
+ {% if event.url %} + {{ event.title }} + {% else %} + {{ event.title }} + {% endif %} +
+ {% endfor %}
{% else %} -
- {{ day.day }} +
+ {{ day.date.day }} + {% for event in day.events %} +
+ {% if event.url %} + {{ event.title }} + {% else %} + {{ event.title }} + {% endif %} +
+ {% endfor %}
{% endif %} {% endfor %} diff --git a/tom_calendar/views.py b/tom_calendar/views.py index 2f19e88ef..cce08a375 100644 --- a/tom_calendar/views.py +++ b/tom_calendar/views.py @@ -20,15 +20,12 @@ def render_calendar(request): # Sunday is 6 in python calendar for some reason calendar = cal_module.Calendar(firstweekday=6) - weeks = calendar.monthdatescalendar(year, month) - # Previous month/year if month == 1: prev_month, prev_year = 12, year - 1 else: prev_month, prev_year = month - 1, year - # Next month/year if month == 12: next_month, next_year = 1, year + 1 else: @@ -36,23 +33,37 @@ def render_calendar(request): month_name = date(year, month, 1).strftime("%B %Y") + weeks = calendar.monthdatescalendar(year, month) + + # Fetch all events for this month instead of querying for each day events = CalendarEvent.objects.filter( start_time__date__lte=weeks[-1][-1], end_time__date__gte=weeks[0][0], ) + events = list(events) + weeks_with_events = [ + [ + { + "date": d, + "events": [e for e in events if e.start_time.date() <= d <= e.end_time.date()], + } + for d in week + ] + for week in weeks + ] + context = { "month": month, "year": year, "month_name": month_name, - "weeks": weeks, + "weeks": weeks_with_events, "day_names": DAY_NAMES, "today": today, "prev_month": prev_month, "prev_year": prev_year, "next_month": next_month, "next_year": next_year, - "events": events, } if request.htmx: From abfe9470bd313be625f3e728d4e18dd0fcfb6d89 Mon Sep 17 00:00:00 2001 From: Austin Riba Date: Fri, 27 Feb 2026 15:29:12 -0800 Subject: [PATCH 05/39] Remove some duplicate logic --- .../tom_calendar/partials/calendar.html | 40 ++++++------------- tom_calendar/views.py | 1 + 2 files changed, 13 insertions(+), 28 deletions(-) diff --git a/tom_calendar/templates/tom_calendar/partials/calendar.html b/tom_calendar/templates/tom_calendar/partials/calendar.html index e56d9d9f0..69d9f5028 100644 --- a/tom_calendar/templates/tom_calendar/partials/calendar.html +++ b/tom_calendar/templates/tom_calendar/partials/calendar.html @@ -53,35 +53,19 @@

{{ month_name }}

{% for week in weeks %} {% for day in week %} - {% if day.date.month != month %} -
- {{ day.date.day }} - {% for event in day.events %} -
- {% if event.url %} - {{ event.title }} - {% else %} - {{ event.title }} - {% endif %} -
- {% endfor %} -
- {% else %} -
- {{ day.date.day }} - {% for event in day.events %} -
- {% if event.url %} - {{ event.title }} - {% else %} - {{ event.title }} - {% endif %} -
- {% endfor %} -
- {% endif %} +
+ {{ day.date.day }} + {% for event in day.events %} +
+ {% if event.url %} + {{ event.title }} + {% else %} + {{ event.title }} + {% endif %} +
+ {% endfor %} +
{% endfor %} {% endfor %} -
diff --git a/tom_calendar/views.py b/tom_calendar/views.py index cce08a375..d2e5845ef 100644 --- a/tom_calendar/views.py +++ b/tom_calendar/views.py @@ -46,6 +46,7 @@ def render_calendar(request): [ { "date": d, + "in_current_month": d.month == month, "events": [e for e in events if e.start_time.date() <= d <= e.end_time.date()], } for d in week From 39167196844127e1329b443a8af3ff0e11753aa6 Mon Sep 17 00:00:00 2001 From: Austin Riba Date: Fri, 27 Feb 2026 15:36:47 -0800 Subject: [PATCH 06/39] Clean up rendering logic --- .../tom_calendar/partials/calendar.html | 28 ++++++++++++++----- tom_calendar/views.py | 1 - 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/tom_calendar/templates/tom_calendar/partials/calendar.html b/tom_calendar/templates/tom_calendar/partials/calendar.html index 69d9f5028..251c5c8a0 100644 --- a/tom_calendar/templates/tom_calendar/partials/calendar.html +++ b/tom_calendar/templates/tom_calendar/partials/calendar.html @@ -5,13 +5,19 @@ } .cal-day { min-height: 110px; + background-color: var(--light); + color: var(--secondary); } - .cal-day:hover { - background-color: rgba(0, 0, 0, .04); + .cal-day.is-current-month { + background-color: transparent; + color: inherit; } - .cal-day.other-month:hover { + .cal-day:hover { background-color: rgba(0, 0, 0, .06); } + .cal-day.is-current-month:hover { + background-color: rgba(0, 0, 0, .04); + } .day-num { display: inline-flex; align-items: center; @@ -23,10 +29,18 @@ border-radius: 50%; margin-bottom: 0.15rem; } + .cal-day.is-current-month.today .day-num { + background-color: var(--primary); + color: var(--white); + font-weight: 700; + } .cal-event { font-size: 0.75rem; line-height: 1.3; } + .cal-event a { + color: inherit; + }
@@ -53,12 +67,12 @@

{{ month_name }}

{% for week in weeks %} {% for day in week %} -
- {{ day.date.day }} +
+ {{ day.date.day }} {% for event in day.events %} -
+
{% if event.url %} - {{ event.title }} + {{ event.title }} {% else %} {{ event.title }} {% endif %} diff --git a/tom_calendar/views.py b/tom_calendar/views.py index d2e5845ef..cce08a375 100644 --- a/tom_calendar/views.py +++ b/tom_calendar/views.py @@ -46,7 +46,6 @@ def render_calendar(request): [ { "date": d, - "in_current_month": d.month == month, "events": [e for e in events if e.start_time.date() <= d <= e.end_time.date()], } for d in week From 609418a2bb59635ab8884397d394372c4c728d17 Mon Sep 17 00:00:00 2001 From: Austin Riba Date: Fri, 27 Feb 2026 19:38:08 -0800 Subject: [PATCH 07/39] Add timezone to calendar page --- tom_calendar/templates/tom_calendar/calendar_page.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tom_calendar/templates/tom_calendar/calendar_page.html b/tom_calendar/templates/tom_calendar/calendar_page.html index df015078d..1c2b9930d 100644 --- a/tom_calendar/templates/tom_calendar/calendar_page.html +++ b/tom_calendar/templates/tom_calendar/calendar_page.html @@ -1,6 +1,8 @@ {% extends 'tom_common/base.html' %} +{% load tz %} {% block title %}Calendar{% endblock %} {% block content %} -

Calendar

{% include 'tom_calendar/partials/calendar.html' %} + {% get_current_timezone as TIME_ZONE %} +

The current time zone is {{ TIME_ZONE }}

{% endblock %} From cefc88fe48c39a2f54f4539013caa911e7742272 Mon Sep 17 00:00:00 2001 From: Austin Riba Date: Fri, 27 Feb 2026 21:54:21 -0800 Subject: [PATCH 08/39] Create events --- .../templates/tom_calendar/calendar_page.html | 22 +++++++++ .../tom_calendar/partials/calendar.html | 6 ++- .../tom_calendar/partials/create_event.html | 8 ++++ tom_calendar/urls.py | 3 +- tom_calendar/views.py | 48 +++++++++++++++++-- 5 files changed, 80 insertions(+), 7 deletions(-) create mode 100644 tom_calendar/templates/tom_calendar/partials/create_event.html diff --git a/tom_calendar/templates/tom_calendar/calendar_page.html b/tom_calendar/templates/tom_calendar/calendar_page.html index 1c2b9930d..efedbb896 100644 --- a/tom_calendar/templates/tom_calendar/calendar_page.html +++ b/tom_calendar/templates/tom_calendar/calendar_page.html @@ -5,4 +5,26 @@ {% include 'tom_calendar/partials/calendar.html' %} {% get_current_timezone as TIME_ZONE %}

The current time zone is {{ TIME_ZONE }}

+ + +{% endblock %} +{% block extra_javascript %} + {% endblock %} diff --git a/tom_calendar/templates/tom_calendar/partials/calendar.html b/tom_calendar/templates/tom_calendar/partials/calendar.html index 251c5c8a0..c8b81fb94 100644 --- a/tom_calendar/templates/tom_calendar/partials/calendar.html +++ b/tom_calendar/templates/tom_calendar/partials/calendar.html @@ -67,7 +67,11 @@

{{ month_name }}

{% for week in weeks %} {% for day in week %} -
+
{{ day.date.day }} {% for event in day.events %}
diff --git a/tom_calendar/templates/tom_calendar/partials/create_event.html b/tom_calendar/templates/tom_calendar/partials/create_event.html new file mode 100644 index 000000000..af35af266 --- /dev/null +++ b/tom_calendar/templates/tom_calendar/partials/create_event.html @@ -0,0 +1,8 @@ +{% load bootstrap4 %} +
+ {% csrf_token %} + {% bootstrap_form form %} + {% buttons %} + + {% endbuttons %} +
diff --git a/tom_calendar/urls.py b/tom_calendar/urls.py index 096f7d374..f5009fb45 100644 --- a/tom_calendar/urls.py +++ b/tom_calendar/urls.py @@ -1,9 +1,10 @@ from django.urls import path -from .views import render_calendar +from .views import render_calendar, create_event app_name = 'tom_calendar' urlpatterns = [ path("", render_calendar, name="calendar"), + path("create/", create_event, name="create-event") ] diff --git a/tom_calendar/views.py b/tom_calendar/views.py index cce08a375..cc2488732 100644 --- a/tom_calendar/views.py +++ b/tom_calendar/views.py @@ -1,22 +1,23 @@ +from django_htmx.http import trigger_client_event import calendar as cal_module -from datetime import date +from datetime import date, datetime from django.utils import timezone from django.shortcuts import render +from django import forms from .models import CalendarEvent DAY_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] +DATETIME_INPUT = forms.DateTimeInput(attrs={'type': 'datetime-local'}, format='%Y-%m-%dT%H:%M') def render_calendar(request): now = timezone.now() today = now.date() - month = int(request.GET.get("month", now.month)) - year = int(request.GET.get("year", now.year)) - month = max(1, min(12, month)) + year = int(request.GET.get("year", now.year)) # Sunday is 6 in python calendar for some reason calendar = cal_module.Calendar(firstweekday=6) @@ -32,7 +33,6 @@ def render_calendar(request): next_month, next_year = month + 1, year month_name = date(year, month, 1).strftime("%B %Y") - weeks = calendar.monthdatescalendar(year, month) # Fetch all events for this month instead of querying for each day @@ -72,3 +72,41 @@ def render_calendar(request): template = "tom_calendar/calendar_page.html" return render(request, template, context) + + +class EventForm(forms.ModelForm): + class Meta: + model = CalendarEvent + fields = ['title', 'start_time', 'end_time', 'description', 'url'] + widgets = { + 'start_time': DATETIME_INPUT, + 'end_time': DATETIME_INPUT, + } + + +def create_event(request): + if request.method == "POST": + form = EventForm(request.POST) + if form.is_valid(): + form.save() + response = render_calendar(request) + return trigger_client_event(response, "eventCreated") + else: + response = render(request, "tom_calendar/partials/create_event.html", {"form": form}) + response["HX-Retarget"] = "#cal-modal-body" + response["HX-Reswap"] = "innerHTML" + return response + + else: + try: + date_str = request.GET["date"] + date_obj = datetime.strptime(date_str, "%Y-%m-%d").date() + initial_data = { + "start_time": datetime.combine(date_obj, datetime.min.time()), + "end_time": datetime.combine(date_obj, datetime.max.time()), + } + form = EventForm(initial=initial_data) + except ValueError: + form = EventForm() + + return render(request, "tom_calendar/partials/create_event.html", {"form": form}) From 5958fc0b4129b946bd4416c208f9a006f642db52 Mon Sep 17 00:00:00 2001 From: Austin Riba Date: Sat, 28 Feb 2026 09:10:13 -0800 Subject: [PATCH 09/39] Add moon to calendar --- .../tom_calendar/partials/calendar.html | 6 +++ tom_calendar/views.py | 38 ++++++++++++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/tom_calendar/templates/tom_calendar/partials/calendar.html b/tom_calendar/templates/tom_calendar/partials/calendar.html index c8b81fb94..0e22f9825 100644 --- a/tom_calendar/templates/tom_calendar/partials/calendar.html +++ b/tom_calendar/templates/tom_calendar/partials/calendar.html @@ -34,6 +34,11 @@ color: var(--white); font-weight: 700; } + .moon-phase { + font-size: 0.9rem; + cursor: default; + float: right; + } .cal-event { font-size: 0.75rem; line-height: 1.3; @@ -73,6 +78,7 @@

{{ month_name }}

hx-on::after-request="$('#cal-modal').modal('show');" > {{ day.date.day }} + {{ day.moon.emoji }} {% for event in day.events %}
{% if event.url %} diff --git a/tom_calendar/views.py b/tom_calendar/views.py index cc2488732..49641101e 100644 --- a/tom_calendar/views.py +++ b/tom_calendar/views.py @@ -1,17 +1,50 @@ -from django_htmx.http import trigger_client_event +from typing import Self +from dataclasses import dataclass import calendar as cal_module -from datetime import date, datetime +import math +from datetime import date, datetime, time + +from astropy.time import Time +from astropy.coordinates import get_body, get_sun +import astropy.units as u from django.utils import timezone from django.shortcuts import render from django import forms +from django_htmx.http import trigger_client_event from .models import CalendarEvent DAY_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] +MOON_EMOJIS = ["🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘"] DATETIME_INPUT = forms.DateTimeInput(attrs={'type': 'datetime-local'}, format='%Y-%m-%dT%H:%M') +@dataclass +class MoonPhase: + illumination: float + emoji: str + + @classmethod + def from_date(cls, date: date) -> Self: + d = datetime.combine(date, time(12, 0)) + t = Time(d) + moon = get_body("moon", t) + sun = get_sun(t) + moon_lon = moon.geocentricmeanecliptic.lon + sun_lon = sun.geocentricmeanecliptic.lon + phase_angle = (moon_lon - sun_lon).wrap_at(360 * u.deg).deg + + # Illumination fraction from elongation + # 0 (new) -> 0.0, 90 (quarter) -> 0.5, 180 (full) -> 1.0 + illumination = (1 - math.cos(math.radians(phase_angle))) / 2 + + # 8 45deg slices map to the phase emoji + emoji = MOON_EMOJIS[int(phase_angle / 45) % 8] + + return cls(illumination, emoji) + + def render_calendar(request): now = timezone.now() today = now.date() @@ -46,6 +79,7 @@ def render_calendar(request): [ { "date": d, + "moon": MoonPhase.from_date(d), "events": [e for e in events if e.start_time.date() <= d <= e.end_time.date()], } for d in week From 439b6957e3aa30bcdcd488e15854306455a9d39f Mon Sep 17 00:00:00 2001 From: Austin Riba Date: Sat, 28 Feb 2026 13:16:49 -0800 Subject: [PATCH 10/39] Style all day events --- tom_calendar/models.py | 16 ++++++++++ .../tom_calendar/partials/calendar.html | 29 +++++++++++++++++++ tom_calendar/views.py | 10 ++++++- 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/tom_calendar/models.py b/tom_calendar/models.py index 660788848..79f0af57c 100644 --- a/tom_calendar/models.py +++ b/tom_calendar/models.py @@ -1,5 +1,17 @@ from django.db import models +ALL_DAY_COLORS = [ + 'var(--blue)', + 'var(--indigo)', + 'var(--purple)', + 'var(--pink)', + 'var(--red)', + 'var(--orange)', + 'var(--green)', + 'var(--teal)', + 'var(--cyan)', +] + class CalendarEvent(models.Model): """ @@ -19,3 +31,7 @@ class CalendarEvent(models.Model): def __str__(self): return self.title + + @property + def color(self) -> str: + return ALL_DAY_COLORS[self.pk % len(ALL_DAY_COLORS)] diff --git a/tom_calendar/templates/tom_calendar/partials/calendar.html b/tom_calendar/templates/tom_calendar/partials/calendar.html index 0e22f9825..dfb12633d 100644 --- a/tom_calendar/templates/tom_calendar/partials/calendar.html +++ b/tom_calendar/templates/tom_calendar/partials/calendar.html @@ -42,10 +42,30 @@ .cal-event { font-size: 0.75rem; line-height: 1.3; + cursor: pointer; } .cal-event a { color: inherit; } + .cal-event-all-day { + display: block; + border-radius: 3px; + padding: 1px 5px; + margin-bottom: 2px; + color: #fff !important; + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + cursor: pointer; + } + .cal-event-all-day a { + color: #fff !important; + text-decoration: none; + } + .cal-event-all-day a:hover { + text-decoration: underline; + }
@@ -79,6 +99,15 @@

{{ month_name }}

> {{ day.date.day }} {{ day.moon.emoji }} + {% for event in day.all_day_events %} +
+ {% if event.url %} + {{ event.title }} + {% else %} + {{ event.title }} + {% endif %} +
+ {% endfor %} {% for event in day.events %}
{% if event.url %} diff --git a/tom_calendar/views.py b/tom_calendar/views.py index 49641101e..34f1fbff0 100644 --- a/tom_calendar/views.py +++ b/tom_calendar/views.py @@ -80,7 +80,15 @@ def render_calendar(request): { "date": d, "moon": MoonPhase.from_date(d), - "events": [e for e in events if e.start_time.date() <= d <= e.end_time.date()], + "all_day_events": [ + e for e in events + if e.start_time.date() <= d <= e.end_time.date() + and e.start_time.date() != e.end_time.date() + ], + "events": [ + e for e in events + if e.start_time.date() == e.end_time.date() == d + ], } for d in week ] From 38d5e2ab754896cd4109634a179484759dc47248 Mon Sep 17 00:00:00 2001 From: Austin Riba Date: Sat, 28 Feb 2026 13:44:40 -0800 Subject: [PATCH 11/39] Styling for single day events --- .../tom_calendar/partials/calendar.html | 42 +++++++++++++++---- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/tom_calendar/templates/tom_calendar/partials/calendar.html b/tom_calendar/templates/tom_calendar/partials/calendar.html index dfb12633d..4565b9bfd 100644 --- a/tom_calendar/templates/tom_calendar/partials/calendar.html +++ b/tom_calendar/templates/tom_calendar/partials/calendar.html @@ -47,6 +47,28 @@ .cal-event a { color: inherit; } + .cal-event-timed { + display: flex; + align-items: center; + gap: 2px; + } + .cal-event-timed .cal-event-title { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .cal-event-timed .cal-event-bullet { + flex-shrink: 0; + opacity: 0.5; + } + .cal-event-timed .cal-event-time { + flex-shrink: 0; + white-space: nowrap; + opacity: 0.55; + font-size: 0.68rem; + } .cal-event-all-day { display: block; border-radius: 3px; @@ -102,19 +124,23 @@

{{ month_name }}

{% for event in day.all_day_events %}
{% if event.url %} - {{ event.title }} + {{ event.title|truncatechars:22 }} {% else %} - {{ event.title }} + {{ event.title|truncatechars:22 }} {% endif %}
{% endfor %} {% for event in day.events %} -
- {% if event.url %} - {{ event.title }} - {% else %} - {{ event.title }} - {% endif %} +
+ • + + {% if event.url %} + {{ event.title|truncatechars:20 }} + {% else %} + {{ event.title|truncatechars:20 }} + {% endif %} + + {{ event.start_time|time:"H:i" }}
{% endfor %}
From ed770264ab9a1aea49ce0cd4ea554b7c9ebe9db2 Mon Sep 17 00:00:00 2001 From: Austin Riba Date: Sat, 28 Feb 2026 14:11:16 -0800 Subject: [PATCH 12/39] Update events --- .../tom_calendar/partials/calendar.html | 14 ++++++-- .../tom_calendar/partials/create_event.html | 7 +++- tom_calendar/urls.py | 5 +-- tom_calendar/views.py | 34 +++++++++++++++++-- 4 files changed, 52 insertions(+), 8 deletions(-) diff --git a/tom_calendar/templates/tom_calendar/partials/calendar.html b/tom_calendar/templates/tom_calendar/partials/calendar.html index 4565b9bfd..6981ca470 100644 --- a/tom_calendar/templates/tom_calendar/partials/calendar.html +++ b/tom_calendar/templates/tom_calendar/partials/calendar.html @@ -122,7 +122,12 @@

{{ month_name }}

{{ day.date.day }} {{ day.moon.emoji }} {% for event in day.all_day_events %} -
+
{% if event.url %} {{ event.title|truncatechars:22 }} {% else %} @@ -131,7 +136,12 @@

{{ month_name }}

{% endfor %} {% for event in day.events %} -
+
• {% if event.url %} diff --git a/tom_calendar/templates/tom_calendar/partials/create_event.html b/tom_calendar/templates/tom_calendar/partials/create_event.html index af35af266..2561498c9 100644 --- a/tom_calendar/templates/tom_calendar/partials/create_event.html +++ b/tom_calendar/templates/tom_calendar/partials/create_event.html @@ -1,5 +1,10 @@ {% load bootstrap4 %} -
+ {% csrf_token %} {% bootstrap_form form %} {% buttons %} diff --git a/tom_calendar/urls.py b/tom_calendar/urls.py index f5009fb45..d6b17d87f 100644 --- a/tom_calendar/urls.py +++ b/tom_calendar/urls.py @@ -1,10 +1,11 @@ from django.urls import path -from .views import render_calendar, create_event +from .views import render_calendar, create_event, update_event app_name = 'tom_calendar' urlpatterns = [ path("", render_calendar, name="calendar"), - path("create/", create_event, name="create-event") + path("create/", create_event, name="create-event"), + path("update//", update_event, name="update-event"), ] diff --git a/tom_calendar/views.py b/tom_calendar/views.py index 34f1fbff0..5780252b2 100644 --- a/tom_calendar/views.py +++ b/tom_calendar/views.py @@ -9,7 +9,7 @@ import astropy.units as u from django.utils import timezone -from django.shortcuts import render +from django.shortcuts import render, get_object_or_404 from django import forms from django_htmx.http import trigger_client_event @@ -134,7 +134,7 @@ def create_event(request): response = render_calendar(request) return trigger_client_event(response, "eventCreated") else: - response = render(request, "tom_calendar/partials/create_event.html", {"form": form}) + response = render(request, "tom_calendar/partials/create_event.html", {"form": form, "action": "create"}) response["HX-Retarget"] = "#cal-modal-body" response["HX-Reswap"] = "innerHTML" return response @@ -151,4 +151,32 @@ def create_event(request): except ValueError: form = EventForm() - return render(request, "tom_calendar/partials/create_event.html", {"form": form}) + return render(request, "tom_calendar/partials/create_event.html", {"form": form, "action": "create"}) + + +def update_event(request, event_id): + event = get_object_or_404(CalendarEvent, pk=event_id) + + if request.method == "POST": + form = EventForm(request.POST, instance=event) + if form.is_valid(): + form.save() + response = render_calendar(request) + return trigger_client_event(response, "eventCreated") + else: + response = render( + request, + "tom_calendar/partials/create_event.html", + {"form": form, "event": event, "action": "update"} + ) + response["HX-Retarget"] = "#cal-modal-body" + response["HX-Reswap"] = "innerHTML" + return response + + else: + form = EventForm(instance=event) + return render( + request, + "tom_calendar/partials/create_event.html", + {"form": form, "event": event, "action": "update"} + ) From 62ebee416e72bdff440198a7c44a8b95630b4018 Mon Sep 17 00:00:00 2001 From: Austin Riba Date: Sat, 28 Feb 2026 14:13:37 -0800 Subject: [PATCH 13/39] Rename to event_form --- .../partials/{create_event.html => event_form.html} | 0 tom_calendar/views.py | 8 ++++---- 2 files changed, 4 insertions(+), 4 deletions(-) rename tom_calendar/templates/tom_calendar/partials/{create_event.html => event_form.html} (100%) diff --git a/tom_calendar/templates/tom_calendar/partials/create_event.html b/tom_calendar/templates/tom_calendar/partials/event_form.html similarity index 100% rename from tom_calendar/templates/tom_calendar/partials/create_event.html rename to tom_calendar/templates/tom_calendar/partials/event_form.html diff --git a/tom_calendar/views.py b/tom_calendar/views.py index 5780252b2..1c618816f 100644 --- a/tom_calendar/views.py +++ b/tom_calendar/views.py @@ -134,7 +134,7 @@ def create_event(request): response = render_calendar(request) return trigger_client_event(response, "eventCreated") else: - response = render(request, "tom_calendar/partials/create_event.html", {"form": form, "action": "create"}) + response = render(request, "tom_calendar/partials/event_form.html", {"form": form, "action": "create"}) response["HX-Retarget"] = "#cal-modal-body" response["HX-Reswap"] = "innerHTML" return response @@ -151,7 +151,7 @@ def create_event(request): except ValueError: form = EventForm() - return render(request, "tom_calendar/partials/create_event.html", {"form": form, "action": "create"}) + return render(request, "tom_calendar/partials/event_form.html", {"form": form, "action": "create"}) def update_event(request, event_id): @@ -166,7 +166,7 @@ def update_event(request, event_id): else: response = render( request, - "tom_calendar/partials/create_event.html", + "tom_calendar/partials/event_form.html", {"form": form, "event": event, "action": "update"} ) response["HX-Retarget"] = "#cal-modal-body" @@ -177,6 +177,6 @@ def update_event(request, event_id): form = EventForm(instance=event) return render( request, - "tom_calendar/partials/create_event.html", + "tom_calendar/partials/event_form.html", {"form": form, "event": event, "action": "update"} ) From c5d3e53b8e652776e1f4c97e6a25b860f4f57052 Mon Sep 17 00:00:00 2001 From: Austin Riba Date: Sat, 28 Feb 2026 14:20:06 -0800 Subject: [PATCH 14/39] Remove some duplicate logic in event click htmx --- .../templates/tom_calendar/calendar_page.html | 1 - .../tom_calendar/partials/calendar.html | 56 +++++++++---------- 2 files changed, 27 insertions(+), 30 deletions(-) diff --git a/tom_calendar/templates/tom_calendar/calendar_page.html b/tom_calendar/templates/tom_calendar/calendar_page.html index efedbb896..520855192 100644 --- a/tom_calendar/templates/tom_calendar/calendar_page.html +++ b/tom_calendar/templates/tom_calendar/calendar_page.html @@ -10,7 +10,6 @@