Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions docs/changes/466.new.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Implemented workflow state handling for GPP observations: Added serializer to validate and convert workflow state selections, enabling users to view and update observation workflow states in the ToO form.
44 changes: 27 additions & 17 deletions src/goats_tom/api_views/gpp/toos.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
ExposureModeSerializer,
InstrumentRegistry,
SourceProfileSerializer,
WorkflowStateSerializer,
)

# Import type for instrument input models.
Expand Down Expand Up @@ -80,6 +81,7 @@ def create(self, request: Request, *args, **kwargs) -> Response:
# TODO: Format source profile from request data.
# print(self._format_clone_observation_input(request.data))
# print(self._format_clone_target_input(request.data))
# print(self._format_workflow_state_properties(request.data))

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

Expand Down Expand Up @@ -148,6 +150,31 @@ def _format_instrument_properties(
else None
)

def _format_workflow_state_properties(
self, data: dict[str, Any]
) -> ObservationWorkflowState | None:
"""Format workflow state property from the request data.

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

Returns
-------
ObservationWorkflowState | None
An ObservationWorkflowState enum instance or ``None`` if no workflow
state is provided.

Raises
------
serializers.ValidationError
If any error occurs during parsing or validation of workflow state values.
"""
workflow_state_serializer = WorkflowStateSerializer(data=data)
workflow_state_serializer.is_valid(raise_exception=True)
return workflow_state_serializer.workflow_state_enum

