From 56bcc4fd66d1bd349d4d9ec104cb2e10b847bc42 Mon Sep 17 00:00:00 2001
From: Roy de Goei <goei@astron.nl>
Date: Mon, 23 Nov 2020 11:01:56 +0100
Subject: [PATCH] TMSS-398 Add name to Reservation model, add testdata abd
 testcase for Reservation and ReservationTemplate

---
 .../tmss/tmssapp/migrations/0001_initial.py   |  13 +-
 .../src/tmss/tmssapp/models/specification.py  |   2 +-
 SAS/TMSS/src/tmss/tmssapp/populate.py         |   3 +-
 .../tmss/tmssapp/viewsets/specification.py    |   2 +-
 .../test/t_tmssapp_specification_REST_API.py  | 143 +++++++++++++++++-
 SAS/TMSS/test/tmss_test_data_django_models.py |  30 +++-
 SAS/TMSS/test/tmss_test_data_rest.py          |  38 ++++-
 7 files changed, 217 insertions(+), 14 deletions(-)

diff --git a/SAS/TMSS/src/tmss/tmssapp/migrations/0001_initial.py b/SAS/TMSS/src/tmss/tmssapp/migrations/0001_initial.py
index b8867668933..17ff8cd5c42 100644
--- a/SAS/TMSS/src/tmss/tmssapp/migrations/0001_initial.py
+++ b/SAS/TMSS/src/tmss/tmssapp/migrations/0001_initial.py
@@ -1,4 +1,4 @@
-# Generated by Django 3.0.9 on 2020-11-18 12:12
+# Generated by Django 3.0.9 on 2020-11-23 07:26
 
 from django.conf import settings
 import django.contrib.postgres.fields
@@ -452,10 +452,11 @@ class Migration(migrations.Migration):
                 ('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='Filter for selecting dataproducts from the output role.')),
+                ('specifications_doc', django.contrib.postgres.fields.jsonb.JSONField(help_text='Properties of this reservation')),
             ],
             options={
                 'abstract': False,
@@ -1238,7 +1239,7 @@ class Migration(migrations.Migration):
         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='first_to_connect', to='tmssapp.Project'),
+            field=models.ForeignKey(help_text='Reservation will be accounted for this project.', on_delete=django.db.models.deletion.CASCADE, related_name='reserved', to='tmssapp.Project'),
         ),
         migrations.AddField(
             model_name='reservation',
@@ -1463,10 +1464,6 @@ class Migration(migrations.Migration):
             model_name='sap',
             index=django.contrib.postgres.indexes.GinIndex(fields=['tags'], name='tmssapp_sap_tags_7451b0_gin'),
         ),
-        migrations.AddIndex(
-            model_name='reservation',
-            index=django.contrib.postgres.indexes.GinIndex(fields=['tags'], name='tmssapp_res_tags_0f3b14_gin'),
-        ),
         migrations.AddIndex(
             model_name='defaulttasktemplate',
             index=django.contrib.postgres.indexes.GinIndex(fields=['tags'], name='tmssapp_def_tags_c88200_gin'),
@@ -1515,4 +1512,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 aaa95fe2c8d..34d444a16fe 100644
--- a/SAS/TMSS/src/tmss/tmssapp/models/specification.py
+++ b/SAS/TMSS/src/tmss/tmssapp/models/specification.py
@@ -1105,7 +1105,7 @@ class TaskSchedulingRelationDraft(BasicCommon):
         super().save(force_insert, force_update, using, update_fields)
 
 
-class Reservation(BasicCommon):
+class Reservation(NamedCommon):
     project = ForeignKey('Project', related_name='reserved', 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.')
diff --git a/SAS/TMSS/src/tmss/tmssapp/populate.py b/SAS/TMSS/src/tmss/tmssapp/populate.py
index 45ca0000b4f..475d80f75f0 100644
--- a/SAS/TMSS/src/tmss/tmssapp/populate.py
+++ b/SAS/TMSS/src/tmss/tmssapp/populate.py
@@ -68,7 +68,8 @@ def populate_test_data():
             # 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(description="Just A non-scheduled reservation as example",
+            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,
diff --git a/SAS/TMSS/src/tmss/tmssapp/viewsets/specification.py b/SAS/TMSS/src/tmss/tmssapp/viewsets/specification.py
index b4f85ef6b25..bb80effff4b 100644
--- a/SAS/TMSS/src/tmss/tmssapp/viewsets/specification.py
+++ b/SAS/TMSS/src/tmss/tmssapp/viewsets/specification.py
@@ -179,7 +179,7 @@ class DefaultReservationTemplateViewSet(LOFARViewSet):
     serializer_class = serializers.DefaultReservationTemplateSerializer
 
 
-class ReservationTemplateViewSet(LOFARViewSet):
+class ReservationTemplateViewSet(AbstractTemplateViewSet):
     queryset = models.ReservationTemplate.objects.all()
     serializer_class = serializers.ReservationTemplateSerializer
 
diff --git a/SAS/TMSS/test/t_tmssapp_specification_REST_API.py b/SAS/TMSS/test/t_tmssapp_specification_REST_API.py
index 018c985f4b6..e69a0a55f0b 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 f58583962a1..4c8d1845e33 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:
@@ -487,3 +486,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 82f35cf01ae..4e7b921f9b6 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:
@@ -671,4 +681,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
-- 
GitLab