diff --git a/SAS/TMSS/client/lib/populate.py b/SAS/TMSS/client/lib/populate.py index bb39f6967617e077aa4c8d00f425534cbfc4d95c..63a148b2eeaa2cb59b7f2a77ecc9b9405d67283e 100644 --- a/SAS/TMSS/client/lib/populate.py +++ b/SAS/TMSS/client/lib/populate.py @@ -72,7 +72,7 @@ def populate_schemas(schema_dir: str=None, templates_filename: str=None): else: template['schema'] = json_schema - logger.info("Uploading template name='%s' version='%s'", name, version) + logger.info("Uploading template with name='%s' version='%s' template='%s' ", name, version, template) client.post_template(template_path=template_name, name=name, diff --git a/SAS/TMSS/src/tmss/tmssapp/migrations/0001_initial.py b/SAS/TMSS/src/tmss/tmssapp/migrations/0001_initial.py index 4ae9725ed8dbaa5c4897e7d309adfdb2fe9ff126..5ee172f8b5694b25455dd932284059d9d87a3885 100644 --- a/SAS/TMSS/src/tmss/tmssapp/migrations/0001_initial.py +++ b/SAS/TMSS/src/tmss/tmssapp/migrations/0001_initial.py @@ -243,6 +243,19 @@ class Migration(migrations.Migration): 'abstract': False, }, ), + migrations.CreateModel( + name='DefaultReservationTemplate', + 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(max_length=128, unique=True)), + ], + options={ + 'abstract': False, + }, + ), migrations.CreateModel( name='DefaultSchedulingConstraintsTemplate', fields=[ @@ -405,6 +418,39 @@ class Migration(migrations.Migration): 'abstract': False, }, ), + migrations.CreateModel( + name='Reservation', + 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(help_text='Short description for this reservation, used in overviews', max_length=255)), + ('start_time', models.DateTimeField(help_text='Start of this reservation.')), + ('duration', models.IntegerField(help_text='Duration of this reservations.', null=True)), + ('specifications_doc', django.contrib.postgres.fields.jsonb.JSONField(help_text='Properties of this reservation')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='ReservationTemplate', + 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.IntegerField(editable=False, help_text='Version of this template (with respect to other templates of the same name)')), + ('schema', django.contrib.postgres.fields.jsonb.JSONField(help_text='Schema for the configurable parameters needed to use this template.')), + ], + options={ + 'abstract': False, + }, + ), migrations.CreateModel( name='ResourceType', fields=[ @@ -1147,6 +1193,20 @@ class Migration(migrations.Migration): name='quantity', field=models.ForeignKey(help_text='The quantity of this resource type.', on_delete=django.db.models.deletion.PROTECT, to='tmssapp.Quantity'), ), + migrations.AddConstraint( + model_name='reservationtemplate', + constraint=models.UniqueConstraint(fields=('name', 'version'), name='reservationtemplate_unique_name_version'), + ), + migrations.AddField( + model_name='reservation', + name='project', + field=models.ForeignKey(help_text='Reservation will be accounted for this project.', on_delete=django.db.models.deletion.CASCADE, related_name='reservations', to='tmssapp.Project'), + ), + migrations.AddField( + model_name='reservation', + name='specifications_template', + field=models.ForeignKey(help_text='Schema used for specifications_doc.', on_delete=django.db.models.deletion.CASCADE, to='tmssapp.ReservationTemplate'), + ), migrations.AddField( model_name='projectquota', name='project', @@ -1211,6 +1271,11 @@ class Migration(migrations.Migration): name='template', field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='tmssapp.SchedulingConstraintsTemplate'), ), + migrations.AddField( + model_name='defaultreservationtemplate', + name='template', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='tmssapp.ReservationTemplate'), + ), migrations.AddField( model_name='defaultgeneratortemplate', name='template', @@ -1380,6 +1445,10 @@ class Migration(migrations.Migration): model_name='defaultschedulingconstraintstemplate', index=django.contrib.postgres.indexes.GinIndex(fields=['tags'], name='tmssapp_def_tags_480bbd_gin'), ), + migrations.AddIndex( + model_name='defaultreservationtemplate', + index=django.contrib.postgres.indexes.GinIndex(fields=['tags'], name='tmssapp_def_tags_58d7a4_gin'), + ), migrations.AddIndex( model_name='defaultgeneratortemplate', index=django.contrib.postgres.indexes.GinIndex(fields=['tags'], name='tmssapp_def_tags_89c89d_gin'), @@ -1404,4 +1473,4 @@ class Migration(migrations.Migration): model_name='dataproduct', index=django.contrib.postgres.indexes.GinIndex(fields=['tags'], name='tmssapp_dat_tags_5932a3_gin'), ), - ] + ] \ No newline at end of file diff --git a/SAS/TMSS/src/tmss/tmssapp/models/specification.py b/SAS/TMSS/src/tmss/tmssapp/models/specification.py index 503bb9dbe020d3e462d936e5dc2f47052a1453d5..1f5a6e3b5be0631f4c5b99ee9e4b6a2176556623 100644 --- a/SAS/TMSS/src/tmss/tmssapp/models/specification.py +++ b/SAS/TMSS/src/tmss/tmssapp/models/specification.py @@ -375,6 +375,44 @@ class DefaultTaskRelationSelectionTemplate(BasicCommon): name = CharField(max_length=128, unique=True) template = ForeignKey("TaskRelationSelectionTemplate", on_delete=PROTECT) + +class ReservationTemplate(Template): + pass + + +class DefaultReservationTemplate(BasicCommon): + name = CharField(max_length=128, unique=True) + template = ForeignKey("ReservationTemplate", on_delete=PROTECT) + + +# +# DatabaseView objects +# +class TaskBlueprintSummary(Model): + taskblueprint_id = IntegerField() + subtask_id = IntegerField() + substate = CharField(max_length=128) + subtask_type = CharField(max_length=128) + + class Meta: + managed = False + db_table = 'tmssapp_taskblueprintsummary' + + +class SchedulingUnitBlueprintSummary(Model): + # Using in an id and ForeignKey is not common for a view BUT the id is a 'dummy' to be able to use in Django + # https://resources.rescale.com/using-database-views-in-django-orm/ + # otherwise an exception will be thrown + id = IntegerField(primary_key=True) + sub_id = IntegerField() + taskblueprint_id = IntegerField() + task_type = CharField(max_length=128) + derived_task_status = CharField(max_length=128) + + class Meta: + managed = False + db_table = 'tmssapp_schedulingunitblueprintsummary' + # # Instance Objects # @@ -1084,3 +1122,15 @@ class TaskSchedulingRelationDraft(BasicCommon): raise ValidationError("Time_offset must be >= 0") super().save(force_insert, force_update, using, update_fields) + +class Reservation(NamedCommon): + project = ForeignKey('Project', related_name='reservations', on_delete=CASCADE, help_text='Reservation will be accounted for this project.') + description = CharField(max_length=255, help_text='Short description for this reservation, used in overviews') + start_time = DateTimeField(help_text='Start of this reservation.') + duration = IntegerField(null=True, help_text='Duration of this reservations.') + specifications_doc = JSONField(help_text='Properties of this reservation') + specifications_template = ForeignKey('ReservationTemplate', on_delete=CASCADE, help_text='Schema used for specifications_doc.') + + 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) diff --git a/SAS/TMSS/src/tmss/tmssapp/populate.py b/SAS/TMSS/src/tmss/tmssapp/populate.py index 05ec07e83f2f102caa1f65d1bcadf8ffb3447935..73116db990983bddcc038740907cae326e5d75b3 100644 --- a/SAS/TMSS/src/tmss/tmssapp/populate.py +++ b/SAS/TMSS/src/tmss/tmssapp/populate.py @@ -68,6 +68,16 @@ def populate_test_data(): 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 scheduling_set in tmss_project.scheduling_sets.all(): for unit_nr in range(2): for strategy_template in [uc1_strategy_template, simple_strategy_template]: diff --git a/SAS/TMSS/src/tmss/tmssapp/schemas/reservation_template-reservation-1.json b/SAS/TMSS/src/tmss/tmssapp/schemas/reservation_template-reservation-1.json new file mode 100644 index 0000000000000000000000000000000000000000..cdcbb306ad3f90ddde1701666ac2eb10845695a7 --- /dev/null +++ b/SAS/TMSS/src/tmss/tmssapp/schemas/reservation_template-reservation-1.json @@ -0,0 +1,132 @@ +{ + "$id": "http://tmss.lofar.org/api/schemas/tasktemplate/reservation/1#", + "$schema": "http://json-schema.org/draft-06/schema#", + "title": "resource reservation", + "description": "This schema defines the parameters to reserve instrument resources, and to annotate the reservation.", + "version": 1, + "type": "object", + "properties": { + "activity": { + "title": "Activity", + "description": "Description of the activity during this reservation", + "type": "object", + "additonalProperties": false, + "default":{}, + "properties": { + "type": { + "title": "Type", + "description": "Reason for this reservation", + "type": "string", + "enum": [ "maintenance", "test", "upgrade", "outage", "pr", "stand-alone mode", "test system", "other" ], + "default": "maintenance" + }, + "description": { + "title": "Description", + "description": "Free-form explanation of the reason", + "type": "string", + "default": "" + }, + "contact": { + "title": "Contact", + "description": "Who coordinates this maintenance", + "type": "string", + "default": "" + }, + "subject": { + "title": "Subject", + "description": "What will be modified or affected (select 'system' if multiple)", + "type": "string", + "enum": [ "environment", "hardware", "firmware", "software", "system", "network", "nothing" ], + "default": "nothing" + }, + "planned": { + "title": "Planned", + "description": "Was this planned?", + "type": "boolean", + "default": true + } + }, + "required": [ + "type" + ] + }, + "resources": { + "title": "Resources", + "description": "Which resources are affected", + "type": "object", + "additonalProperties": false, + "default":{}, + "properties": { + "stations": { + "title": "Stations", + "description": "List of stations", + "$ref": "http://tmss.lofar.org/api/schemas/commonschematemplate/stations/1#/definitions/station_list" + } + }, + "required": [] + }, + "schedulability": { + "title": "Schedulability", + "description": "Schedulability of the reserved resources", + "type": "object", + "additonalProperties": false, + "default":{}, + "properties": { + "manual": { + "title": "Manual", + "description": "Manual scheduling is allowed", + "type": "boolean", + "default": true + }, + "dynamic": { + "title": "Dynamic", + "description": "Dynamic scheduling is allowed", + "type": "boolean", + "default": false + }, + "project_exclusive": { + "title": "Schedule only for this project", + "description": "Only tasks from this project can be scheduled", + "type": "boolean", + "default": true + } + } + }, + "effects": { + "title": "Effect", + "description": "Effect the actions have during this reservation", + "type": "object", + "additonalProperties": false, + "default":{}, + "properties": { + "lba_rfi": { + "title": "LBA RFI", + "description": "RFI increases in the LBA spectrum during this maintenance", + "type": "boolean", + "default": false + }, + "hba_rfi": { + "title": "HBA RFI", + "description": "RFI increases in the HBA spectrum during this maintenance", + "type": "boolean", + "default": false + }, + "expert": { + "title": "Expert mode", + "description": "Quality cannot be guaranteed", + "type": "boolean", + "default": true + } + }, + "required": [] + } + }, + "required": [ + "activity", "resources", "effects" + ] +} + + + + + diff --git a/SAS/TMSS/src/tmss/tmssapp/schemas/subtask_template-reservation-1.json b/SAS/TMSS/src/tmss/tmssapp/schemas/subtask_template-reservation-1.json new file mode 100644 index 0000000000000000000000000000000000000000..dd1d1996959e6630bc0b78f5be64a6422ec6721f --- /dev/null +++ b/SAS/TMSS/src/tmss/tmssapp/schemas/subtask_template-reservation-1.json @@ -0,0 +1,27 @@ +{ + "$id": "http://tmss.lofar.org/api/schemas/subtasktemplate/reservation/1#", + "$schema": "http://json-schema.org/draft-06/schema#", + "title": "resource reservation", + "description": "This schema defines reserved resources", + "version": 1, + "type": "object", + "properties": { + "resources": { + "title": "Resources", + "description": "Which resources are reserved", + "type": "object", + "additonalProperties": false, + "properties": { + "stations": { + "title": "Stations", + "description": "List of stations", + "$ref": "http://tmss.lofar.org/api/schemas/commonschematemplate/stations/1#/definitions/station_list" + } + }, + "required": [] + } + }, + "required": [ + "resources" + ] +} diff --git a/SAS/TMSS/src/tmss/tmssapp/schemas/templates.json b/SAS/TMSS/src/tmss/tmssapp/schemas/templates.json index 6e1d2c710101efe1a396935340fcdee899fe3ded..55f5cd2d4401c5592f0faaa3b627557e29f6cace 100644 --- a/SAS/TMSS/src/tmss/tmssapp/schemas/templates.json +++ b/SAS/TMSS/src/tmss/tmssapp/schemas/templates.json @@ -126,14 +126,18 @@ "file_name": "sap_template-1.json", "template": "sap_template" }, - { + { "file_name": "subtask_template-ingest-1.json", "template": "subtask_template", "type": "copy" - }, - { + }, + { "file_name": "task_template-ingest-1.json", "template": "task_template", "type": "ingest" + }, + { + "file_name": "reservation_template-reservation-1.json", + "template": "reservation_template" } ] \ No newline at end of file diff --git a/SAS/TMSS/src/tmss/tmssapp/serializers/specification.py b/SAS/TMSS/src/tmss/tmssapp/serializers/specification.py index 0c215aa57d1915e0660bd31572775bd3992d00d9..19afb2076dfec6e7b2644021680e1750d90db36e 100644 --- a/SAS/TMSS/src/tmss/tmssapp/serializers/specification.py +++ b/SAS/TMSS/src/tmss/tmssapp/serializers/specification.py @@ -385,3 +385,20 @@ class TaskTypeSerializer(RelationalHyperlinkedModelSerializer): model = models.TaskType fields = '__all__' + +class ReservationTemplateSerializer(AbstractTemplateSerializer): + class Meta: + model = models.ReservationTemplate + fields = '__all__' + + +class DefaultReservationTemplateSerializer(RelationalHyperlinkedModelSerializer): + class Meta: + model = models.DefaultReservationTemplate + fields = '__all__' + + +class ReservationSerializer(RelationalHyperlinkedModelSerializer): + class Meta: + model = models.Reservation + fields = '__all__' diff --git a/SAS/TMSS/src/tmss/tmssapp/viewsets/specification.py b/SAS/TMSS/src/tmss/tmssapp/viewsets/specification.py index f4f1e95ddbe38152855429597c6360be6448e4dc..64c4e9e588e228a509c12be4f66a687b132ae096 100644 --- a/SAS/TMSS/src/tmss/tmssapp/viewsets/specification.py +++ b/SAS/TMSS/src/tmss/tmssapp/viewsets/specification.py @@ -174,6 +174,21 @@ class DefaultTaskRelationSelectionTemplateViewSet(LOFARViewSet): serializer_class = serializers.DefaultTaskRelationSelectionTemplateSerializer +class DefaultReservationTemplateViewSet(LOFARViewSet): + queryset = models.DefaultReservationTemplate.objects.all() + serializer_class = serializers.DefaultReservationTemplateSerializer + + +class ReservationTemplateViewSet(AbstractTemplateViewSet): + queryset = models.ReservationTemplate.objects.all() + serializer_class = serializers.ReservationTemplateSerializer + + +class ReservationViewSet(LOFARViewSet): + queryset = models.Reservation.objects.all() + serializer_class = serializers.ReservationSerializer + + class RoleViewSet(LOFARViewSet): queryset = models.Role.objects.all() serializer_class = serializers.RoleSerializer @@ -914,4 +929,5 @@ class TaskRelationBlueprintNestedViewSet(LOFARNestedViewSet): class TaskTypeViewSet(LOFARViewSet): queryset = models.TaskType.objects.all() - serializer_class = serializers.TaskTypeSerializer \ No newline at end of file + serializer_class = serializers.TaskTypeSerializer + diff --git a/SAS/TMSS/src/tmss/urls.py b/SAS/TMSS/src/tmss/urls.py index 623d43642732d4a11463f252adffb0938259d9c9..0ba7a22d9fbf789defbb7022074cec4963ead9bc 100644 --- a/SAS/TMSS/src/tmss/urls.py +++ b/SAS/TMSS/src/tmss/urls.py @@ -127,12 +127,14 @@ router.register(r'scheduling_unit_template', viewsets.SchedulingUnitTemplateView router.register(r'scheduling_constraints_template', viewsets.SchedulingConstraintsTemplateViewSet) router.register(r'task_template', viewsets.TaskTemplateViewSet) router.register(r'task_relation_selection_template', viewsets.TaskRelationSelectionTemplateViewSet) +router.register(r'reservation_template', viewsets.ReservationTemplateViewSet) router.register(r'task_connector_type', viewsets.TaskConnectorTypeViewSet) router.register(r'default_generator_template', viewsets.DefaultGeneratorTemplateViewSet) router.register(r'default_scheduling_unit_template', viewsets.DefaultSchedulingUnitTemplateViewSet) router.register(r'default_scheduling_constraints_template', viewsets.DefaultSchedulingConstraintsTemplateViewSet) 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) # instances router.register(r'cycle', viewsets.CycleViewSet) @@ -141,6 +143,7 @@ router.register(r'project', viewsets.ProjectViewSet) router.register(r'resource_type', viewsets.ResourceTypeViewSet) router.register(r'project_quota', viewsets.ProjectQuotaViewSet) router.register(r'setting', viewsets.SettingViewSet) +router.register(r'reservation', viewsets.ReservationViewSet) router.register(r'scheduling_set', viewsets.SchedulingSetViewSet) router.register(r'scheduling_unit_draft', viewsets.SchedulingUnitDraftViewSet) diff --git a/SAS/TMSS/test/t_tmssapp_specification_REST_API.py b/SAS/TMSS/test/t_tmssapp_specification_REST_API.py index 018c985f4b69f7b626564bda91f076dcc49591b9..e69a0a55f0bf1cbd466253a051ec5db88cd42392 100755 --- a/SAS/TMSS/test/t_tmssapp_specification_REST_API.py +++ b/SAS/TMSS/test/t_tmssapp_specification_REST_API.py @@ -267,6 +267,70 @@ class SchedulingConstraintsTemplateTestCase(unittest.TestCase): DELETE_and_assert_gone(self, url) +class ReservationTemplateTestCase(unittest.TestCase): + def test_reservation_template_list_apiformat(self): + r = requests.get(BASE_URL + '/reservation_template/?format=api', auth=AUTH) + self.assertEqual(r.status_code, 200) + self.assertTrue("Reservation Template List" in r.content.decode('utf8')) + + def test_reservation_template_GET_nonexistant_raises_error(self): + GET_and_assert_equal_expected_code(self, BASE_URL + '/reservation_template/1234321/', 404) + + def test_reservation_template_POST_and_GET(self): + # POST and GET a new item and assert correctness + test_data = test_data_creator.ReservationTemplate() + expected_data = test_data_creator.update_schema_from_template("reservationtemplate", test_data) + r_dict = POST_and_assert_expected_response(self, BASE_URL + '/reservation_template/', test_data, 201, expected_data) + url = r_dict['url'] + GET_OK_and_assert_equal_expected_response(self, url+'?format=json', expected_data) + + def test_reservation_template_PUT_invalid_raises_error(self): + test_data = test_data_creator.ReservationTemplate() + PUT_and_assert_expected_response(self, BASE_URL + '/reservation_template/9876789876/', test_data, 404, {}) + + def test_reservation_template_PUT(self): + # POST new item, verify + test_data = test_data_creator.ReservationTemplate() + expected_data = test_data_creator.update_schema_from_template("reservationtemplate", test_data) + r_dict = POST_and_assert_expected_response(self, BASE_URL + '/reservation_template/', test_data, 201, expected_data) + url = r_dict['url'] + GET_OK_and_assert_equal_expected_response(self, url, expected_data) + # PUT new values, verify + test_data2 = test_data_creator.ReservationTemplate("reservationtemplate2") + expected_data2 = test_data_creator.update_schema_from_template("reservationtemplate", test_data2) + PUT_and_assert_expected_response(self, url, test_data2, 200, expected_data2) + GET_OK_and_assert_equal_expected_response(self, url, expected_data2) + + def test_reservation_template_PATCH(self): + # POST new item, verify + test_data = test_data_creator.ReservationTemplate() + expected_data = test_data_creator.update_schema_from_template("reservationtemplate", test_data) + r_dict = POST_and_assert_expected_response(self, BASE_URL + '/reservation_template/', test_data, 201, expected_data) + url = r_dict['url'] + GET_OK_and_assert_equal_expected_response(self, url, expected_data) + + test_patch = {"name": "new_name", + "description": "better description", + "schema": minimal_json_schema(properties={"mykey": {"type":"string", "default":"my better value"}})} + + # PATCH item and verify + expected_patch_data = test_data_creator.update_schema_from_template("reservationtemplate", test_patch) + PATCH_and_assert_expected_response(self, url, test_patch, 200, expected_patch_data) + expected_data = dict(test_data) + expected_data.update(expected_patch_data) + GET_OK_and_assert_equal_expected_response(self, url, expected_data) + + def test_reservation_template_DELETE(self): + # POST new item, verify + test_data = test_data_creator.ReservationTemplate() + expected_data = test_data_creator.update_schema_from_template("reservationtemplate", test_data) + r_dict = POST_and_assert_expected_response(self, BASE_URL + '/reservation_template/', test_data, 201, expected_data) + url = r_dict['url'] + GET_OK_and_assert_equal_expected_response(self, url, expected_data) + # DELETE and check it's gone + DELETE_and_assert_gone(self, url) + + class TaskTemplateTestCase(unittest.TestCase): def test_task_template_list_apiformat(self): @@ -1099,7 +1163,6 @@ class ProjectQuotaTestCase(unittest.TestCase): GET_OK_and_assert_equal_expected_response(self, project_quota_url, project_quota_test_data) - class SchedulingSetTestCase(unittest.TestCase): def test_scheduling_set_list_apiformat(self): r = requests.get(BASE_URL + '/scheduling_set/?format=api', auth=AUTH) @@ -2661,6 +2724,84 @@ class TaskSchedulingRelationDraftTestCase(unittest.TestCase): GET_OK_and_assert_equal_expected_response(self, BASE_URL + '/task_scheduling_relation_draft/%s/' % id2, test_data_2) +class ReservationTestCase(unittest.TestCase): + def test_reservation_list_apiformat(self): + r = requests.get(BASE_URL + '/reservation/?format=api', auth=AUTH) + self.assertEqual(r.status_code, 200) + self.assertTrue("Reservation List" in r.content.decode('utf8')) + + def test_reservation_GET_nonexistant_raises_error(self): + GET_and_assert_equal_expected_code(self, BASE_URL + '/reservation/1234321/', 404) + + def test_reservation_POST_and_GET(self): + reservation_test_data = test_data_creator.Reservation(duration=60) + + # POST and GET a new item and assert correctness + r_dict = POST_and_assert_expected_response(self, BASE_URL + '/reservation/', reservation_test_data, 201, reservation_test_data) + url = r_dict['url'] + GET_OK_and_assert_equal_expected_response(self, url, reservation_test_data) + + def test_reservation_PUT_invalid_raises_error(self): + reservation_test_data = test_data_creator.Reservation(duration=60) + PUT_and_assert_expected_response(self, BASE_URL + '/reservation/9876789876/', reservation_test_data, 404, {}) + + def test_reservation_PUT(self): + project_url = test_data_creator.post_data_and_get_url(test_data_creator.Project(), '/project/') + reservation_test_data = test_data_creator.Reservation(name="reservation 1", duration=50, project_url=project_url) + + # POST new item, verify + r_dict = POST_and_assert_expected_response(self, BASE_URL + '/reservation/', reservation_test_data, 201, reservation_test_data) + url = r_dict['url'] + GET_OK_and_assert_equal_expected_response(self, url, reservation_test_data) + + reservation_test_data2 = test_data_creator.Reservation(name="reservation2", project_url=project_url) + # PUT new values, verify + PUT_and_assert_expected_response(self, url, reservation_test_data2, 200, reservation_test_data2) + GET_OK_and_assert_equal_expected_response(self, url, reservation_test_data2) + + def test_reservation_PATCH(self): + reservation_test_data = test_data_creator.Reservation(duration=60) + + # POST new item, verify + r_dict = POST_and_assert_expected_response(self, BASE_URL + '/reservation/', reservation_test_data, 201, reservation_test_data) + url = r_dict['url'] + GET_OK_and_assert_equal_expected_response(self, url, reservation_test_data) + + test_patch = {"description": "This is a new and improved description", + "duration": 90} + + # PATCH item and verify + expected_patch_data = test_data_creator.update_schema_from_template("reservationtemplate", test_patch) + PATCH_and_assert_expected_response(self, url, test_patch, 200, expected_patch_data) + expected_data = dict(reservation_test_data) + expected_data.update(test_patch) + GET_OK_and_assert_equal_expected_response(self, url, expected_patch_data) + + def test_reservation_DELETE(self): + reservation_test_data = test_data_creator.Reservation(duration=30, start_time=datetime.utcnow() + timedelta(days=1)) + # POST new item, verify + r_dict = POST_and_assert_expected_response(self, BASE_URL + '/reservation/', reservation_test_data, 201, reservation_test_data) + url = r_dict['url'] + GET_OK_and_assert_equal_expected_response(self, url, reservation_test_data) + + # DELETE and check it's gone + DELETE_and_assert_gone(self, url) + + def test_GET_Reservation_list_shows_entry(self): + test_data_1 = Reservation_test_data(duration=3600) + models.Reservation.objects.create(**test_data_1) + nbr_results = models.Reservation.objects.count() + GET_and_assert_in_expected_response_result_list(self, BASE_URL + '/reservation/', test_data_1, nbr_results) + + def test_GET_Reservation_view_returns_correct_entry(self): + test_data_1 = Reservation_test_data(name="Reservation 1", duration=60, start_time=datetime.utcnow() + timedelta(days=1)) + test_data_2 = Reservation_test_data(name="Reservation 2", duration=120, start_time=datetime.utcnow() + timedelta(days=2)) + id1 = models.Reservation.objects.create(**test_data_1).id + id2 = models.Reservation.objects.create(**test_data_2).id + GET_OK_and_assert_equal_expected_response(self, BASE_URL + '/reservation/' + str(id1) + '/', test_data_1) + GET_OK_and_assert_equal_expected_response(self, BASE_URL + '/reservation/' + str(id2) + '/', test_data_2) + + if __name__ == "__main__": logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s', level=logging.INFO) diff --git a/SAS/TMSS/test/tmss_test_data_django_models.py b/SAS/TMSS/test/tmss_test_data_django_models.py index 7f5f266be4ac768d006f7860abd9d8f85351c723..8c59a5c8959d5252825dff208bb07bce1574cb65 100644 --- a/SAS/TMSS/test/tmss_test_data_django_models.py +++ b/SAS/TMSS/test/tmss_test_data_django_models.py @@ -68,7 +68,6 @@ def SchedulingConstraintsTemplate_test_data(name="my_SchedulingConstraintsTempla "tags": ["TMSS", "TESTING"]} - def SchedulingUnitObservingStrategyTemplate_test_data(name="my_SchedulingUnitObservingStrategyTemplate", scheduling_unit_template:models.SchedulingUnitTemplate=None, template:dict=None) -> dict: @@ -495,3 +494,32 @@ def SAPTemplate_test_data() -> dict: "schema": minimal_json_schema(), "tags": ["TMSS", "TESTING"]} + +def ReservationTemplate_test_data(name="my_ReservationTemplate", schema:dict=None) -> dict: + if schema is None: + schema = minimal_json_schema(properties={ "foo" : { "type": "string", "default": "bar" } }, required=["foo"]) + + return {"name": name, + "description": 'My ReservationTemplate description', + "schema": schema, + "tags": ["TMSS", "TESTING"]} + + +def Reservation_test_data(name="MyReservation", duration=None, start_time=None, project: models.Project = None) -> dict: + if project is None: + project = models.Project.objects.create(**Project_test_data()) + + if start_time is None: + start_time = datetime.utcnow() + timedelta(hours=12) + + specifications_template = models.ReservationTemplate.objects.create(**ReservationTemplate_test_data()) + specifications_doc = get_default_json_object_for_schema(specifications_template.schema) + + return {"name": name, + "project": project, + "description": "Test Reservation", + "tags": ["TMSS", "TESTING"], + "start_time": start_time, + "duration": duration, # can be None + "specifications_doc": specifications_doc, + "specifications_template": specifications_template} diff --git a/SAS/TMSS/test/tmss_test_data_rest.py b/SAS/TMSS/test/tmss_test_data_rest.py index 1a16d480f10c74cd783b3ea88d39fd363b1c2cfc..17f78eaf04f89360724f2c1896037f65c2029445 100644 --- a/SAS/TMSS/test/tmss_test_data_rest.py +++ b/SAS/TMSS/test/tmss_test_data_rest.py @@ -130,6 +130,16 @@ class TMSSRESTTestDataCreator(): "tags": ["TMSS", "TESTING"]} + def ReservationTemplate(self, name="reservationtemplate1", schema:dict=None) -> dict: + if schema is None: + schema = minimal_json_schema(properties={"foo": {"type": "string", "default": "bar"}}) + + return { "name": name, + "description": 'My description', + "schema": schema, + "tags": ["TMSS", "TESTING"]} + + def SchedulingUnitObservingStrategyTemplate(self, name="my_SchedulingUnitObservingStrategyTemplate", scheduling_unit_template_url=None, template:dict=None) -> dict: @@ -669,4 +679,30 @@ class TMSSRESTTestDataCreator(): return {"specifications_doc": specifications_doc, "specifications_template": specifications_template_url, - "tags": ['tmss', 'testing']} \ No newline at end of file + "tags": ['tmss', 'testing']} + + def Reservation(self, name="My Reservation", duration=None, start_time=None, project_url=None, + specifications_template_url=None, specifications_doc=None) -> dict: + + if project_url is None: + project_url = self.post_data_and_get_url(self.Project(), '/project/') + if start_time is None: + start_time = datetime.utcnow() + timedelta(hours=12) + + if specifications_template_url is None: + specifications_template_url = self.post_data_and_get_url(self.ReservationTemplate(), '/reservation_template/') + + if specifications_doc is None: + specifications_doc = self.get_response_as_json_object(specifications_template_url + '/default') + + if isinstance(start_time, datetime): + start_time = start_time.isoformat() + + return {"name": name, + "project": project_url, + "description": "Test Reservation", + "tags": ["TMSS", "TESTING"], + "start_time": start_time, + "duration": duration, # can be None + "specifications_doc": specifications_doc, + "specifications_template": specifications_template_url} \ No newline at end of file