diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/specification.py b/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/specification.py index fcca3ccbc408a6366c8326add37f1902d4750bbb..7363ab0cc96d0d3a6ef9e2b9a61b938392b05fd9 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/specification.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/specification.py @@ -1274,6 +1274,38 @@ class SchedulingUnitBlueprintViewSet(LOFARViewSet): return Response(serializers.SchedulingUnitDraftExtendedSerializer(scheduling_unit_draft_copy, context={'request':request}).data, status=status.HTTP_201_CREATED) + + @swagger_auto_schema(responses={201: 'The newly created SchedulingUnitBlueprint-copy with the given station(s) removed', + 403: 'forbidden'}, + operation_description="Create a new SchedulingUnitBlueprint-copy with the given station(s) removed, cancel this unit and mark it obsolete") + @action(methods=['post'], detail=True, url_name="create_copy_without_given_stations") + def create_copy_without_given_stations(self, request, pk=None): + scheduling_unit_blueprint = get_object_or_404(models.SchedulingUnitBlueprint, pk=pk) + stations_to_be_removed = set([s.upper() for s in request.data['stations_to_be_removed']]) + with transaction.atomic(): + # start with creating a draft copy + scheduling_unit_draft_copy = create_scheduling_unit_draft_from_scheduling_unit_blueprint(scheduling_unit_blueprint, add_copy_annotations=False) + + # and then remove the given stations from each task + for task in scheduling_unit_draft_copy.tasks.all(): + if 'station_configuration' in task.specifications_doc: + if 'station_groups' in task.specifications_doc['station_configuration']: + for group in task.specifications_doc['station_configuration']['station_groups']: + group['stations'] = sorted(list(set(group['stations']) - stations_to_be_removed)) + group['max_nr_missing'] = min(len(group['stations'])-1, group.get('max_nr_missing', 0)) + task.save() + + # blueprint the copy + scheduling_unit_blueprint_copy = create_scheduling_unit_blueprint_and_tasks_and_subtasks_from_scheduling_unit_draft(scheduling_unit_draft_copy) + + # and cancel and obsolete the original + mark_scheduling_unit_blueprint_as_obsolete(cancel_scheduling_unit_blueprint(scheduling_unit_blueprint)) + + # return the new blueprint + return Response(serializers.SchedulingUnitBlueprintSerializer(scheduling_unit_blueprint_copy, context={'request':request}).data, + status=status.HTTP_201_CREATED) + + @swagger_auto_schema(responses={200: 'The system events of this scheduling unit blueprint', 403: 'forbidden'}, operation_description="Get the system events of this scheduling unit blueprint.") diff --git a/SAS/TMSS/backend/test/t_scheduling_units.py b/SAS/TMSS/backend/test/t_scheduling_units.py index c626dca7ab4c6883cbdd0223d23a8b3e2582616d..a2ec4c2778eabd5efb526b14c964e1744902d777 100644 --- a/SAS/TMSS/backend/test/t_scheduling_units.py +++ b/SAS/TMSS/backend/test/t_scheduling_units.py @@ -1650,6 +1650,50 @@ class SchedulingUnitBlueprintIndirectModificationsTestCase(unittest.TestCase): # the bug from TMSS-2275 used to be that the unit had status SCHEDULABLE. Now it has the correct status CANCELLED. self.assertEqual(models.SchedulingUnitStatus.Choices.CANCELLED.value, unit.status.value) + def test_create_schedulingunitblueprint_copy_without_given_station(self): + with tmss_test_env.create_tmss_client() as client: + # create project, and a scheduling set within it + project = rest_data_creator.post_data_and_get_response_as_json_object(rest_data_creator.Project(auto_ingest=True, auto_pin=True), '/project/') + scheduling_set = rest_data_creator.post_data_and_get_response_as_json_object(rest_data_creator.SchedulingSet(project_url=project['url']), '/scheduling_set/') + + # create 'Simple Observation' scheduling unit draft in the scheduling set + observing_strategy_templates = client.get_path_as_json_object('scheduling_unit_observing_strategy_template') + strategy_template = next(ost for ost in observing_strategy_templates if ost['name'] == 'Simple Observation') + self.assertIsNotNone(strategy_template) + scheduling_unit_draft = client.create_scheduling_unit_draft_from_strategy_template(strategy_template['id'], scheduling_set['id']) + + # turn draft into a blueprint + scheduling_unit_blueprint = client.create_scheduling_unit_blueprint_and_tasks_and_subtasks_tree(scheduling_unit_draft['id']) + + # schedule it + scheduling_unit_blueprint = client.schedule_scheduling_unit(scheduling_unit_blueprint['id'], datetime.utcnow() + timedelta(minutes=10)) + self.assertEqual(models.SchedulingUnitStatus.Choices.SCHEDULED.value, scheduling_unit_blueprint['status_value']) + + # get the originally specified group(s) of stations + scheduling_unit_blueprint = client.get_schedulingunit_blueprint(scheduling_unit_blueprint['id'], include_specifications_doc=True) + org_station_groups = scheduling_unit_blueprint['specifications_doc']['tasks']["Observation"]['specifications_doc']['station_configuration']['station_groups'] + + stations_to_be_removed = ['CS002', 'CS005'] + + # reassure these stations are in the original spec + for station in stations_to_be_removed: + self.assertIn(station, str(org_station_groups)) + + # take out the stations (in a new copy) + scheduling_unit_blueprint_copy = client.create_scheduling_unit_blueprint_copy_without_given_stations(scheduling_unit_blueprint['id'], stations_to_be_removed) + + # check it + scheduling_unit_blueprint_copy = client.get_schedulingunit_blueprint(scheduling_unit_blueprint_copy['id'], include_specifications_doc=True) + self.assertNotEqual(scheduling_unit_blueprint['id'], scheduling_unit_blueprint_copy['id']) + + # Check the core feature: are the stations taken out of the new copy? + new_station_groups = scheduling_unit_blueprint_copy['specifications_doc']['tasks']["Observation"]['specifications_doc']['station_configuration']['station_groups'] + for station in stations_to_be_removed: + self.assertNotIn(station, str(new_station_groups)) + + # check the status of the original, is it cancelled and obsolete? + scheduling_unit_blueprint = client.get_schedulingunit_blueprint(scheduling_unit_blueprint['id'], include_specifications_doc=True) + self.assertEqual(models.SchedulingUnitStatus.Choices.CANCELLED.value, scheduling_unit_blueprint['status_value']) if __name__ == "__main__": diff --git a/SAS/TMSS/client/lib/tmss_http_rest_client.py b/SAS/TMSS/client/lib/tmss_http_rest_client.py index fbe748499c9a7a3fc98efee4cfd9add587af47a1..e734e5a17749f388fa8e459327cc34d1cef95ca8 100644 --- a/SAS/TMSS/client/lib/tmss_http_rest_client.py +++ b/SAS/TMSS/client/lib/tmss_http_rest_client.py @@ -644,7 +644,7 @@ class TMSSsession(object): def copy_scheduling_unit_blueprint_specifications_doc_back_into_draft(self, scheduling_unit_blueprint_id: int, including_copies_for_failed_tasks: bool=False, retry_count: int=0) -> {}: """Copy this blueprint's specifications_doc back into the originating draft scheduling unit. if including_copies_for_failed_tasks==True then extend the graph specification with copies for all failed tasks and link them. - returns the scheduling_unit_blueprint (which containts a link to the draft with the updated specifications_doc), or raises.""" + returns the scheduling_unit_blueprint (which contains a link to the draft with the updated specifications_doc), or raises.""" action = 'copy_specifications_doc_including_copies_for_failed_tasks_back_into_draft' if including_copies_for_failed_tasks else 'copy_specifications_doc_back_into_draft' return self.post_to_path_and_get_result_as_json_object('scheduling_unit_blueprint/%s/%s' % (scheduling_unit_blueprint_id, action), retry_count=retry_count) @@ -678,6 +678,11 @@ class TMSSsession(object): returns the copied task_draft, or raises.""" return self.post_to_path_and_get_result_as_json_object('task_draft/%s/copy' % (task_draft_id, ), retry_count=retry_count) + def create_scheduling_unit_blueprint_copy_without_given_stations(self, scheduling_unit_blueprint_id: int, stations_to_be_removed: list, retry_count: int=0) -> {}: + return self.post_to_path_and_get_result_as_json_object('scheduling_unit_blueprint/%s/create_copy_without_given_stations' % (scheduling_unit_blueprint_id, ), + json_data={'stations_to_be_removed': stations_to_be_removed}, + retry_count=retry_count) + def get_scheduling_set(self, scheduling_set_id: str) -> dict: '''get the schedulingunit_set as dict for the given scheduling_set_id.'''