From e22c3203f34fd246a1e71c083a3af39ea744fced Mon Sep 17 00:00:00 2001 From: Dan Avner Date: Fri, 20 Mar 2026 07:17:30 -0700 Subject: [PATCH 1/5] Convert telescope facility to use HTMX for lazy loading and updating --- .../tom_observations/facility_status.html | 28 +++++++- .../existing_observation_confirm.html | 0 .../partials/facility_status.html | 33 --------- .../partials/facility_status_table.html | 31 +++++++++ .../templatetags/observation_extras.py | 35 +--------- tom_observations/urls.py | 3 +- tom_observations/views.py | 67 ++++++++++++++----- 7 files changed, 115 insertions(+), 82 deletions(-) rename tom_observations/templates/tom_observations/{ => partials}/existing_observation_confirm.html (100%) delete mode 100644 tom_observations/templates/tom_observations/partials/facility_status.html create mode 100644 tom_observations/templates/tom_observations/partials/facility_status_table.html diff --git a/tom_observations/templates/tom_observations/facility_status.html b/tom_observations/templates/tom_observations/facility_status.html index 52e61db6d..25e561eb3 100644 --- a/tom_observations/templates/tom_observations/facility_status.html +++ b/tom_observations/templates/tom_observations/facility_status.html @@ -5,6 +5,32 @@

Facility Status

