|
1 | 1 | from abc import ABC, abstractmethod |
2 | 2 | import copy |
| 3 | +from enum import Enum |
3 | 4 | import logging |
4 | 5 | import requests |
5 | 6 |
|
|
8 | 9 | from django import forms |
9 | 10 | from django.conf import settings |
10 | 11 | from django.contrib.auth.models import Group |
| 12 | +from django.core.exceptions import ImproperlyConfigured |
11 | 13 | from django.core.files.base import ContentFile |
12 | 14 | from django.utils.module_loading import import_string |
13 | 15 |
|
14 | 16 | from tom_targets.models import Target |
15 | 17 |
|
16 | 18 | logger = logging.getLogger(__name__) |
17 | 19 |
|
| 20 | + |
| 21 | +class CredentialStatus(Enum): |
| 22 | + """ |
| 23 | + Enum representing the status of facility credentials. |
| 24 | +
|
| 25 | + This enum is used to track the state of credentials throughout the facility lifecycle, |
| 26 | + providing clear information about whether credentials are available, where they came from, |
| 27 | + and any validation issues. |
| 28 | + """ |
| 29 | + NOT_INITIALIZED = "not_initialized" # set_user() hasn't been called yet |
| 30 | + NO_PROFILE = "no_profile" # User has no Profile (raises ImproperlyConfigured) |
| 31 | + PROFILE_EMPTY = "profile_empty" # Profile exists but credentials empty |
| 32 | + USING_DEFAULTS = "using_defaults" # Using settings.FACILITIES defaults |
| 33 | + USING_USER_CREDS = "using_user_creds" # Using user's Profile credentials |
| 34 | + VALIDATION_FAILED_AUTH = "validation_failed_auth" # Credentials failed (401/403) |
| 35 | + VALIDATION_FAILED_NETWORK = "validation_failed_network" # Network/server issue |
| 36 | + |
| 37 | + |
18 | 38 | DEFAULT_FACILITY_CLASSES = [ |
19 | 39 | 'tom_observations.facilities.lco.LCOFacility', |
20 | 40 | 'tom_observations.facilities.gemini.GEMFacility', |
@@ -67,8 +87,17 @@ class BaseObservationForm(forms.Form): |
67 | 87 | observation_type = forms.CharField(required=False, max_length=50, widget=forms.HiddenInput()) |
68 | 88 |
|
69 | 89 | def __init__(self, *args, **kwargs): |
| 90 | + # DEBUG: Log what parameters are being passed |
| 91 | + logger.debug(f'BaseObservationForm.__init__ kwargs: {kwargs}') |
| 92 | + |
| 93 | + # Accept facility parameter but don't require it (for backward compatibility) |
| 94 | + facility = kwargs.pop('facility', None) |
70 | 95 | self.validation_message = 'This observation is valid.' |
71 | 96 | super().__init__(*args, **kwargs) |
| 97 | + |
| 98 | + # Store facility reference if provided |
| 99 | + if facility is not None: |
| 100 | + self.facility = facility |
72 | 101 | self.helper = FormHelper() |
73 | 102 | if settings.TARGET_PERMISSIONS_ONLY: |
74 | 103 | self.common_layout = Layout('facility', 'target_id', 'observation_type') |
@@ -195,10 +224,91 @@ class BaseObservationFacility(ABC): |
195 | 224 |
|
196 | 225 | def __init__(self): |
197 | 226 | self.user = None |
| 227 | + self.credential_status = CredentialStatus.NOT_INITIALIZED |
198 | 228 |
|
199 | 229 | def set_user(self, user): |
200 | 230 | self.user = user |
201 | 231 |
|
| 232 | + def _is_credential_empty(self, credential): |
| 233 | + """ |
| 234 | + Check if a credential is empty (None, empty string, or whitespace only). |
| 235 | +
|
| 236 | + Args: |
| 237 | + credential: The credential value to check |
| 238 | +
|
| 239 | + Returns: |
| 240 | + bool: True if credential is empty, False otherwise |
| 241 | + """ |
| 242 | + return credential is None or (isinstance(credential, str) and not credential.strip()) |
| 243 | + |
| 244 | + def _get_setting_credentials(self, facility_name, credential_keys): |
| 245 | + """ |
| 246 | + Safely get credentials from settings.FACILITIES. |
| 247 | +
|
| 248 | + Args: |
| 249 | + facility_name (str): Name of the facility in settings.FACILITIES |
| 250 | + credential_keys (list): List of credential key names to retrieve |
| 251 | +
|
| 252 | + Returns: |
| 253 | + dict: Dictionary mapping credential keys to their values |
| 254 | +
|
| 255 | + Raises: |
| 256 | + ImproperlyConfigured: If facility or required keys are missing from settings |
| 257 | + """ |
| 258 | + if not hasattr(settings, 'FACILITIES') or facility_name not in settings.FACILITIES: |
| 259 | + raise ImproperlyConfigured( |
| 260 | + f"No configuration found for '{facility_name}' in settings.FACILITIES. " |
| 261 | + f"Please add default credentials to settings.FACILITIES['{facility_name}']." |
| 262 | + ) |
| 263 | + |
| 264 | + facility_settings = settings.FACILITIES[facility_name] |
| 265 | + credentials = {} |
| 266 | + |
| 267 | + for key in credential_keys: |
| 268 | + if key not in facility_settings: |
| 269 | + raise ImproperlyConfigured( |
| 270 | + f"Required credential key '{key}' not found in " |
| 271 | + f"settings.FACILITIES['{facility_name}']. " |
| 272 | + f"Please add '{key}' to the facility configuration." |
| 273 | + ) |
| 274 | + credentials[key] = facility_settings[key] |
| 275 | + |
| 276 | + return credentials |
| 277 | + |
| 278 | + def _raise_no_profile_error(self, user, facility_name): |
| 279 | + """ |
| 280 | + Raise ImproperlyConfigured for missing user profile. |
| 281 | +
|
| 282 | + Args: |
| 283 | + user: Django User instance |
| 284 | + facility_name (str): Name of the facility |
| 285 | +
|
| 286 | + Raises: |
| 287 | + ImproperlyConfigured: Always raises with informative message |
| 288 | + """ |
| 289 | + raise ImproperlyConfigured( |
| 290 | + f"User '{user.username}' has no {facility_name}Profile configured. " |
| 291 | + f"Please create a {facility_name}Profile for this user in the admin interface." |
| 292 | + ) |
| 293 | + |
| 294 | + def _raise_no_defaults_error(self, user, facility_name): |
| 295 | + """ |
| 296 | + Raise ImproperlyConfigured when default credentials are needed but missing. |
| 297 | +
|
| 298 | + Args: |
| 299 | + user: Django User instance |
| 300 | + facility_name (str): Name of the facility |
| 301 | +
|
| 302 | + Raises: |
| 303 | + ImproperlyConfigured: Always raises with informative message |
| 304 | + """ |
| 305 | + raise ImproperlyConfigured( |
| 306 | + f"User '{user.username}' has no credentials configured and no default credentials " |
| 307 | + f"found in settings.FACILITIES['{facility_name}']. " |
| 308 | + f"Please configure either user credentials in {facility_name}Profile or " |
| 309 | + f"default credentials in settings." |
| 310 | + ) |
| 311 | + |
202 | 312 | def all_data_products(self, observation_record): |
203 | 313 | from tom_dataproducts.models import DataProduct |
204 | 314 | products = {'saved': [], 'unsaved': []} |
@@ -227,7 +337,27 @@ def all_data_products(self, observation_record): |
227 | 337 | def get_form(self, observation_type): |
228 | 338 | """ |
229 | 339 | This method takes in an observation type and returns the form type that matches it. |
| 340 | +
|
| 341 | + Note: This method returns form classes, not instances, to support composite form creation |
| 342 | + in ObservationCreateView. Use create_form_instance() for direct form instantiation. |
| 343 | + """ |
| 344 | + |
| 345 | + def create_form_instance(self, observation_type, **kwargs): |
| 346 | + """ |
| 347 | + Create a form instance with facility context injected. |
| 348 | +
|
| 349 | + The ObservationCreateView handles setting the user context on the facility instance |
| 350 | + via set_user() in its dispatch() method. Forms receive the facility instance and |
| 351 | + can query it for user-specific data (credentials, API clients, etc.) rather than |
| 352 | + handling business logic themselves. |
| 353 | +
|
| 354 | + :param observation_type: The type of observation form to create |
| 355 | + :param kwargs: Additional keyword arguments for form instantiation |
| 356 | + :return: Form instance configured for the observation type |
230 | 357 | """ |
| 358 | + form_class = self.get_form(observation_type) |
| 359 | + kwargs['facility'] = self |
| 360 | + return form_class(**kwargs) |
231 | 361 |
|
232 | 362 | def get_form_classes_for_display(self, **kwargs): |
233 | 363 | """ |
|
0 commit comments