diff --git a/SAS/TMSS/src/tmss/tmssapp/models/specification.py b/SAS/TMSS/src/tmss/tmssapp/models/specification.py index 710b9cdd942de6a52ed6c40ebd92d774602b61ae..353f7a16ea0eb7c3f5977161162336f3214e675a 100644 --- a/SAS/TMSS/src/tmss/tmssapp/models/specification.py +++ b/SAS/TMSS/src/tmss/tmssapp/models/specification.py @@ -436,7 +436,8 @@ class CycleQuota(Model): class Project(NamedCommonPK): - cycles = ManyToManyField('Cycle', blank=True, related_name='projects', help_text='Cycles to which this project belongs (NULLable).') + # todo: cycles should be protected since we have to manually decide to clean up projects with a cycle or keep them without cycle, however, ManyToManyField does not allow for that + cycles = ManyToManyField('Cycle', related_name='projects', null=True, help_text='Cycles to which this project belongs (NULLable).') priority_rank = FloatField(null=False, help_text='Priority of this project w.r.t. other projects. Projects can interrupt observations of lower-priority projects.') # todo: add if needed: validators=[MinValueValidator(0.0), MaxValueValidator(1.0)] trigger_priority = IntegerField(default=1000, help_text='Priority of this project w.r.t. triggers.') # todo: verify meaning and add to help_text: "Triggers with higher priority than this threshold can interrupt observations of projects." can_trigger = BooleanField(default=False, help_text='True if this project is allowed to supply observation requests on the fly, possibly interrupting currently running observations (responsive telescope).') @@ -587,6 +588,19 @@ class SchedulingUnitDraft(NamedCommon): class SchedulingUnitBlueprint(NamedCommon): + class Status(Enum): + DEFINED = "defined" + FINISHED = "finished" + CANCELLED = "cancelled" + ERROR = "error" + OBSERVING = "observing" + OBSERVED = "observed" + PROCESSING = "processing" + PROCESSED = "processed" + INGESTING = "ingesting" + SCHEDULED = "scheduled" + SCHEDULABLE = "schedulable" + requirements_doc = JSONField(help_text='Scheduling and/or quality requirements for this scheduling unit (IMMUTABLE).') do_cancel = BooleanField() requirements_template = ForeignKey('SchedulingUnitTemplate', on_delete=CASCADE, help_text='Schema used for requirements_doc (IMMUTABLE).') @@ -657,36 +671,34 @@ class SchedulingUnitBlueprint(NamedCommon): # Determine status per task_type (unfortunately did not manage with updatable view) status_overview_counter = Counter() - status_overview_counter_per_type = {type.value:Counter() for type in TaskType.Choices} - for tb in self.task_blueprints.prefetch_related('specifications_template').all(): + status_overview_counter_per_type = {type.value: Counter() for type in TaskType.Choices} + for tb in self.task_blueprints.select_related('specifications_template').all(): status_overview_counter[tb.status] += 1 status_overview_counter_per_type[tb.specifications_template.type.value][tb.status] += 1 # The actual determination of the SchedulingunitBlueprint status if not self._task_graph_instantiated(): - status = "defined" + return SchedulingUnitBlueprint.Status.DEFINED.value elif self._all_task_finished(status_overview_counter): - status = "finished" + return SchedulingUnitBlueprint.Status.FINISHED.value elif self._any_task_cancelled(status_overview_counter): - status = "cancelled" + return SchedulingUnitBlueprint.Status.CANCELLED.value elif self._any_task_error(status_overview_counter): - status = "error" + return SchedulingUnitBlueprint.Status.ERROR.value elif self._any_task_started_observed_finished(status_overview_counter): if not self._all_observation_task_observed_finished(status_overview_counter_per_type): - status = "observing" + return SchedulingUnitBlueprint.Status.OBSERVING.value elif not self._any_processing_task_started_or_finished(status_overview_counter_per_type): - status = "observed" + return SchedulingUnitBlueprint.Status.OBSERVED.value elif not self._all_processing_tasks_and_observation_tasks_finished(status_overview_counter_per_type): - status = "processing" + return SchedulingUnitBlueprint.Status.PROCESSING.value elif not self._any_ingest_task_started(status_overview_counter_per_type): - status = "processed" + return SchedulingUnitBlueprint.Status.PROCESSED.value else: - status = "ingesting" + return SchedulingUnitBlueprint.Status.INGESTING.value elif self._any_task_scheduled(status_overview_counter): - status = "scheduled" - else: - status = "schedulable" - return status + return SchedulingUnitBlueprint.Status.SCHEDULED.value + return SchedulingUnitBlueprint.Status.SCHEDULABLE.value def _task_graph_instantiated(self): return self._get_total_nbr_tasks() > 0 @@ -734,15 +746,15 @@ class SchedulingUnitBlueprint(NamedCommon): @staticmethod def _get_total_nbr_observation_tasks(status_overview_counter_per_type): - return len(status_overview_counter_per_type[TaskType.Choices.OBSERVATION.value].elements()) + return len(tuple(status_overview_counter_per_type[TaskType.Choices.OBSERVATION.value].elements())) @staticmethod def _get_total_nbr_processing_tasks(status_overview_counter_per_type): - return len(status_overview_counter_per_type[TaskType.Choices.PIPELINE.value].elements()) + return len(tuple(status_overview_counter_per_type[TaskType.Choices.PIPELINE.value].elements())) @staticmethod def _get_total_nbr_ingest_tasks(status_overview_counter_per_type): - return len(status_overview_counter_per_type[TaskType.Choices.INGEST.value].elements()) + return len(tuple(status_overview_counter_per_type[TaskType.Choices.INGEST.value].elements())) class TaskDraft(NamedCommon): @@ -968,27 +980,28 @@ class TaskBlueprint(NamedCommon): if nr_of_subtasks == 0: return "defined" - if len([s for s in subtasks if s['state'] in ('defining','defined')]) == nr_of_subtasks: + if any(s for s in subtasks if s['state'] == 'defining'): return "defined" if len([s for s in subtasks if s['state'] == 'finished']) == nr_of_subtasks: return "finished" - if any(s for s in subtasks if s['state'] == 'scheduled'): - return "scheduled" - if any(s for s in subtasks if s['state'] in ('cancelling', 'cancelled')): return "cancelled" if any(s for s in subtasks if s['state'] == 'error'): return "error" - if any(s for s in subtasks if s['specifications_template__type_id'] == 'observation' and s['state'] in ('cancelling', 'cancelled')): + 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 "observed" if any(s for s in subtasks if s['state'] in ('starting','started','queueing','queued','finishing','finished')): return "started" + if any(s for s in subtasks if s['state'] == 'scheduled'): + return "scheduled" + return "schedulable" diff --git a/SAS/TMSS/test/t_tasks.py b/SAS/TMSS/test/t_tasks.py index 1b4aefc9e56a1dccccb7fdbf92abac85068ace38..6bca6f3f1bc03fac0d59777fb195b5a50230921c 100755 --- a/SAS/TMSS/test/t_tasks.py +++ b/SAS/TMSS/test/t_tasks.py @@ -475,7 +475,7 @@ class SchedulingUnitBlueprintStateTest(unittest.TestCase): subtask.save() # Check task.status as precondition self.assertEqual(task_state, task.status, - "PRECONDITION DOES NOT MET. Expect %s task to be equal to %s (but is %s)" % ( + "INCORRECT PRECONDITION. Expected %s task to have status=%s, but actual status=%s)" % ( task_type, task_state, task.status)) def test_state_with_no_tasks(self): diff --git a/SAS/TMSS/test/tmss_test_data_rest.py b/SAS/TMSS/test/tmss_test_data_rest.py index e0e121edb5c823c74e8619fd8e59c131bebc11de..82f35cf01ae41d98230365c02cc85fbdc0ec8908 100644 --- a/SAS/TMSS/test/tmss_test_data_rest.py +++ b/SAS/TMSS/test/tmss_test_data_rest.py @@ -199,7 +199,7 @@ class TMSSRESTTestDataCreator(): "trigger_priority": 1000, "can_trigger": False, "private_data": True, - "cycles": [], + "cycles": [self.post_data_and_get_url(self.Cycle(), '/cycle')], "archive_subdirectory": 'my_project/'} def ResourceType(self, description="my resource_type description"):