Skip to content
Draft
58 changes: 40 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,30 @@ Install the module into your TOM environment:
pip install tom-cfht
```

1. In your project `settings.py`, add `tom_cfht` to your `INSTALLED_APPS` setting:
Then, in your project `settings.py`, add `tom_cfht` to your `INSTALLED_APPS` setting:

```python
INSTALLED_APPS = [
...
'tom_cfht',
]
```

2. Add `tom_cfht.cfht.CFHTFacility` to the `TOM_FACILITY_CLASSES` in your TOM's
`settings.py`:
```python
TOM_FACILITY_CLASSES = [
'tom_observations.facilities.lco.LCOFacility',
...
'tom_cfht.cfht.CFHTFacility',
]
```
```python
INSTALLED_APPS = [
...
'tom_cfht',
]
```

That's it. `tom_cfht` implements the `observation_facilities()` AppConfig integration point,
so the CFHT facility is discovered automatically — it does not need to be added to
`TOM_FACILITY_CLASSES` in your `settings.py`.

## Configuration

Include the following settings inside the `FACILITIES` dictionary inside `settings.py`:
### Kealahou API access token

Observing-program and target-sync features talk to CFHT's Kealahou API, which authenticates
with an **API access token** — this is not your Kealahou web-UI password. Generate one in the
Kealahou web UI under *Account → Manage Tokens* (it is only shown once, at creation).

Each TOM user should save their token on their CFHT user profile (User Profile page). A
TOM-wide fallback token can be configured in the `FACILITIES` dictionary in `settings.py`;
a user's profile token, when set, takes precedence:

```python
FACILITIES = {
Expand All @@ -43,3 +45,23 @@ Include the following settings inside the `FACILITIES` dictionary inside `settin
}
```

## Target syncing with Kealahou

The CFHT facility page (*Facilities → CFHT* in the navbar) shows one tab per Kealahou
observing program. Each program's Targets section shows which targets are only in the
program's Target Grouping (the section is titled with the Target Grouping's name), only in
Kealahou, or in both — in agreement, or with property discrepancies shown field by field. Check rows in either direction and press **Sync selected targets** to import the
checked Kealahou targets into the TOM and upload the checked TOM targets to Kealahou.

Before syncing, each program must be associated with a **Target Grouping** — you choose an
existing one or create one (a name like `CFHT-MEGACAM-25BE25` is suggested) the first time
you open the program's Targets section. Membership in that Target Grouping is what marks a
target for syncing with the program: adding a target marks it for upload, downloads from
Kealahou land in it, and removing a member withdraws the target from TOM-side syncing (it
then shows under "In Kealahou only" again, ready to re-download). Only sidereal targets are
supported for now.

