diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/migrations/0001_initial.py b/SAS/TMSS/backend/src/tmss/tmssapp/migrations/0001_initial.py index 2df67c9b37f610d3abe48ac11aeaa440843794f5..bc33c5696ec718a0c662eb600e2e28a9091913d0 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/migrations/0001_initial.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/migrations/0001_initial.py @@ -460,6 +460,22 @@ class Migration(migrations.Migration): 'abstract': False, }, ), + migrations.CreateModel( + name='ReservationStrategyTemplate', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('tags',django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=128), blank=True, default=list, help_text='User-defined search keywords for object.', size=8)), + ('created_at', models.DateTimeField(auto_now_add=True, help_text='Moment of object creation.')), + ('updated_at', models.DateTimeField(auto_now=True, help_text='Moment of last object update.')), + ('name', models.CharField(help_text='Human-readable name of this object.', max_length=128)), + ('description', models.CharField(blank=True, default='', help_text='A longer description of this object.', max_length=255)), + ('version', models.CharField(help_text='Version of this template (with respect to other templates of the same name).', max_length=128)), + ('template', django.contrib.postgres.fields.jsonb.JSONField(help_text='JSON-data compliant with the JSON-schema in the scheduling_unit_template. This observation strategy template like a predefined recipe with all the correct settings, and defines which parameters the user can alter.')), + ], + options={ + 'abstract': False, + }, + ), migrations.CreateModel( name='ReservationTemplate', fields=[ @@ -1200,6 +1216,11 @@ class Migration(migrations.Migration): model_name='reservationtemplate', constraint=models.UniqueConstraint(fields=('name', 'version'), name='reservationtemplate_unique_name_version'), ), + migrations.AddField( + model_name='reservationstrategytemplate', + name='reservation_template', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='tmssapp.ReservationTemplate'), + ), migrations.AddField( model_name='reservation', name='project', @@ -1218,12 +1239,12 @@ class Migration(migrations.Migration): migrations.AddField( model_name='projectquotaarchivelocation', name='project_quota', - field=models.ForeignKey(help_text='Project to wich this quota belongs.', on_delete=django.db.models.deletion.PROTECT, related_name='project_quota', to='tmssapp.ProjectQuota'), + field=models.ForeignKey(help_text='Project to which this quota belongs.', on_delete=django.db.models.deletion.PROTECT, related_name='project_quota', to='tmssapp.ProjectQuota'), ), migrations.AddField( model_name='projectquota', name='project', - field=models.ForeignKey(help_text='Project to wich this quota belongs.', on_delete=django.db.models.deletion.PROTECT, related_name='quota', to='tmssapp.Project'), + field=models.ForeignKey(help_text='Project to which this quota belongs.', on_delete=django.db.models.deletion.PROTECT, related_name='quota', to='tmssapp.Project'), ), migrations.AddField( model_name='projectquota', diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/models/specification.py b/SAS/TMSS/backend/src/tmss/tmssapp/models/specification.py index 4c00aa3da2312e0849ca93f5deb0d3a67bee7a9a..3d0ae5c1ef9e46b48c017a480354b74672cfd249 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/models/specification.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/models/specification.py @@ -235,6 +235,26 @@ class DefaultTaskRelationSelectionTemplate(BasicCommon): template = ForeignKey("TaskRelationSelectionTemplate", on_delete=PROTECT) +class ReservationStrategyTemplate(NamedCommon): + ''' + A ReservationStrategyTemplate is a template in the sense that it serves as a template to fill in json data objects + conform its referred reservation_template. + It is however not derived from the (abstract) Template super-class, because the Template super class is for + JSON schemas, not JSON data objects. + ''' + version = CharField(max_length=128, help_text='Version of this template (with respect to other templates of the same name).') + template = JSONField(null=False, help_text='JSON-data compliant with the JSON-schema in the reservation_template. ' + 'This reservation strategy template like a predefined recipe with all ' + 'the correct settings, and defines which parameters the user can alter.') + reservation_template = ForeignKey("ReservationTemplate", on_delete=PROTECT, null=False, help_text="") + + def save(self, force_insert=False, force_update=False, using=None, update_fields=None): + if self.template and self.reservation_template_id and self.reservation_template.schema: + validate_json_against_schema(self.template, self.reservation_template.schema) + + super().save(force_insert, force_update, using, update_fields) + + class ReservationTemplate(Template): pass @@ -1056,3 +1076,11 @@ class Reservation(NamedCommon): def save(self, force_insert=False, force_update=False, using=None, update_fields=None): annotate_validate_add_defaults_to_doc_using_template(self, 'specifications_doc', 'specifications_template') super().save(force_insert, force_update, using, update_fields) + + # RGOE: is this really needed we already do a validate ???? + # def save(self, force_insert=False, force_update=False, using=None, update_fields=None): + # if self.requirements_doc is not None and self.requirements_template_id and self.requirements_template.schema is not None: + # # If this scheduling unit was created from an observation_strategy_template, + # # then make sure that the observation_strategy_template validates against this unit's requirements_template.schema + # if self.observation_strategy_template_id and self.observation_strategy_template.template: + # validate_json_against_schema(self.observation_strategy_template.template, self.requirements_template.schema) \ No newline at end of file diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/populate.py b/SAS/TMSS/backend/src/tmss/tmssapp/populate.py index d9352294552550890cf404aaf40074803f83c6fb..132e47533c2345b494f7f2471034378f1f51a16e 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/populate.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/populate.py @@ -74,20 +74,26 @@ def populate_test_data(): simple_strategy_template = models.SchedulingUnitObservingStrategyTemplate.objects.get(name="Simple Observation") simple_beamforming_strategy_template = models.SchedulingUnitObservingStrategyTemplate.objects.get(name="Simple Beamforming Observation") + core_strategy_template = models.ReservationStrategyTemplate.objects.get(name="Simple Core Reservation") + projects = models.Project.objects.order_by('-priority_rank').all() for tmss_project in projects: if 'Commissioning' not in tmss_project.tags: continue - # for test purposes also add a reservation object - reservation_template = models.ReservationTemplate.objects.get(name="resource reservation") - reservation_template_spec = get_default_json_object_for_schema(reservation_template.schema) - Reservation.objects.create(name="DummyReservation", - description="Just A non-scheduled reservation as example", - project=tmss_project, - specifications_template=reservation_template, - specifications_doc=reservation_template_spec, - start_time=datetime.now()) + # for test purposes also add a reservation objects from reservation strategies + for strategy_template in [core_strategy_template]: + reservation_spec = add_defaults_to_json_object_for_schema(strategy_template.template, + strategy_template.reservation_template.schema) + reservation = Reservation.objects.create(name="Core Stations for Maintenance", + description="Core Reservation created from reservation strategy", + project=None, + specifications_template=strategy_template.reservation_template, + specifications_doc=reservation_spec, + # Do we need to add to the model? reservation_strategy_template=strategy_template, + start_time=datetime.now()+timedelta(days=1), + duration=None) # TODO change to stop_time when merge TMSS-668 + logger.info('created test reservation: %s', reservation.name) for scheduling_set in tmss_project.scheduling_sets.all(): for unit_nr in range(2): diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/schemas/simple-core-stations-reservation-strategy.json b/SAS/TMSS/backend/src/tmss/tmssapp/schemas/simple-core-stations-reservation-strategy.json new file mode 100644 index 0000000000000000000000000000000000000000..b960cdae67b74d14d8d0b0c603bc042ef4ff519d --- /dev/null +++ b/SAS/TMSS/backend/src/tmss/tmssapp/schemas/simple-core-stations-reservation-strategy.json @@ -0,0 +1,47 @@ +{ + "activity": { + "type": "maintenance", + "description": "Maintenance of all core stations", + "contact": "SDCO", + "subject": "system", + "planned": true + }, + "resources": { + "stations": [ + "CS001", + "CS002", + "CS003", + "CS004", + "CS005", + "CS006", + "CS007", + "CS011", + "CS013", + "CS017", + "CS021", + "CS024", + "CS026", + "CS028", + "CS030", + "CS031", + "CS032", + "CS101", + "CS103", + "CS201", + "CS301", + "CS302", + "CS401", + "CS501" + ] + }, + "effects": { + "lba_rfi": false, + "hba_rfi": false, + "expert": false + }, + "schedulability": { + "manual": false, + "dynamic": false, + "project_exclusive": false + } +} \ No newline at end of file diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/schemas/templates.json b/SAS/TMSS/backend/src/tmss/tmssapp/schemas/templates.json index 480d7a4abb715673befa1742ef8fedb6ac04a00f..74f0a61e0f52ebc24c40b79899ad12974e28caaa 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/schemas/templates.json +++ b/SAS/TMSS/backend/src/tmss/tmssapp/schemas/templates.json @@ -167,5 +167,14 @@ { "file_name": "reservation_template-reservation-1.json", "template": "reservation_template" + }, + { + "file_name": "simple-core-stations-reservation-strategy.json", + "template": "reservation_strategy_template", + "reservation_template_name": "reservation", + "reservation_template_version": "1", + "name": "Simple Core Reservation", + "description": "This reservation strategy template defines a reservation of all core station for system maintenance.", + "version": 1 } ] diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/serializers/specification.py b/SAS/TMSS/backend/src/tmss/tmssapp/serializers/specification.py index 650d1f6816667256d074ac1994ab3c37a8727d37..188494e0936defd8c69f473f887661876a8ad5d1 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/serializers/specification.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/serializers/specification.py @@ -362,6 +362,14 @@ class TaskTypeSerializer(DynamicRelationalHyperlinkedModelSerializer): fields = '__all__' +class ReservationStrategyTemplateSerializer(DynamicRelationalHyperlinkedModelSerializer): + template = JSONEditorField(schema_source="reservation_template.schema") + + class Meta: + model = models.ReservationStrategyTemplate + fields = '__all__' + + class ReservationTemplateSerializer(AbstractTemplateSerializer): class Meta: model = models.ReservationTemplate diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/specification.py b/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/specification.py index 9605ead221a9ae4a18596d0c6d887b4ad2791bc2..e34f22304e474413487c2e177e77525da2798d4d 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/specification.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/specification.py @@ -37,6 +37,7 @@ from rest_framework.filters import OrderingFilter import json import logging +import dateutil from django.core.exceptions import ObjectDoesNotExist @@ -199,6 +200,69 @@ class DefaultTaskRelationSelectionTemplateViewSet(LOFARViewSet): serializer_class = serializers.DefaultTaskRelationSelectionTemplateSerializer +class ReservationStrategyTemplateViewSet(LOFARViewSet): + queryset = models.ReservationStrategyTemplate.objects.all() + serializer_class = serializers.ReservationStrategyTemplateSerializer + + @swagger_auto_schema(responses={status.HTTP_201_CREATED: 'The newly created reservation', + status.HTTP_403_FORBIDDEN: 'forbidden'}, + operation_description="Create a new Reservation based on this ReservationStrategyTemplate, " + "with the given <name>, <description>, <start_time> and <stop_time>", + manual_parameters=[Parameter(name='start_time', required=True, type='string', in_='query', + description="The start time as a timestamp string in isoformat"), + Parameter(name='stop_time', required=True, type='string', in_='query', + description="The stop time as a timestamp string in isoformat"), + Parameter(name='name', required=False, type='string', in_='query', + description="The name for the newly created reservation"), + Parameter(name='description', required=False, type='string', in_='query', + description="The description for the newly created reservation"), + Parameter(name='project_id', required=False, type='integer', in_='query', + description="the id of the project which will be the parent of the newly created reservation"), + ]) + @action(methods=['get'], detail=True) + def create_reservation(self, request, pk=None): + strategy_template = get_object_or_404(models.ReservationStrategyTemplate, pk=pk) + reservation_template_spec = add_defaults_to_json_object_for_schema(strategy_template.template, + strategy_template.reservation_template.schema) + + start_time = request.query_params.get('start_time', None) + stop_time = request.query_params.get('stop_time', None) + if start_time: + start_time = dateutil.parser.parse(start_time) # string to datetime + else: + start_time = datetime.now() + if stop_time: + stop_time = dateutil.parser.parse(stop_time) # string to datetime + duration = (stop_time - start_time).total_seconds() + else: + duration = None + # TODO: when merging with TMSS-668 it wil become stop_time and calculation of duration can be removed!!! + + project_id = request.query_params.get('project_id', None) + if project_id: + project = get_object_or_404(models.Project, pk=request.query_params['project_id']) + else: + project = None + + reservation = Reservation.objects.create(name=request.query_params.get('name', "reservation"), + description=request.query_params.get('description', ""), + project=project, + specifications_template=strategy_template.reservation_template, + specifications_doc=reservation_template_spec, + # Do we need to add to the model? reservation_strategy_template=strategy_template, + start_time=start_time, + duration=duration) # TODO change to stop_time when merge TMSS-668 + + reservation_strategy_template_path = request._request.path + base_path = reservation_strategy_template_path[:reservation_strategy_template_path.find('/reservation_strategy_template')] + reservation_path = '%s/reservation/%s/' % (base_path, reservation.id,) + + # return a response with the new serialized Reservation, and a Location to the new instance in the header + return Response(serializers.ReservationSerializer(reservation, context={'request':request}).data, + status=status.HTTP_201_CREATED, + headers={'Location': reservation_path}) + + class DefaultReservationTemplateViewSet(LOFARViewSet): queryset = models.DefaultReservationTemplate.objects.all() serializer_class = serializers.DefaultReservationTemplateSerializer diff --git a/SAS/TMSS/backend/src/tmss/urls.py b/SAS/TMSS/backend/src/tmss/urls.py index 039b531a658e3bed589f131860f3d1193bfc3b39..66e58162725f917a20c5020e6492ad6f39bed7d0 100644 --- a/SAS/TMSS/backend/src/tmss/urls.py +++ b/SAS/TMSS/backend/src/tmss/urls.py @@ -142,6 +142,7 @@ router.register(r'default_scheduling_constraints_template', viewsets.DefaultSche router.register(r'default_task_template', viewsets.DefaultTaskTemplateViewSet) router.register(r'default_task_relation_selection_template', viewsets.DefaultTaskRelationSelectionTemplateViewSet) router.register(r'default_reservation_template', viewsets.DefaultReservationTemplateViewSet) +router.register(r'reservation_strategy_template', viewsets.ReservationStrategyTemplateViewSet) # instances router.register(r'cycle', viewsets.CycleViewSet) diff --git a/SAS/TMSS/backend/test/tmss_test_data_django_models.py b/SAS/TMSS/backend/test/tmss_test_data_django_models.py index 6b1089baf00d6989aff0ad87ad132c21000f4b1c..8535c5e223890e3be48467853ecb486f073985fe 100644 --- a/SAS/TMSS/backend/test/tmss_test_data_django_models.py +++ b/SAS/TMSS/backend/test/tmss_test_data_django_models.py @@ -575,6 +575,22 @@ def Reservation_test_data(name="MyReservation", duration=None, start_time=None, "specifications_template": specifications_template} +def ReservationStrategyTemplate_test_data(name="my_ReservationStrategyTemplate", + reservation_template:models.ReservationTemplate=None, + template:dict=None) -> dict: + if reservation_template is None: + reservation_template = models.ReservationTemplate.objects.create(**ReservationTemplate_test_data()) + + if template is None: + template = get_default_json_object_for_schema(reservation_template.schema) + + return {"name": name, + "description": 'My Reservation Template description', + "template": template, + "reservation_template": reservation_template, + "tags": ["TMSS", "TESTING"]} + + def ProjectPermission_test_data(name=None, GET=None, PUT=None, POST=None, PATCH=None, DELETE=None) -> dict: if name is None: name = 'MyProjectPermission_%s' % uuid.uuid4() diff --git a/SAS/TMSS/client/lib/populate.py b/SAS/TMSS/client/lib/populate.py index 6d3420403a6490f9b74c7117f4fb845bce66e9e5..9ee870bed2f3888dd9371ad270bbb32fcfdbf6eb 100644 --- a/SAS/TMSS/client/lib/populate.py +++ b/SAS/TMSS/client/lib/populate.py @@ -38,6 +38,7 @@ def populate_schemas(schema_dir: str=None, templates_filename: str=None): # keep track of the templates, json schemas and references templates_dict = {} observing_strategy_templates = [] + reservation_strategy_templates = [] schema_references = {} all_references = set() @@ -71,7 +72,7 @@ def populate_schemas(schema_dir: str=None, templates_filename: str=None): # get the id without trailing # and/or / json_schema_id = json_schema.get('$id', "").rstrip("#").rstrip("/") - if template_name == 'scheduling_unit_observing_strategy_template': + if 'strategy_template' in template_name: template['template'] = json_schema else: template['schema'] = json_schema @@ -84,6 +85,8 @@ def populate_schemas(schema_dir: str=None, templates_filename: str=None): # store the prepared template for upload if template_name == 'scheduling_unit_observing_strategy_template': observing_strategy_templates.append(template) + elif template_name == 'reservation_strategy_template': + reservation_strategy_templates.append(template) else: templates_dict[json_schema_id] = template @@ -111,6 +114,15 @@ def populate_schemas(schema_dir: str=None, templates_filename: str=None): logger.info("Uploading observation strategy with name='%s' version='%s'", template['name'], template['version']) client.post_template(template_path='scheduling_unit_observing_strategy_template', **template) + # helper functions for uploading reservation_strategy_templates + def upload_reservation_strategy_templates(template: dict): + scheduling_unit_templates = client.get_path_as_json_object('reservation_template?name=' + template.get('reservation_template_name') + '&version=' + template.get('reservation_template_version')) + scheduling_unit_template = scheduling_unit_templates[0] + template['reservation_template'] = scheduling_unit_template['url'] + logger.info("Uploading reservation strategy with name='%s' version='%s'", template['name'], + template['version']) + client.post_template(template_path='reservation_strategy_template', **template) + # first, upload all dependent templates for ref in all_references: upload_template_if_needed_with_dependents_first(ref) @@ -120,6 +132,10 @@ def populate_schemas(schema_dir: str=None, templates_filename: str=None): with ThreadPoolExecutor() as executor: executor.map(upload_template, rest_templates) + # the reservation_strategy_templates + with ThreadPoolExecutor() as executor: + executor.map(upload_reservation_strategy_templates, reservation_strategy_templates) + # and finally, the observing_strategy_templates with ThreadPoolExecutor() as executor: executor.map(upload_observing_strategy_templates, observing_strategy_templates)