def _format_elevation_range_properties(
self, data: dict[str, Any]
) -> ElevationRangeInput | None:
Expand Down Expand Up @@ -411,20 +438,3 @@ def _set_workflow_state(
return async_to_sync(client.workflow_state.update_by_id)(
workflow_state=workflow_state, observation_id=observation_id
)

def _format_workflow_state_properties(
self, data: dict[str, Any]
) -> ObservationWorkflowState:
"""Format the workflow state property from the request data.

Parameters
----------
data : dict[str, Any]
The request data containing workflow state property.

Returns
-------
ObservationWorkflowState
The formatted workflow state property.
"""
raise NotImplementedError
2 changes: 2 additions & 0 deletions src/goats_tom/serializers/gpp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from .exposure_mode import ExposureModeSerializer
from .instruments import InstrumentRegistry
from .source_profile import SourceProfileSerializer
from .workflow_state import WorkflowStateSerializer

__all__ = [
"BrightnessesSerializer",
Expand All @@ -14,4 +15,5 @@
"InstrumentRegistry",
"CloneTargetSerializer",
"CloneObservationSerializer",
"WorkflowStateSerializer",
]
31 changes: 31 additions & 0 deletions src/goats_tom/serializers/gpp/_base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""
Base serializer for GPP serializers.
"""

__all__ = ["_BaseSerializer"]

from typing import Any

from rest_framework import serializers

from .utils import normalize


class _BaseSerializer(serializers.Serializer):
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)
22 changes: 2 additions & 20 deletions src/goats_tom/serializers/gpp/clone_observation.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@
)
from rest_framework import serializers

from .utils import normalize
from ._base import _BaseSerializer


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

Expand Down Expand Up @@ -58,24 +58,6 @@ class CloneObservationSerializer(serializers.Serializer):
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
Expand Down
22 changes: 2 additions & 20 deletions src/goats_tom/serializers/gpp/clone_target.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@

from rest_framework import serializers

from .utils import normalize
from ._base import _BaseSerializer


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

This serializer processes hidden input fields related to target cloning, such as
Expand All @@ -26,24 +26,6 @@ class CloneTargetSerializer(serializers.Serializer):
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
Expand Down
58 changes: 58 additions & 0 deletions src/goats_tom/serializers/gpp/workflow_state.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
__all__ = ["WorkflowStateSerializer"]

from gpp_client.api.enums import ObservationWorkflowState
from rest_framework import serializers

from ._base import _BaseSerializer


class WorkflowStateSerializer(_BaseSerializer):
workflowStateSelect = serializers.ChoiceField(
choices=[c.value for c in ObservationWorkflowState],
required=False,
allow_blank=False,
)

def validate(self, data: dict[str, str]) -> dict[str, str]:
"""
Validate and return the data.

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

Returns
-------
dict[str, str]
The validated data dictionary.
"""
self._workflow_state = data.get("workflowStateSelect")
self._workflow_state_enum = (
ObservationWorkflowState(self._workflow_state)
if self._workflow_state
else None
)
return data

@property
def workflow_state(self) -> str | None:
"""Get the workflow state value.

Returns
-------
str | None
The workflow state value if set, otherwise None.
"""
return getattr(self, "_workflow_state", None)

@property
def workflow_state_enum(self) -> ObservationWorkflowState | None:
"""Get the workflow state enum.

Returns
-------
ObservationWorkflowState | None
The workflow state enum if set, otherwise None.
"""
return getattr(self, "_workflow_state_enum", None)
19 changes: 19 additions & 0 deletions src/goats_tom/static/js/gpp/fields.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
*
* options : array (optional)
* For select elements, the list of options to display.
* Each option can be a string (used for both value and label) or an object
* with `labelText`, `value`, and `disabled` properties.
*
* type : string (default: "text")
* Input type attribute (e.g., "number", "text", etc.).
Expand Down Expand Up @@ -91,6 +93,23 @@ const SHARED_FIELDS = [
showIfMode: "normal",
readOnly: "normal",
},
{
labelText: "State",
path: "workflow.value.state",
id: "workflowState",
colSize: "col-12",
element: "select",
readOnly: "normal",
options: [
{ value: "READY", labelText: "Ready" },
{ value: "DEFINED", labelText: "Defined" },
{ value: "INACTIVE", labelText: "Inactive" },
{ value: "ONGOING", labelText: "Ongoing", disabled: true },
{ value: "COMPLETED", labelText: "Completed", disabled: true },
{ value: "UNAPPROVED", labelText: "Unapproved", disabled: true },
{ value: "UNDEFINED", labelText: "Undefined", disabled: true },
],
},
{
labelText: "Right Ascension",
path: "targetEnvironment.firstScienceTarget.sidereal.ra.hms",
Expand Down
6 changes: 5 additions & 1 deletion src/goats_tom/static/js/gpp/observation_form.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,6 @@ class ObservationForm {
meta.showIfMode !== "both" &&
meta.showIfMode !== this.#mode
) {
console.log("Skipping field:", meta.id);
return;
}

Expand Down Expand Up @@ -289,6 +288,11 @@ class ObservationForm {
// User passed in JSON object {value: "", labelText: ""}.
optionEl.value = opt.value;
optionEl.textContent = opt.labelText;

// Handle disabled state.
if (opt.disabled) {
optionEl.disabled = true;
}
}
if (optionEl.value === value) {
optionEl.selected = true;
Expand Down
1 change: 0 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import pytest
from django.conf import settings


@pytest.fixture(scope="session", autouse=True)
def temp_media_root():
original_media_root = settings.MEDIA_ROOT
Expand Down
48 changes: 47 additions & 1 deletion tests/goats_tom/api_views/gpp/test_toos.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,6 @@ def test_workflow_state_methods(self, mocker) -> None:
[
"_format_observation_properties",
"_format_target_properties",
"_format_workflow_state_properties",
],
)
def test_not_implemented_methods(self, method_name: str) -> None:
Expand Down Expand Up @@ -537,3 +536,50 @@ def test_format_clone_target_input_invalid_data(self, mocker) -> None:

mock_serializer.assert_called_once_with(data={"field": "invalid"})
mock_serializer_instance.is_valid.assert_called_once_with(raise_exception=True)

def test_format_workflow_state_properties_valid_data(self, mocker) -> None:
"""Test _format_workflow_state_properties with valid data."""
mock_serializer = mocker.patch(
"goats_tom.api_views.gpp.toos.WorkflowStateSerializer"
)
mock_serializer_instance = mock_serializer.return_value
mock_serializer_instance.is_valid.return_value = True
mock_serializer_instance.workflow_state_enum = ObservationWorkflowState.READY

viewset = GPPTooViewSet()
result = viewset._format_workflow_state_properties({"state": "READY"})

assert result == ObservationWorkflowState.READY
mock_serializer.assert_called_once_with(data={"state": "READY"})
mock_serializer_instance.is_valid.assert_called_once_with(raise_exception=True)

def test_format_workflow_state_properties_no_data(self, mocker) -> None:
"""Test _format_workflow_state_properties with no workflow state data."""
mock_serializer = mocker.patch(
"goats_tom.api_views.gpp.toos.WorkflowStateSerializer"
)
mock_serializer_instance = mock_serializer.return_value
mock_serializer_instance.is_valid.return_value = True
mock_serializer_instance.workflow_state_enum = None

viewset = GPPTooViewSet()
result = viewset._format_workflow_state_properties({})

assert result is None
mock_serializer.assert_called_once_with(data={})
mock_serializer_instance.is_valid.assert_called_once_with(raise_exception=True)

def test_format_workflow_state_properties_invalid_data(self, mocker) -> None:
"""Test _format_workflow_state_properties with invalid data."""
mock_serializer = mocker.patch(
"goats_tom.api_views.gpp.toos.WorkflowStateSerializer"
)
mock_serializer_instance = mock_serializer.return_value
mock_serializer_instance.is_valid.side_effect = serializers.ValidationError

viewset = GPPTooViewSet()
with pytest.raises(serializers.ValidationError):
viewset._format_workflow_state_properties({"state": "INVALID"})

mock_serializer.assert_called_once_with(data={"state": "INVALID"})
mock_serializer_instance.is_valid.assert_called_once_with(raise_exception=True)
Loading