diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/models/scheduling.py b/SAS/TMSS/backend/src/tmss/tmssapp/models/scheduling.py index c31f5a2fae35c15755121aeb29db71c3a31525ee..b543d778a05c4c0533c6f67a4181be5e282038d3 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/models/scheduling.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/models/scheduling.py @@ -161,7 +161,17 @@ class Subtask(BasicCommon, ProjectPropertyMixin, TemplateSchemaMixin): # keep original state for logging self.__original_state_id = self.state_id - self.__original_obsolete_since = self.obsolete_since + + # keep original obsolete_since to detect changes on save + # Note: we cannot use self.obsolete_since here since that causes an infinite loop of update_from_db + if 'obsolete_since' in kwargs.keys(): + self.__original_obsolete_since = kwargs['obsolete_since'] + else: + field_names = [f.name for f in self._meta.fields] + if len(args) == len(field_names): + self.__original_obsolete_since = args[field_names.index('obsolete_since')] + else: + self.__original_obsolete_since = None @property def duration(self) -> timedelta: @@ -318,7 +328,7 @@ 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 + # make sure that obsolete_since can only be set when it is None, but prevents changes: 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)) diff --git a/SAS/TMSS/backend/test/t_scheduling.py b/SAS/TMSS/backend/test/t_scheduling.py index 2bc761f60cb8a2fd2cfb0b28657a159e747b00e8..da949f885c0705517dd13bc262bcc2937266c870 100755 --- a/SAS/TMSS/backend/test/t_scheduling.py +++ b/SAS/TMSS/backend/test/t_scheduling.py @@ -78,6 +78,8 @@ from lofar.sas.tmss.tmss.tmssapp.tasks import * from lofar.sas.tmss.test.test_utils import set_subtask_state_following_allowed_transitions from lofar.messaging.rpc import RPCService, ServiceMessageHandler import threading +import dateutil.parser +from django.core.exceptions import ValidationError def create_subtask_object_for_testing(subtask_type_value, subtask_state_value): """ @@ -230,16 +232,28 @@ class SchedulingTest(unittest.TestCase): self.assertEqual('cancelled', subtask['state_value']) # mark it as obsolete... (the user thereby states that the cancelled subtask will is not to be used again) + self.assertIsNone(subtask['obsolete_since']) + before = datetime.utcnow() subtask = client.mark_subtask_as_obsolete(subtask_id) - self.assertEqual('obsolete', subtask['state_value']) + after = datetime.utcnow() + obsolete_since = dateutil.parser.parse(subtask['obsolete_since'], ignoretz=True) + self.assertIsNotNone(obsolete_since) + self.assertLess(before, obsolete_since) + self.assertGreater(after, obsolete_since) # scheduling should fail with self.assertRaises(Exception): client.schedule_subtask(subtask_id) - # and status should still be obsolete + # marking an obsolete subtask as obsolete again should be prevented + with self.assertRaises(ValidationError) as context: + subtask = client.mark_subtask_as_obsolete(subtask_id) + self.assertIn("has been marked obsolete on %s" % obsolete_since, str(context)) + + # and obsolete_since timestamp should still be the same as before subtask = client.get_subtask(subtask_id) - self.assertEqual('obsolete', subtask['state_value']) + obsolete_since_new = dateutil.parser.parse(subtask['obsolete_since'], ignoretz=True) + self.assertEqual(obsolete_since, obsolete_since_new) def test_cancel_scheduled_observation_subtask(self): with tmss_test_env.create_tmss_client() as client: diff --git a/SAS/TMSS/backend/test/t_tmssapp_specification_REST_API.py b/SAS/TMSS/backend/test/t_tmssapp_specification_REST_API.py index af16f953e9eccda2773674ecd9e9a321408658ee..6247d65dd99ebe8f211cc45437dae6acaf34b62b 100755 --- a/SAS/TMSS/backend/test/t_tmssapp_specification_REST_API.py +++ b/SAS/TMSS/backend/test/t_tmssapp_specification_REST_API.py @@ -2384,7 +2384,7 @@ class TaskBlueprintTestCase(unittest.TestCase): # assert response_1 = GET_and_assert_equal_expected_code(self, BASE_URL + '/task_blueprint/?status=defined&status=finished', 200) - response_2 = GET_and_assert_equal_expected_code(self, BASE_URL + '/task_blueprint/?status=obsolete', 200) + response_2 = GET_and_assert_equal_expected_code(self, BASE_URL + '/task_blueprint/?status=schedulable', 200) GET_and_assert_equal_expected_code(self, BASE_URL + '/task_blueprint/?status=gibberish', 400) self.assertGreater(response_1['count'], 0) self.assertEqual(response_2['count'], 0) diff --git a/SAS/TMSS/client/lib/tmss_http_rest_client.py b/SAS/TMSS/client/lib/tmss_http_rest_client.py index bb174a684c6b243ba3a27fe40056efbe1d7854e1..b8a86dd6113609614702c10c77f3f76f7adc4d80 100644 --- a/SAS/TMSS/client/lib/tmss_http_rest_client.py +++ b/SAS/TMSS/client/lib/tmss_http_rest_client.py @@ -161,6 +161,20 @@ class TMSSsession(object): content = response.content.decode('utf-8') raise Exception("Could not set status with url %s - %s %s - %s" % (response.request.url, response.status_code, responses.get(response.status_code), content)) + def set_subtask_obsolete_since_and_scheduled_on_sky_to_now(self, subtask_id: int) -> {}: + '''set the subtask obsolete flag and scheduled on sky start time to now and return the subtask with its new state, or raise on error''' + now = datetime.utcnow().isoformat() + json_doc = {'obsolete_since': "%s" % now, + 'scheduled_on_sky_stop_time': "%s" % now} + logger.info("marking subtask id=%s as obsolete", subtask_id) + response = self.session.patch(url='%s/subtask/%s/' % (self.api_url, subtask_id), json=json_doc) + + if response.status_code >= 200 and response.status_code < 300: + return json.loads(response.content.decode('utf-8')) + + content = response.content.decode('utf-8') + raise Exception("Could not mark subtask as obsolete with url %s - %s %s - %s" % (response.request.url, response.status_code, responses.get(response.status_code), content)) + def wait_for_subtask_status(self, subtask_id: int, expected_status: typing.Union[str,list,tuple], timeout: int=10, poll_interval: float=1.0) -> dict: """wait for the subtask with the given id to have/get the expected status. @@ -437,7 +451,7 @@ class TMSSsession(object): def mark_subtask_as_obsolete(self, subtask_id: int, retry_count: int=0) -> {}: """mark the subtask for the given subtask_id as obsolete. returns the marked_as_obsolete subtask upon success, or raises.""" - return self.set_subtask_status(subtask_id=subtask_id, status='obsolete') + return self.set_subtask_obsolete_since_and_scheduled_on_sky_to_now(subtask_id=subtask_id) def mark_task_blueprint_as_obsolete(self, task_blueprint_id: int, retry_count: int=0) -> {}: """mark the task_blueprint for the given task_blueprint_id as obsolete.