diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/models/scheduling.py b/SAS/TMSS/backend/src/tmss/tmssapp/models/scheduling.py index 7b3200b15cc433f0feaa38cb77c72c8927bcd279..c31f5a2fae35c15755121aeb29db71c3a31525ee 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/models/scheduling.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/models/scheduling.py @@ -50,7 +50,6 @@ class SubtaskState(AbstractChoice): CANCELLED = "cancelled" ERROR = "error" UNSCHEDULABLE = "unschedulable" - OBSOLETE = "obsolete" class SubtaskType(AbstractChoice): @@ -155,12 +154,14 @@ class Subtask(BasicCommon, ProjectPropertyMixin, TemplateSchemaMixin): raw_feedback = CharField(null=True, max_length=1048576, help_text='The raw feedback for this Subtask') global_identifier = OneToOneField('SIPidentifier', null=False, editable=False, on_delete=PROTECT, help_text='The global unique identifier for LTA SIP.') path_to_project = 'task_blueprint__scheduling_unit_blueprint__draft__scheduling_set__project' + obsolete_since = DateTimeField(null=True, help_text='When this subtask was marked obsolete, or NULL if not obsolete (NULLable).') def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # keep original state for logging self.__original_state_id = self.state_id + self.__original_obsolete_since = self.obsolete_since @property def duration(self) -> timedelta: @@ -317,6 +318,10 @@ class Subtask(BasicCommon, ProjectPropertyMixin, TemplateSchemaMixin): if self.scheduled_on_sky_start_time is None: raise SubtaskSchedulingException("Cannot schedule subtask id=%s when start time is 'None'." % (self.pk, )) + # make sure that obsolete_since can only be set, but not unset + if self.obsolete_since != self.__original_obsolete_since and self.__original_obsolete_since is not None: + raise ValidationError("This Subtask has been marked obsolete on %s and that cannot be changed to %s" % (self.__original_obsolete_since, self.obsolete_since)) + # set actual_process_start_time when subtask goes to STARTED state if self.state.value == SubtaskState.Choices.STARTED.value and self.__original_state_id == SubtaskState.Choices.STARTING.value: self.actual_process_start_time = datetime.utcnow() diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/models/specification.py b/SAS/TMSS/backend/src/tmss/tmssapp/models/specification.py index 9581b2e558a93920b4e746e89b9263e8e70ce3b0..ed9abfb39c0bd07a6df9c375c0f376408d83d42a 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/models/specification.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/models/specification.py @@ -585,7 +585,6 @@ class SchedulingUnitBlueprint(ProjectPropertyMixin, TemplateSchemaMixin, NamedCo INGESTING = "ingesting" SCHEDULED = "scheduled" SCHEDULABLE = "schedulable" - OBSOLETE = "obsolete" # todo: are many of these fields supposed to be immutable in the database? # Or are we fine to just not allow most users to change them? @@ -857,9 +856,6 @@ class SchedulingUnitBlueprint(ProjectPropertyMixin, TemplateSchemaMixin, NamedCo return SchedulingUnitBlueprint.Status.CANCELLED.value elif self._any_task_error(status_overview_counter): return SchedulingUnitBlueprint.Status.ERROR.value - elif self._any_task_obsolete(status_overview_counter): - # TODO: in TMSS-850 implement the various conditional aggregations for the 'obsolete' vs 'finished' states - return SchedulingUnitBlueprint.Status.OBSOLETE.value elif self._any_task_started_observed_finished(status_overview_counter): if not self._all_observation_task_observed_finished(status_overview_counter_per_type): return SchedulingUnitBlueprint.Status.OBSERVING.value @@ -1127,7 +1123,6 @@ class TaskBlueprint(ProjectPropertyMixin, TemplateSchemaMixin, NamedCommon): FINISHED = "finished" CANCELLED = "cancelled" ERROR = "error" - OBSOLETE = "obsolete" OBSERVED = "observed" STARTED = "started" SCHEDULED = "scheduled" @@ -1239,6 +1234,16 @@ class TaskBlueprint(ProjectPropertyMixin, TemplateSchemaMixin, NamedCommon): else: return None + @cached_property + def obsolete_since(self) -> datetime or None: + '''return the earliest obsolete_since time of all subtasks of this task + ''' + subtasks_with_obsolete_time = list(filter(lambda x: x.obsolete_since is not None, self.subtasks.all())) + if subtasks_with_obsolete_time: + return min(subtasks_with_obsolete_time, key=lambda x: x.obsolete_since).obsolete_since + else: + return None + @property def status(self): """ @@ -1267,10 +1272,6 @@ class TaskBlueprint(ProjectPropertyMixin, TemplateSchemaMixin, NamedCommon): if any(s for s in subtasks if s['state'] == 'error'): return TaskBlueprint.Status.ERROR.value - # TODO: in TMSS-850 implement the various conditional aggregations for the 'obsolete' vs 'finished' states - if any(s for s in subtasks if s['state'] == 'obsolete'): - return TaskBlueprint.Status.OBSOLETE.value - observations = [s for s in subtasks if s['specifications_template__type_id'] == 'observation'] if observations and all(obs and obs['state'] in ('finishing', 'finished') for obs in observations): return TaskBlueprint.Status.OBSERVED.value diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/populate.py b/SAS/TMSS/backend/src/tmss/tmssapp/populate.py index a4f69a6cb4701301e01d6a7b3c3c397939de473c..ffbfa1b45fb8f4abf272bb7744753884f28b75ff 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/populate.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/populate.py @@ -151,7 +151,6 @@ def populate_subtask_allowed_state_transitions(apps, schema_editor): CANCELLED = SubtaskState.objects.get(value=SubtaskState.Choices.CANCELLED.value) ERROR = SubtaskState.objects.get(value=SubtaskState.Choices.ERROR.value) UNSCHEDULABLE = SubtaskState.objects.get(value=SubtaskState.Choices.UNSCHEDULABLE.value) - OBSOLETE = SubtaskState.objects.get(value=SubtaskState.Choices.OBSOLETE.value) SubtaskAllowedStateTransitions.objects.bulk_create([ SubtaskAllowedStateTransitions(old_state=None, new_state=DEFINING), @@ -180,10 +179,6 @@ def populate_subtask_allowed_state_transitions(apps, schema_editor): SubtaskAllowedStateTransitions(old_state=FINISHING, new_state=ERROR), SubtaskAllowedStateTransitions(old_state=CANCELLING, new_state=ERROR), - # allow transition from the "end"-states cancelled/error to obsolete to indicate user-intent - SubtaskAllowedStateTransitions(old_state=CANCELLED, new_state=OBSOLETE), - SubtaskAllowedStateTransitions(old_state=ERROR, new_state=OBSOLETE), - SubtaskAllowedStateTransitions(old_state=DEFINED, new_state=CANCELLING), SubtaskAllowedStateTransitions(old_state=SCHEDULED, new_state=CANCELLING), SubtaskAllowedStateTransitions(old_state=QUEUED, new_state=CANCELLING), diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/tasks.py b/SAS/TMSS/backend/src/tmss/tmssapp/tasks.py index aa9b0b3a0a68df9fda3df95771a725d07fa1a2fe..040748aaf0c49e1cfa1132cb93f38f57b2df4be5 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/tasks.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/tasks.py @@ -504,10 +504,9 @@ def cancel_task_blueprint(task_blueprint: TaskBlueprint) -> TaskBlueprint: def mark_task_blueprint_as_obsolete(task_blueprint: TaskBlueprint) -> TaskBlueprint: '''Convenience method: mark all cancelled/error subtasks in the task_blueprint as obsolete''' - obsolete_state = models.SubtaskState.objects.get(value=models.SubtaskState.Choices.OBSOLETE.value) - transitionable_states = models.SubtaskAllowedStateTransitions.allowed_old_states(obsolete_state) - for subtask in task_blueprint.subtasks.filter(state__in=transitionable_states): - subtask.state = obsolete_state + now = datetime.utcnow() + for subtask in task_blueprint.subtasks: + subtask.obsolete_since = now subtask.save() task_blueprint.refresh_from_db() return task_blueprint diff --git a/SAS/TMSS/backend/test/t_subtasks.py b/SAS/TMSS/backend/test/t_subtasks.py index d7cad57c1bc0809fe3e84544dad637e4da086bf0..74179a75127dfd1f1ca3ba089b4b1d9b8336132d 100755 --- a/SAS/TMSS/backend/test/t_subtasks.py +++ b/SAS/TMSS/backend/test/t_subtasks.py @@ -800,9 +800,6 @@ class SubtaskAllowedStateTransitionsTest(unittest.TestCase): # there should be no state to go to from FINISHED self.assertEqual(0, SubtaskAllowedStateTransitions.objects.filter(old_state__value=SubtaskState.Choices.FINISHED.value).count()) - # there should be no state to go to from OBSOLETE - self.assertEqual(0, SubtaskAllowedStateTransitions.objects.filter(old_state__value=SubtaskState.Choices.OBSOLETE.value).count()) - def test_illegal_state_transitions(self): for state_value in [choice.value for choice in SubtaskState.Choices]: # assume helper method set_subtask_state_following_allowed_transitions is working (see other tests above) diff --git a/SAS/TMSS/backend/test/test_utils.py b/SAS/TMSS/backend/test/test_utils.py index 3f24e55cf19187411df45887a18b92e0d641090a..9f96fd02520f58ce7aed5bd77b386121d7a65ad3 100644 --- a/SAS/TMSS/backend/test/test_utils.py +++ b/SAS/TMSS/backend/test/test_utils.py @@ -92,7 +92,7 @@ def set_subtask_state_following_allowed_transitions(subtask: typing.Union[Subtas subtask = Subtask.objects.get(id=subtask) # end states that we cannot get out of accoring to the design - END_STATE_VALUES = (SubtaskState.Choices.FINISHED.value, SubtaskState.Choices.UNSCHEDULABLE.value, SubtaskState.Choices.OBSOLETE.value) + END_STATE_VALUES = (SubtaskState.Choices.FINISHED.value, SubtaskState.Choices.UNSCHEDULABLE.value) while subtask.state.value != desired_state_value and (subtask.state.value not in END_STATE_VALUES): # handle "unsuccessful path" to cancelled/canceling end state @@ -112,17 +112,6 @@ def set_subtask_state_following_allowed_transitions(subtask: typing.Union[Subtas SubtaskState.Choices.CANCELLING.value): subtask.state = SubtaskState.objects.get(value=SubtaskState.Choices.ERROR.value) - # handle "unsuccessful path" to OBSOLETE end state (via CANCELLED) - elif desired_state_value == SubtaskState.Choices.OBSOLETE.value: - if subtask.state.value == SubtaskState.Choices.DEFINING.value: - subtask.state = SubtaskState.objects.get(value=SubtaskState.Choices.DEFINED.value) - elif subtask.state.value in (SubtaskState.Choices.DEFINED.value, SubtaskState.Choices.SCHEDULED.value, SubtaskState.Choices.QUEUED.value, SubtaskState.Choices.ERROR.value): - subtask.state = SubtaskState.objects.get(value=SubtaskState.Choices.CANCELLING.value) - elif subtask.state.value == SubtaskState.Choices.CANCELLING.value: - subtask.state = SubtaskState.objects.get(value=SubtaskState.Choices.CANCELLED.value) - elif subtask.state.value == SubtaskState.Choices.CANCELLED.value: - subtask.state = SubtaskState.objects.get(value=SubtaskState.Choices.OBSOLETE.value) - # handle "unsuccessful path" to unschedulable end state elif desired_state_value == SubtaskState.Choices.UNSCHEDULABLE.value and subtask.state.value == SubtaskState.Choices.SCHEDULING.value: subtask.state = SubtaskState.objects.get(value=SubtaskState.Choices.UNSCHEDULABLE.value)