From 0741fb8cec2f5b356eaa094ab7c3496069fc5c1c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=B6rn=20K=C3=BCnsem=C3=B6ller?=
 <jkuensem@physik.uni-bielefeld.de>
Date: Tue, 12 Oct 2021 09:30:04 +0200
Subject: [PATCH] TMSS-780: replace obsolete status by obsolete_since
 timestamp/flag

---
 .../src/tmss/tmssapp/models/scheduling.py     |  7 ++++++-
 .../src/tmss/tmssapp/models/specification.py  | 19 ++++++++++---------
 SAS/TMSS/backend/src/tmss/tmssapp/populate.py |  5 -----
 SAS/TMSS/backend/src/tmss/tmssapp/tasks.py    |  7 +++----
 SAS/TMSS/backend/test/t_subtasks.py           |  3 ---
 SAS/TMSS/backend/test/test_utils.py           | 13 +------------
 6 files changed, 20 insertions(+), 34 deletions(-)

diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/models/scheduling.py b/SAS/TMSS/backend/src/tmss/tmssapp/models/scheduling.py
index 7b3200b15cc..c31f5a2fae3 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 9581b2e558a..ed9abfb39c0 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 a4f69a6cb47..ffbfa1b45fb 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 aa9b0b3a0a6..040748aaf0c 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 d7cad57c1bc..74179a75127 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 3f24e55cf19..9f96fd02520 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)
-- 
GitLab