The CFHT observation form also shows the target's Kealahou status per program, with a
one-click upload (which also adds the target to the program's Target Grouping) for targets
that are not in Kealahou yet.

3 changes: 0 additions & 3 deletions tom_cfht/admin.py

This file was deleted.

43 changes: 42 additions & 1 deletion tom_cfht/apps.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,47 @@
from django.apps import AppConfig
from django.urls import path, include


class TomCFHTConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'tom_cfht'
name = 'tom_cfht' # python path to the application, like 'django.contrib.admin'
url_prefix = 'cfht' # URL path prefix for this app's pages: HOST:PORT/cfht/... (see include_url_paths())

def include_url_paths(self):
"""
Integration point for adding URL patterns to the Tom Common URL configuration.
This method should return a list of URL patterns to be included in the main URL configuration.

Note: url_prefix only affects the path; the URL namespace remains self.label ('tom_cfht'),
so reverses like 'tom_cfht:facility-index' are unaffected.
"""
urlpatterns = [
path(f'{self.url_prefix}/', include(f'{self.name}.urls', namespace=f'{self.label}'))
]
return urlpatterns

def observation_facilities(self):
"""
Integration point for including this app's observation facilities in the TOM.

This method should return a list of dictionaries, each with a `class` key giving the dot separated
path to a Facility class (consumed by ``tom_observations.facility.get_service_classes()``, so the
facility is available without being listed in ``settings.TOM_FACILITY_CLASSES``), and an optional
`url` key giving the namespaced URL name of the facility's landing page (used by the navbar
"Facilities" menu). Omit `url` for a facility with no landing page: it is still registered, but
gets no navbar menu item.
"""
return [{'class': f'{self.name}.cfht.CFHTFacility',
'url': f'{self.label}:facility-index'}]

def profile_details(self):
"""
Integration point for adding items to the user profile page.

This method should return a list of dictionaries that include a `partial` key pointing to the path of the html
profile partial. The `context` key should point to the dot separated string path to the templatetag that will
return a dictionary containing new context for the accompanying partial.
Typically, this partial will be a bootstrap card displaying some app specific user data.
"""
return [{'partial': f'{self.name}/partials/profile_cfht.html',
'context': f'{self.name}.templatetags.cfht_extras.cfht_profile_data'}]
122 changes: 122 additions & 0 deletions tom_cfht/cfht.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
from __future__ import annotations

from crispy_forms.layout import Layout
from django import forms
from django.core.exceptions import ImproperlyConfigured

from aeonlib.cfht.facility import CFHTFacility as AeonCFHTFacility
from aeonlib.cfht.models import ProgramInfo

from tom_observations.facility import BaseRoboticObservationFacility, BaseRoboticObservationForm, CredentialStatus

from tom_cfht.models import CFHTProfile


class CFHTFacilityForm(BaseRoboticObservationForm):
exposure_time = forms.IntegerField()
exposure_count = forms.IntegerField()

def layout(self):
return Layout(
'exposure_time',
'exposure_count'
)


class CFHTFacility(BaseRoboticObservationFacility):
name = 'CFHT'
# Facility-specific observation form page: adds the Kealahou target status/upload panel.
# ObservationCreateView.get_template_names() tries this template first.
template_name = 'tom_cfht/observation_form.html'
observation_types: list[tuple[str, str]] = [
('OBSERVATION', 'Custom Observation')
]

observation_forms: dict[str, type[BaseRoboticObservationForm]] = {
'OBSERVATION': CFHTFacilityForm,
}

def get_access_token(self) -> str:
"""Return the Kealahou API access token for the current user.

The per-user token from the ``CFHTProfile`` (set via ``set_user()``) takes
precedence; falls back to the TOM-wide default in
``settings.FACILITIES['CFHT']['CFHT_ACCESS_TOKEN']``. Tracks the outcome in
``self.credential_status``.

Note: this is the Bearer credential for API authentication -- unrelated to
Kealahou's entity identifiers, which are unfortunately also called "tokens".

Raises:
ImproperlyConfigured: if neither source provides a token.
"""
if self.user is not None and self.user.is_authenticated:
try:
profile_access_token = str(self.user.cfhtprofile.cfht_access_token or '').strip()
except CFHTProfile.DoesNotExist:
profile_access_token = ''
if profile_access_token:
self.credential_status = CredentialStatus.USING_USER_CREDS
return profile_access_token

# fall back to the TOM-wide default from settings.FACILITIES
setting_credentials = self._get_setting_credentials('CFHT', ['CFHT_ACCESS_TOKEN'])
default_access_token = str(setting_credentials['CFHT_ACCESS_TOKEN'] or '').strip()
if self._is_credential_empty(default_access_token):
self.credential_status = CredentialStatus.PROFILE_EMPTY
raise ImproperlyConfigured(
'No CFHT access token found. Generate one on the Kealahou "Manage Tokens" page and '
"save it in your CFHT user profile (or in settings.FACILITIES['CFHT'])."
)
self.credential_status = CredentialStatus.USING_DEFAULTS
return default_access_token

def get_aeon_facility(self) -> AeonCFHTFacility:
"""Return an aeonlib Kealahou client authenticated as the current user."""
return AeonCFHTFacility(access_token=self.get_access_token())

def get_observing_programs(self) -> list[ProgramInfo]:
"""Return the current user's CFHT observing programs from the Kealahou API."""
return self.get_aeon_facility().programs()

def data_products(self):
pass

def get_form(self, observation_type: str | None) -> type[BaseRoboticObservationForm]:
"""Return the observation form class for ``observation_type``.
"""
if observation_type is None:
return CFHTFacilityForm
return self.observation_forms.get(observation_type, CFHTFacilityForm)

def get_observation_status(self):
pass

def get_observation_url(self):
pass

def get_observing_sites(self) -> dict[str, dict]:
"""Return the facility's observing site(s) for the visibility and airmass planner.

From CFHT Observatory Manual: The telescope itself is of 3.58 meters aperture.
It is located on Mauna Kea at an altitude (declination axis) of 4204 m (13,793 feet),
at latitude +19o 49' 41.86" and longitude 155o 28' 18.00".
"""
cfht_location_params = {
'Mauna Kea': {
'sitecode': 'cfht',
'latitude': 19.8283,
'longitude': -155.4716,
'elevation': 4204,
}
}
return cfht_location_params

def get_terminal_observing_states(self):
pass

def submit_observation(self):
pass

def validate_observation(self):
pass
Loading
Loading