Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions src/goats_tom/api_views/gpp/toos.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
from gpp_client.api.enums import ObservationWorkflowState
from gpp_client.api.input_types import (
BandBrightnessIntegratedInput,
CloneObservationInput,
CloneTargetInput,
ElevationRangeInput,
ExposureTimeModeInput,
ObservationPropertiesInput,
Expand All @@ -23,6 +25,8 @@

from goats_tom.serializers.gpp import (
BrightnessesSerializer,
CloneObservationSerializer,
CloneTargetSerializer,
ElevationRangeSerializer,
ExposureModeSerializer,
InstrumentRegistry,
Expand Down Expand Up @@ -74,6 +78,8 @@ def create(self, request: Request, *args, **kwargs) -> Response:
# TODO: Format elevation range from request data.
# TODO: Format instrument from request data.
# TODO: Format source profile from request data.
# print(self._format_clone_observation_input(request.data))
# print(self._format_clone_target_input(request.data))

return Response({"detail": "Not yet implemented."})

Expand Down Expand Up @@ -313,6 +319,52 @@ def _format_target_properties(self, data: dict[str, Any]) -> TargetPropertiesInp
"""
raise NotImplementedError

def _format_clone_observation_input(
self, data: dict[str, Any]
) -> CloneObservationInput:
"""Format the clone observation input from the request data.

Parameters
----------
data : dict[str, Any]
The request data containing observation fields.

Returns
-------
CloneObservationInput
The formatted clone observation input.
"""
clone_observation = CloneObservationSerializer(data=data)
clone_observation.is_valid(raise_exception=True)

observation_properties = ObservationPropertiesInput(
**clone_observation.validated_data
)

return CloneObservationInput(
observation_id=clone_observation.observation_id, set=observation_properties
)

def _format_clone_target_input(self, data: dict[str, Any]) -> CloneTargetInput:
"""Format the target input from the request data.

Parameters
----------
data : dict[str, Any]
The request data containing target fields.

Returns
-------
CloneTargetInput
The formatted clone target input.
"""
clone_target = CloneTargetSerializer(data=data)
clone_target.is_valid(raise_exception=True)

target_properties = TargetPropertiesInput(**clone_target.validated_data)

return CloneTargetInput(target_id=clone_target.target_id, set=target_properties)

def _get_workflow_state(
self, client: GPPClient, observation_id: str
) -> dict[str, Any]:
Expand Down
4 changes: 4 additions & 0 deletions src/goats_tom/serializers/gpp/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from .brightnesses import BrightnessesSerializer
from .clone_observation import CloneObservationSerializer
from .clone_target import CloneTargetSerializer
from .elevation_range import ElevationRangeSerializer
from .exposure_mode import ExposureModeSerializer
from .instruments import InstrumentRegistry
Expand All @@ -10,4 +12,6 @@
"ElevationRangeSerializer",
"SourceProfileSerializer",
"InstrumentRegistry",
"CloneTargetSerializer",
"CloneObservationSerializer",
]
140 changes: 140 additions & 0 deletions src/goats_tom/serializers/gpp/clone_observation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
"""
Clone Observation Serializer for the GPP module.
"""

__all__ = ["CloneObservationSerializer"]

from typing import Any

from gpp_client.api.enums import (
CloudExtinctionPreset,
ImageQualityPreset,
PosAngleConstraintMode,
SkyBackground,
WaterVapor,
)
from rest_framework import serializers

from .utils import normalize


class CloneObservationSerializer(serializers.Serializer):
"""
Serializer for cloning observation data.

This serializer processes hidden input fields related to observation cloning, such
as observation ID, observing mode, and various constraints.
"""

hiddenObservationIdInput = serializers.CharField(
required=False, allow_blank=True, allow_null=True
)
hiddenObservingModeInput = serializers.CharField(
required=False, allow_blank=True, allow_null=True
)
observerNotesTextarea = serializers.CharField(
required=False, allow_blank=True, allow_null=True
)
imageQualitySelect = serializers.ChoiceField(
choices=[c.value for c in ImageQualityPreset], required=False, allow_blank=False
)
cloudExtinctionSelect = serializers.ChoiceField(
choices=[c.value for c in CloudExtinctionPreset],
required=False,
allow_blank=False,
)
skyBackgroundSelect = serializers.ChoiceField(
choices=[c.value for c in SkyBackground], required=False, allow_blank=False
)
waterVaporSelect = serializers.ChoiceField(
choices=[c.value for c in WaterVapor], required=False, allow_blank=False
)
posAngleConstraintModeSelect = serializers.ChoiceField(
choices=[c.value for c in PosAngleConstraintMode],
required=False,
allow_blank=False,
)
posAngleConstraintAngleInput = serializers.FloatField(
required=False, allow_null=True, min_value=0.0, max_value=360.0
)

def to_internal_value(self, data: dict[str, Any]) -> dict[str, Any]:
"""
Normalize blank strings to ``None`` before standard processing because this is
form data.

Parameters
----------
data : dict[str, Any]
The input data from the form.

Returns
-------
dict[str, Any]
The normalized internal value dictionary.
"""
normalized_data = {key: normalize(value) for key, value in data.items()}
return super().to_internal_value(normalized_data)

def validate(self, data: dict[str, Any]) -> dict[str, Any]:
"""
Perform cross-field validation and build the structured data for the clone
observation.

Parameters
----------
data : dict[str, Any]
The validated data dictionary.

Returns
-------
dict[str, Any]
The validated data dictionary.
"""
# Assign observation ID and observing mode if provided.
self._observation_id = data.get("hiddenObservationIdInput")
self._observing_mode = data.get("hiddenObservingModeInput")

mode = data.get("posAngleConstraintModeSelect")
angle = data.get("posAngleConstraintAngleInput")

# Validate that angle is provided if mode requires it.
if mode in {
PosAngleConstraintMode.FIXED.value,
PosAngleConstraintMode.ALLOW_FLIP.value,
PosAngleConstraintMode.PARALLACTIC_OVERRIDE.value,
}:
if angle is None:
raise serializers.ValidationError(
{
"Position Angle Input": (
"This angle is required for the selected mode."
)
}
)

return {
"observerNotes": data.get("observerNotesTextarea"),
"constraintSet": {
"imageQuality": data.get("imageQualitySelect"),
"cloudExtinction": data.get("cloudExtinctionSelect"),
"skyBackground": data.get("skyBackgroundSelect"),
"waterVapor": data.get("waterVaporSelect"),
# Placeholder for other serializer field.
"elevationRange": None,
},
"posAngleConstraint": {
"mode": mode,
"angle": {"degrees": angle},
},
# Placeholder for other serializer field.
"observingMode": None,
}

@property
def observation_id(self) -> str | None:
return getattr(self, "_observation_id", None)

@property
def observing_mode(self) -> str | None:
return getattr(self, "_observing_mode", None)
92 changes: 92 additions & 0 deletions src/goats_tom/serializers/gpp/clone_target.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
"""
Clone Target Serializer for the GPP module.
"""

__all__ = ["CloneTargetSerializer"]

from typing import Any

from rest_framework import serializers

from .utils import normalize


class CloneTargetSerializer(serializers.Serializer):
"""Serializer for cloning target data.

This serializer processes hidden input fields related to target cloning, such as
target ID, radial velocity, parallax, and proper motion.
"""

hiddenTargetIdInput = serializers.CharField(
required=False, allow_null=True, allow_blank=True
)
radialVelocityInput = serializers.FloatField(required=False, allow_null=True)
parallaxInput = serializers.FloatField(required=False, allow_null=True)
uRaInput = serializers.FloatField(required=False, allow_null=True)
uDecInput = serializers.FloatField(required=False, allow_null=True)

def to_internal_value(self, data: dict[str, Any]) -> dict[str, Any]:
"""
Normalize blank strings to ``None`` before standard processing because this is
form data.

Parameters
----------
data : dict[str, Any]
The input data from the form.

Returns
-------
dict[str, Any]
The normalized internal value dictionary.
"""
normalized_data = {key: normalize(value) for key, value in data.items()}
return super().to_internal_value(normalized_data)

def validate(self, data: dict[str, Any]) -> dict[str, Any]:
"""
Perform cross-field validation and build the structured data for the clone
target.

Parameters
----------
data : dict[str, Any]
The validated data dictionary.

Returns
-------
dict[str, Any]
The validated data dictionary.

Notes
-----
- RA and Dec are set to dummy values as they are not modified via the ToO form.
- The epoch is set to a standard value of "J2000".
"""
# Assign target ID if provided.
self._target_id = data.get("hiddenTargetIdInput")

return {
"sidereal": {
"radialVelocity": {
"kilometersPerSecond": data.get("radialVelocityInput")
},
"parallax": {"milliarcseconds": data.get("parallaxInput")},
"properMotion": {
"ra": {"milliarcsecondsPerYear": data.get("uRaInput")},
"dec": {"milliarcsecondsPerYear": data.get("uDecInput")},
},
# RA and Dec are not modified via TOO form, set to dummy values.
"ra": {"degrees": None},
"dec": {"degrees": None},
# Use standard epoch.
"epoch": "J2000",
},
# Placeholder for other serializer field.
"sourceProfile": None,
}

@property
def target_id(self) -> str | None:
return getattr(self, "_target_id", None)
Loading