{% facility_map %}
- {% facility_status %} + +
+
+ + + + + + + + + + + + + + + +
FacilitySiteTelescopeStatusWeather URL
Facility statuses loading...
+
{% endblock %} diff --git a/tom_observations/templates/tom_observations/existing_observation_confirm.html b/tom_observations/templates/tom_observations/partials/existing_observation_confirm.html similarity index 100% rename from tom_observations/templates/tom_observations/existing_observation_confirm.html rename to tom_observations/templates/tom_observations/partials/existing_observation_confirm.html diff --git a/tom_observations/templates/tom_observations/partials/facility_status.html b/tom_observations/templates/tom_observations/partials/facility_status.html deleted file mode 100644 index 621741421..000000000 --- a/tom_observations/templates/tom_observations/partials/facility_status.html +++ /dev/null @@ -1,33 +0,0 @@ -{% load tom_common_extras %} -
- - - - - - - - - - - - {% for facility in facilities %} - {% for site in facility.sites %} - {% for telescope in site.telescopes %} - - - - - - - - {% endfor %} - {% endfor %} - {% empty %} - - - - {% endfor %} - -
FacilitySiteTelescopeStatusWeather URL
{{ facility.code }}{{ site.code }}{{ telescope.code }}{{ telescope.status }}link
Facility status unknown.
-
\ No newline at end of file diff --git a/tom_observations/templates/tom_observations/partials/facility_status_table.html b/tom_observations/templates/tom_observations/partials/facility_status_table.html new file mode 100644 index 000000000..a66dc64aa --- /dev/null +++ b/tom_observations/templates/tom_observations/partials/facility_status_table.html @@ -0,0 +1,31 @@ +{% load tom_common_extras %} + + + + + + + + + + + +{% for facility in facilities %} +{% for site in facility.sites %} +{% for telescope in site.telescopes %} + + + + + + + +{% endfor %} +{% endfor %} +{% empty %} + + + +{% endfor %} + +
FacilitySiteTelescopeStatusWeather URL
{{ facility.code }}{{ site.code }}{{ telescope.code }}{{ telescope.status }}link
Facility status unknown.
\ No newline at end of file diff --git a/tom_observations/templatetags/observation_extras.py b/tom_observations/templatetags/observation_extras.py index 4179ca041..f064d1ee2 100644 --- a/tom_observations/templatetags/observation_extras.py +++ b/tom_observations/templatetags/observation_extras.py @@ -1,20 +1,19 @@ from datetime import datetime, timedelta from urllib.parse import urlencode +import plotly.graph_objs as go from django import forms, template from django.conf import settings from django.urls import reverse from guardian.shortcuts import get_objects_for_user from plotly import offline -import plotly.graph_objs as go +from tom_targets.models import Target +from tom_observations.facility import get_service_class, get_service_classes from tom_observations.forms import AddExistingObservationForm, UpdateObservationId from tom_observations.models import ObservationRecord -from tom_observations.facility import get_service_class, get_service_classes from tom_observations.observation_template import ApplyObservationTemplateForm from tom_observations.utils import get_sidereal_visibility -from tom_targets.models import Target - register = template.Library() @@ -267,34 +266,6 @@ def observation_distribution(observations): return {'figure': figure} -@register.inclusion_tag('tom_observations/partials/facility_status.html', takes_context=True) -def facility_status(context): - """ - Collect the facility status from the registered facilities and pass them - to the facility_status.html partial template. - See lco.py Facility implementation for example. - :return: - """ - - facility_statuses = [] - for facility_class in get_service_classes().values(): - facility = facility_class() - facility.set_user(context['request'].user) - weather_urls = facility.get_facility_weather_urls() - status = facility.get_facility_status() - - # add the weather_url to the site dictionary - for site in status.get('sites', []): - url = next((site_url['weather_url'] for site_url in weather_urls.get('sites', []) - if site_url['code'] == site['code']), None) - if url is not None: - site['weather_url'] = url - - facility_statuses.append(status) - - return {'facilities': facility_statuses} - - @register.inclusion_tag('tom_observations/partials/facility_map.html', takes_context=True) def facility_map(context): facility_locations = [] diff --git a/tom_observations/urls.py b/tom_observations/urls.py index 2169c37b6..93f80cf17 100644 --- a/tom_observations/urls.py +++ b/tom_observations/urls.py @@ -5,7 +5,7 @@ ObservationGroupListView, ObservationListView, ObservationRecordCancelView, ObservationRecordDetailView, ObservationTemplateCreateView, ObservationTemplateDeleteView, ObservationTemplateListView, - ObservationTemplateUpdateView, ObservationCallbackView, ObservationRedirectView) + ObservationTemplateUpdateView, ObservationCallbackView, ObservationRedirectView, render_facility_status_list) from tom_observations.api_views import ObservationRecordViewSet from tom_common.api_router import SharedAPIRootRouter @@ -18,6 +18,7 @@ path('add/', AddExistingObservationView.as_view(), name='add-existing'), path('list/', ObservationListView.as_view(), name='list'), path('status/', FacilityStatusView.as_view(), name='facility-status'), + path('status/list/', render_facility_status_list, name='render-facility-status-list'), path('template/list/', ObservationTemplateListView.as_view(), name='template-list'), path('template//create/', ObservationTemplateCreateView.as_view(), name='template-create'), path('template//update/', ObservationTemplateUpdateView.as_view(), name='template-update'), diff --git a/tom_observations/views.py b/tom_observations/views.py index 91804acbd..3c8480d93 100644 --- a/tom_observations/views.py +++ b/tom_observations/views.py @@ -1,41 +1,56 @@ -from io import StringIO -from urllib.parse import urlencode import logging +from io import StringIO from typing import Any, List +from urllib.parse import urlencode from crispy_forms.bootstrap import FormActions from crispy_forms.layout import HTML, Layout, Submit from django import forms -from django.core.exceptions import BadRequest from django.conf import settings from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin +from django.core.exceptions import BadRequest from django.core.management import call_command -from django_filters import CharFilter, ChoiceFilter, DateTimeFromToRangeFilter, ModelMultipleChoiceFilter -from django_filters import OrderingFilter, MultipleChoiceFilter, rest_framework -from django_filters.views import FilterView -from django.shortcuts import get_object_or_404, redirect +from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse, reverse_lazy from django.utils.safestring import mark_safe -from django.views.generic import View, TemplateView +from django.views.generic import TemplateView, View from django.views.generic.detail import DetailView from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView from django.views.generic.list import ListView -from guardian.shortcuts import get_objects_for_user, assign_perm +from django_filters import ( + CharFilter, + ChoiceFilter, + DateTimeFromToRangeFilter, + ModelMultipleChoiceFilter, + MultipleChoiceFilter, + OrderingFilter, + rest_framework, +) +from django_filters.views import FilterView from guardian.mixins import PermissionListMixin - +from guardian.shortcuts import assign_perm, get_objects_for_user from tom_common.hints import add_hint from tom_common.mixins import Raise403PermissionRequiredMixin from tom_dataproducts.forms import AddProductToGroupForm, DataProductUploadForm from tom_dataproducts.models import is_fits_image_file -from tom_observations.cadence import CadenceForm, get_cadence_strategy -from tom_observations.facility import get_service_class, get_service_classes -from tom_observations.facility import BaseManualObservationFacility -from tom_observations.forms import AddExistingObservationForm, facility_choices -from tom_observations.models import ObservationRecord, ObservationGroup, ObservationTemplate, DynamicCadence from tom_targets.models import Target from tom_targets.permissions import targets_for_user +from tom_observations.cadence import CadenceForm, get_cadence_strategy +from tom_observations.facility import ( + BaseManualObservationFacility, + get_service_class, + get_service_classes, +) +from tom_observations.forms import AddExistingObservationForm, facility_choices +from tom_observations.models import ( + DynamicCadence, + ObservationGroup, + ObservationRecord, + ObservationTemplate, +) + logger = logging.getLogger(__name__) @@ -842,3 +857,25 @@ class ObservationTemplateDeleteView(LoginRequiredMixin, DeleteView): class FacilityStatusView(TemplateView): template_name = 'tom_observations/facility_status.html' + + +def render_facility_status_list(request, *args, **kwargs): + """ + View function for rendering the facility status partial. This is called by the HTMX trigger in the facility_status_placeholder partial when the page loads. It gathers the status of each facility and returns it as context for rendering the facility_status partial. + """ + facility_statuses = [] + for facility_class in get_service_classes().values(): + facility = facility_class() + facility.set_user(request.user) + weather_urls = facility.get_facility_weather_urls() + status = facility.get_facility_status() + + # add the weather_url to the site dictionary + for site in status.get('sites', []): + url = next((site_url['weather_url'] for site_url in weather_urls.get('sites', []) + if site_url['code'] == site['code']), None) + if url is not None: + site['weather_url'] = url + + facility_statuses.append(status) + return render(request, 'tom_observations/partials/facility_status_table.html', context={'facilities': facility_statuses}) From 3114bd3f403a7c24c30304f27ad21f2dac146fa9 Mon Sep 17 00:00:00 2001 From: Dan Avner Date: Fri, 20 Mar 2026 09:32:16 -0700 Subject: [PATCH 2/5] Add conditional trigger to display messages --- .../existing_observation_confirm.html | 0 .../tom_observations/facility_status.html | 40 +++++-------- .../partials/facility_status_table.html | 57 +++++++++++-------- .../templatetags/observation_extras.py | 7 ++- tom_observations/views.py | 52 +++++++---------- 5 files changed, 72 insertions(+), 84 deletions(-) rename tom_observations/templates/tom_observations/{partials => }/existing_observation_confirm.html (100%) diff --git a/tom_observations/templates/tom_observations/partials/existing_observation_confirm.html b/tom_observations/templates/tom_observations/existing_observation_confirm.html similarity index 100% rename from tom_observations/templates/tom_observations/partials/existing_observation_confirm.html rename to tom_observations/templates/tom_observations/existing_observation_confirm.html diff --git a/tom_observations/templates/tom_observations/facility_status.html b/tom_observations/templates/tom_observations/facility_status.html index 25e561eb3..152f5583e 100644 --- a/tom_observations/templates/tom_observations/facility_status.html +++ b/tom_observations/templates/tom_observations/facility_status.html @@ -5,32 +5,22 @@

Facility Status

{% facility_map %}
- -
-
- - - - - - - - - - - - - - - -
FacilitySiteTelescopeStatusWeather URL
Facility statuses loading...
-
+ +
+
+
+ {% include 'tom_observations/partials/facility_status_table.html' with loading=True facilities=None %} +
{% endblock %} diff --git a/tom_observations/templates/tom_observations/partials/facility_status_table.html b/tom_observations/templates/tom_observations/partials/facility_status_table.html index a66dc64aa..11c305f93 100644 --- a/tom_observations/templates/tom_observations/partials/facility_status_table.html +++ b/tom_observations/templates/tom_observations/partials/facility_status_table.html @@ -2,30 +2,37 @@ - - - - - + + + + + - -{% for facility in facilities %} -{% for site in facility.sites %} -{% for telescope in site.telescopes %} - - - - - - - -{% endfor %} -{% endfor %} -{% empty %} - - - -{% endfor %} - -
FacilitySiteTelescopeStatusWeather URLFacilitySiteTelescopeStatusWeather URL
{{ facility.code }}{{ site.code }}{{ telescope.code }}{{ telescope.status }}link
Facility status unknown.
\ No newline at end of file + + {% if loading %} + + Fetching facility status... + + {% else %} + {% for facility in facilities %} + {% for site in facility.sites %} + {% for telescope in site.telescopes %} + + {{ facility.code }} + {{ site.code }} + {{ telescope.code }} + {{ telescope.status }} + link + + {% endfor %} + {% endfor %} + {% empty %} + + Facility status unknown. + + {% endfor %} + {% endif %} + + +{% include 'tom_common/partials/messages.html' %} diff --git a/tom_observations/templatetags/observation_extras.py b/tom_observations/templatetags/observation_extras.py index f064d1ee2..ec09b24f4 100644 --- a/tom_observations/templatetags/observation_extras.py +++ b/tom_observations/templatetags/observation_extras.py @@ -1,19 +1,20 @@ from datetime import datetime, timedelta from urllib.parse import urlencode -import plotly.graph_objs as go from django import forms, template from django.conf import settings from django.urls import reverse from guardian.shortcuts import get_objects_for_user from plotly import offline -from tom_targets.models import Target +import plotly.graph_objs as go -from tom_observations.facility import get_service_class, get_service_classes from tom_observations.forms import AddExistingObservationForm, UpdateObservationId from tom_observations.models import ObservationRecord +from tom_observations.facility import get_service_class, get_service_classes from tom_observations.observation_template import ApplyObservationTemplateForm from tom_observations.utils import get_sidereal_visibility +from tom_targets.models import Target + register = template.Library() diff --git a/tom_observations/views.py b/tom_observations/views.py index 3c8480d93..0191d3b13 100644 --- a/tom_observations/views.py +++ b/tom_observations/views.py @@ -1,55 +1,40 @@ -import logging from io import StringIO -from typing import Any, List from urllib.parse import urlencode +import logging +from typing import Any, List from crispy_forms.bootstrap import FormActions from crispy_forms.layout import HTML, Layout, Submit from django import forms +from django.core.exceptions import BadRequest from django.conf import settings from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin -from django.core.exceptions import BadRequest from django.core.management import call_command +from django_filters import CharFilter, ChoiceFilter, DateTimeFromToRangeFilter, ModelMultipleChoiceFilter +from django_filters import OrderingFilter, MultipleChoiceFilter, rest_framework +from django_filters.views import FilterView from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse, reverse_lazy from django.utils.safestring import mark_safe -from django.views.generic import TemplateView, View +from django.views.generic import View, TemplateView from django.views.generic.detail import DetailView from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView from django.views.generic.list import ListView -from django_filters import ( - CharFilter, - ChoiceFilter, - DateTimeFromToRangeFilter, - ModelMultipleChoiceFilter, - MultipleChoiceFilter, - OrderingFilter, - rest_framework, -) -from django_filters.views import FilterView +from guardian.shortcuts import get_objects_for_user, assign_perm from guardian.mixins import PermissionListMixin -from guardian.shortcuts import assign_perm, get_objects_for_user + from tom_common.hints import add_hint from tom_common.mixins import Raise403PermissionRequiredMixin from tom_dataproducts.forms import AddProductToGroupForm, DataProductUploadForm from tom_dataproducts.models import is_fits_image_file -from tom_targets.models import Target -from tom_targets.permissions import targets_for_user - from tom_observations.cadence import CadenceForm, get_cadence_strategy -from tom_observations.facility import ( - BaseManualObservationFacility, - get_service_class, - get_service_classes, -) +from tom_observations.facility import get_service_class, get_service_classes +from tom_observations.facility import BaseManualObservationFacility from tom_observations.forms import AddExistingObservationForm, facility_choices -from tom_observations.models import ( - DynamicCadence, - ObservationGroup, - ObservationRecord, - ObservationTemplate, -) +from tom_observations.models import ObservationRecord, ObservationGroup, ObservationTemplate, DynamicCadence +from tom_targets.models import Target +from tom_targets.permissions import targets_for_user logger = logging.getLogger(__name__) @@ -861,7 +846,7 @@ class FacilityStatusView(TemplateView): def render_facility_status_list(request, *args, **kwargs): """ - View function for rendering the facility status partial. This is called by the HTMX trigger in the facility_status_placeholder partial when the page loads. It gathers the status of each facility and returns it as context for rendering the facility_status partial. + View function for rendering the facility status partial. """ facility_statuses = [] for facility_class in get_service_classes().values(): @@ -878,4 +863,9 @@ def render_facility_status_list(request, *args, **kwargs): site['weather_url'] = url facility_statuses.append(status) - return render(request, 'tom_observations/partials/facility_status_table.html', context={'facilities': facility_statuses}) + + hx_trigger = request.GET.get('hx_trigger') + if hx_trigger != 'load': + messages.info(request, "Facility statuses updated.") + + return render(request, 'tom_observations/partials/facility_status_table.html', context={'facilities': facility_statuses, 'loading': False}) From 7f3f45ea6756f7612f1307dea30847373380e661 Mon Sep 17 00:00:00 2001 From: Dan Avner Date: Fri, 20 Mar 2026 10:01:10 -0700 Subject: [PATCH 3/5] Fix linting --- tom_observations/urls.py | 7 +++++-- tom_observations/views.py | 9 ++++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/tom_observations/urls.py b/tom_observations/urls.py index 93f80cf17..76b236592 100644 --- a/tom_observations/urls.py +++ b/tom_observations/urls.py @@ -5,7 +5,9 @@ ObservationGroupListView, ObservationListView, ObservationRecordCancelView, ObservationRecordDetailView, ObservationTemplateCreateView, ObservationTemplateDeleteView, ObservationTemplateListView, - ObservationTemplateUpdateView, ObservationCallbackView, ObservationRedirectView, render_facility_status_list) + ObservationTemplateUpdateView, + ObservationCallbackView, ObservationRedirectView, + render_facility_status_list) from tom_observations.api_views import ObservationRecordViewSet from tom_common.api_router import SharedAPIRootRouter @@ -18,7 +20,8 @@ path('add/', AddExistingObservationView.as_view(), name='add-existing'), path('list/', ObservationListView.as_view(), name='list'), path('status/', FacilityStatusView.as_view(), name='facility-status'), - path('status/list/', render_facility_status_list, name='render-facility-status-list'), + path('status/list/', render_facility_status_list, + name='render-facility-status-list'), path('template/list/', ObservationTemplateListView.as_view(), name='template-list'), path('template//create/', ObservationTemplateCreateView.as_view(), name='template-create'), path('template//update/', ObservationTemplateUpdateView.as_view(), name='template-update'), diff --git a/tom_observations/views.py b/tom_observations/views.py index 0191d3b13..3d643f1c0 100644 --- a/tom_observations/views.py +++ b/tom_observations/views.py @@ -857,8 +857,9 @@ def render_facility_status_list(request, *args, **kwargs): # add the weather_url to the site dictionary for site in status.get('sites', []): - url = next((site_url['weather_url'] for site_url in weather_urls.get('sites', []) - if site_url['code'] == site['code']), None) + url = next(( + site_url['weather_url'] for site_url in weather_urls.get('sites', []) + if site_url['code'] == site['code']), None) if url is not None: site['weather_url'] = url @@ -868,4 +869,6 @@ def render_facility_status_list(request, *args, **kwargs): if hx_trigger != 'load': messages.info(request, "Facility statuses updated.") - return render(request, 'tom_observations/partials/facility_status_table.html', context={'facilities': facility_statuses, 'loading': False}) + return render( + request, 'tom_observations/partials/facility_status_table.html', + context={'facilities': facility_statuses, 'loading': False}) From c16497347213ba7fe0d89499540d298376e18a5f Mon Sep 17 00:00:00 2001 From: Dan Avner Date: Fri, 20 Mar 2026 10:10:01 -0700 Subject: [PATCH 4/5] Fix tests --- tom_observations/tests/tests.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tom_observations/tests/tests.py b/tom_observations/tests/tests.py index e671e9c64..8bde9c3fa 100644 --- a/tom_observations/tests/tests.py +++ b/tom_observations/tests/tests.py @@ -319,7 +319,8 @@ def setUp(self): pass def test_facility_status(self): - response = self.client.get(reverse('tom_observations:facility-status')) + response = self.client.get( + reverse('tom_observations:render-facility-status-list')) self.assertEqual(response.status_code, 200) self.assertContains(response, 'coj.domb.1m0a', status_code=HTTPStatus.OK) From 202de88e26cfb526963785147f53215da222ade0 Mon Sep 17 00:00:00 2001 From: Dan Avner Date: Fri, 20 Mar 2026 10:18:44 -0700 Subject: [PATCH 5/5] Address review comments [skip ci] --- .../templates/tom_observations/facility_status.html | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tom_observations/templates/tom_observations/facility_status.html b/tom_observations/templates/tom_observations/facility_status.html index 152f5583e..b4c67cce1 100644 --- a/tom_observations/templates/tom_observations/facility_status.html +++ b/tom_observations/templates/tom_observations/facility_status.html @@ -8,8 +8,7 @@

Facility Status

+ hx-trigger="load">