From a7095c56f0401a72832096120c6cd5938f8832b4 Mon Sep 17 00:00:00 2001
From: Jorrit Schaap <schaap@astron.nl>
Date: Tue, 4 Aug 2020 14:49:05 +0200
Subject: [PATCH] TMSS-287: added Quantity AbstractChoice model, and used it in
 ResourceType to annotate each resource type with the correct quantity.

---
 SAS/TMSS/src/remakemigrations.py              |  2 +-
 .../tmss/tmssapp/migrations/0001_initial.py   | 20 +++++++++++--
 .../tmss/tmssapp/migrations/0002_populate.py  |  2 +-
 .../src/tmss/tmssapp/models/specification.py  | 21 ++++++++++++--
 SAS/TMSS/src/tmss/tmssapp/populate.py         | 28 +++++++++----------
 .../tmss/tmssapp/serializers/specification.py | 10 +++++--
 .../tmss/tmssapp/viewsets/specification.py    |  5 ++++
 SAS/TMSS/src/tmss/urls.py                     |  1 +
 8 files changed, 65 insertions(+), 24 deletions(-)

diff --git a/SAS/TMSS/src/remakemigrations.py b/SAS/TMSS/src/remakemigrations.py
index 503432045e7..397b52ac237 100755
--- a/SAS/TMSS/src/remakemigrations.py
+++ b/SAS/TMSS/src/remakemigrations.py
@@ -79,8 +79,8 @@ class Migration(migrations.Migration):
                    migrations.RunPython(populate_settings),
                    migrations.RunPython(populate_misc),
                    migrations.RunPython(populate_lofar_json_schemas),
-                   migrations.RunPython(populate_cycles),
                    migrations.RunPython(populate_resources),
+                   migrations.RunPython(populate_cycles),
                    migrations.RunPython(populate_projects),
                    migrations.RunPython(populate_test_scheduling_set) ]
 """
diff --git a/SAS/TMSS/src/tmss/tmssapp/migrations/0001_initial.py b/SAS/TMSS/src/tmss/tmssapp/migrations/0001_initial.py
index 25c030c9a7a..b04ecd0b93b 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 2.2.12 on 2020-08-03 10:00
+# Generated by Django 2.2.12 on 2020-08-04 12:35
 
 from django.conf import settings
 import django.contrib.postgres.fields
@@ -354,6 +354,15 @@ class Migration(migrations.Migration):
                 ('value', models.FloatField(help_text='Resource Quota value')),
             ],
         ),
+        migrations.CreateModel(
+            name='Quantity',
+            fields=[
+                ('value', models.CharField(max_length=128, primary_key=True, serialize=False, unique=True)),
+            ],
+            options={
+                'abstract': False,
+            },
+        ),
         migrations.CreateModel(
             name='ResourceType',
             fields=[
@@ -991,10 +1000,15 @@ class Migration(migrations.Migration):
             name='project',
             field=models.ForeignKey(help_text='Project to which this scheduling set belongs.', on_delete=django.db.models.deletion.PROTECT, related_name='scheduling_sets', to='tmssapp.Project'),
         ),
+        migrations.AddField(
+            model_name='resourcetype',
+            name='quantity',
+            field=models.ForeignKey(help_text='The quantity of this resource type.', on_delete=django.db.models.deletion.PROTECT, to='tmssapp.Quantity'),
+        ),
         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='project_quota', to='tmssapp.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'),
         ),
         migrations.AddField(
             model_name='projectquota',
@@ -1111,7 +1125,7 @@ class Migration(migrations.Migration):
         migrations.AddField(
             model_name='cyclequota',
             name='cycle',
-            field=models.ForeignKey(help_text='Cycle to which these quota apply.', on_delete=django.db.models.deletion.PROTECT, related_name='cycle_quota', to='tmssapp.Cycle'),
+            field=models.ForeignKey(help_text='Cycle to which these quota apply.', on_delete=django.db.models.deletion.PROTECT, related_name='quota', to='tmssapp.Cycle'),
         ),
         migrations.AddField(
             model_name='cyclequota',
diff --git a/SAS/TMSS/src/tmss/tmssapp/migrations/0002_populate.py b/SAS/TMSS/src/tmss/tmssapp/migrations/0002_populate.py
index 58b591cf062..b24c0bccf0d 100644
--- a/SAS/TMSS/src/tmss/tmssapp/migrations/0002_populate.py
+++ b/SAS/TMSS/src/tmss/tmssapp/migrations/0002_populate.py
@@ -20,7 +20,7 @@ class Migration(migrations.Migration):
                    migrations.RunPython(populate_settings),
                    migrations.RunPython(populate_misc),
                    migrations.RunPython(populate_lofar_json_schemas),
-                   migrations.RunPython(populate_resources),
                    migrations.RunPython(populate_cycles),
+                   migrations.RunPython(populate_resources),
                    migrations.RunPython(populate_projects),
                    migrations.RunPython(populate_test_scheduling_set) ]
diff --git a/SAS/TMSS/src/tmss/tmssapp/models/specification.py b/SAS/TMSS/src/tmss/tmssapp/models/specification.py
index f8cf8e2127b..60b95bb0dff 100644
--- a/SAS/TMSS/src/tmss/tmssapp/models/specification.py
+++ b/SAS/TMSS/src/tmss/tmssapp/models/specification.py
@@ -150,6 +150,21 @@ class Flag(AbstractChoice):
         AUTOSCHEDULE = "allow_scheduling_observations"
 
 
+
+class Quantity(AbstractChoice):
+    """Defines the model and predefined list of possible period categories to be used in a.o. ProjectQuota and CycleQuota.
+       Please note that, by agreement, all values of a certain quantity are stored in SI units.
+       For example: a duration has the quantity "time" which is stored in "seconds".
+       We only store the quatity-type in the database, because we known the correct SI units for each quantity, so there is no need to store the unit as well.
+        The items in the Choices class below are automagically populated into the database via a data migration."""
+
+    class Choices(Enum):
+        # these are the basic quantities that we currently use. More can be added if needed.
+        TIME = "time"
+        BYTES = "bytes"
+        NUMBER = "number"
+
+
 class PeriodCategory(AbstractChoice):
     """Defines the model and predefined list of possible period categories to be used in Project.
         The items in the Choices class below are automagically populated into the database via a data migration."""
@@ -277,7 +292,7 @@ class Cycle(NamedCommonPK):
 
 
 class CycleQuota(Model):
-    cycle = ForeignKey('Cycle', related_name="cycle_quota", on_delete=PROTECT, help_text='Cycle to which these quota apply.')
+    cycle = ForeignKey('Cycle', related_name="quota", on_delete=PROTECT, help_text='Cycle to which these quota apply.')
     value = FloatField(help_text='Resource Quota value')
     resource_type = ForeignKey('ResourceType', on_delete=PROTECT, help_text='Resource type.')
 
@@ -319,13 +334,13 @@ class Project(NamedCommonPK):
 
 
 class ProjectQuota(Model):
-    project = ForeignKey('Project', related_name="project_quota", on_delete=PROTECT, help_text='Project to wich this quota belongs.')  # protected to avoid accidents
+    project = ForeignKey('Project', related_name="quota", on_delete=PROTECT, help_text='Project to wich this quota belongs.')  # protected to avoid accidents
     value = FloatField(help_text='Resource Quota value')
     resource_type = ForeignKey('ResourceType', on_delete=PROTECT, help_text='Resource type.')  # protected to avoid accidents
 
 
 class ResourceType(NamedCommonPK):
-    pass
+    quantity = ForeignKey('Quantity', null=False, on_delete=PROTECT, help_text='The quantity of this resource type.')
 
 
 class SchedulingSet(NamedCommon):
diff --git a/SAS/TMSS/src/tmss/tmssapp/populate.py b/SAS/TMSS/src/tmss/tmssapp/populate.py
index f847cfecc9d..65d405c019b 100644
--- a/SAS/TMSS/src/tmss/tmssapp/populate.py
+++ b/SAS/TMSS/src/tmss/tmssapp/populate.py
@@ -37,7 +37,7 @@ def populate_choices(apps, schema_editor):
     '''
     for choice_class in [Role, Datatype, Dataformat, CopyReason,
                          SubtaskState, SubtaskType, StationType, Algorithm, ScheduleMethod, SchedulingRelationPlacement,
-                         Flag, ProjectCategory, PeriodCategory]:
+                         Flag, ProjectCategory, PeriodCategory, Quantity]:
         choice_class.objects.bulk_create([choice_class(value=x.value) for x in choice_class.Choices])
 
 def populate_settings(apps, schema_editor):
@@ -207,7 +207,7 @@ def populate_cycles(apps, schema_editor):
                                             start=datetime(2013+nr//2, 6 if nr%2==0 else 11, 1, 0, 0, 0, 0, tzinfo=timezone.utc),
                                             stop=datetime(2013+(nr+1)//2, 6 if nr%2==1 else 11, 1, 0, 0, 0, 0, tzinfo=timezone.utc))
         models.CycleQuota.objects.create(cycle=cycle,
-                                         resource_type=ResourceType.objects.get(name="lofar_observing_time"),
+                                         resource_type=ResourceType.objects.get(name="observing_time"),
                                          value=0.8*cycle.duration.total_seconds()) # rough guess. 80% of total time available for observing
         models.CycleQuota.objects.create(cycle=cycle,
                                          resource_type=ResourceType.objects.get(name="cep_processing_time"),
@@ -219,13 +219,13 @@ def populate_cycles(apps, schema_editor):
                                          resource_type=ResourceType.objects.get(name="support_time"),
                                          value=0)  # needs to be filled in by user (SOS)
         models.CycleQuota.objects.create(cycle=cycle,
-                                         resource_type=ResourceType.objects.get(name="lofar_observing_time_commissioning"),
+                                         resource_type=ResourceType.objects.get(name="observing_time_commissioning"),
                                          value=0.05*cycle.duration.total_seconds()) # rough guess. 5% of total time available for observing
         models.CycleQuota.objects.create(cycle=cycle,
-                                         resource_type=ResourceType.objects.get(name="lofar_observing_time_prio_a"),
+                                         resource_type=ResourceType.objects.get(name="observing_time_prio_a"),
                                          value=0) # needs to be filled in by user (SOS)
         models.CycleQuota.objects.create(cycle=cycle,
-                                         resource_type=ResourceType.objects.get(name="lofar_observing_time_prio_b"),
+                                         resource_type=ResourceType.objects.get(name="observing_time_prio_b"),
                                          value=0) # needs to be filled in by user (SOS)
 
 
@@ -241,15 +241,15 @@ def populate_projects(apps, schema_editor):
 
 
 def populate_resources(apps, schema_editor):
-    ResourceType.objects.create(name="lta_storage", description="Amount of storage in the LTA (in bytes)")
-    ResourceType.objects.create(name="cep_storage", description="Amount of storage on the CEP processing cluster (in bytes)")
-    ResourceType.objects.create(name="cep_processing_time", description="Processing time on the CEP processing cluster (in seconds)")
-    ResourceType.objects.create(name="lofar_observing_time", description="Observing time (in seconds)")
-    ResourceType.objects.create(name="lofar_observing_time_prio_a", description="Observing time with priority A (in seconds)")
-    ResourceType.objects.create(name="lofar_observing_time_prio_b", description="Observing time with priority B (in seconds)")
-    ResourceType.objects.create(name="lofar_observing_time_commissioning", description="Observing time for Commissioning/DDT (in seconds)")
-    ResourceType.objects.create(name="support_time", description="Support time by human (in seconds)")
-    ResourceType.objects.create(name="number_of_triggers", description="Number of trigger events (as integer)")
+    ResourceType.objects.create(name="lta_storage", description="Amount of storage in the LTA (in bytes)", quantity=Quantity.objects.get(value=Quantity.Choices.BYTES.value))
+    ResourceType.objects.create(name="cep_storage", description="Amount of storage on the CEP processing cluster (in bytes)", quantity=Quantity.objects.get(value=Quantity.Choices.BYTES.value))
+    ResourceType.objects.create(name="cep_processing_time", description="Processing time on the CEP processing cluster (in seconds)", quantity=Quantity.objects.get(value=Quantity.Choices.TIME.value))
+    ResourceType.objects.create(name="observing_time", description="Observing time (in seconds)", quantity=Quantity.objects.get(value=Quantity.Choices.TIME.value))
+    ResourceType.objects.create(name="observing_time_prio_a", description="Observing time with priority A (in seconds)", quantity=Quantity.objects.get(value=Quantity.Choices.TIME.value))
+    ResourceType.objects.create(name="observing_time_prio_b", description="Observing time with priority B (in seconds)", quantity=Quantity.objects.get(value=Quantity.Choices.TIME.value))
+    ResourceType.objects.create(name="observing_time_commissioning", description="Observing time for Commissioning/DDT (in seconds)", quantity=Quantity.objects.get(value=Quantity.Choices.TIME.value))
+    ResourceType.objects.create(name="support_time", description="Support time by human (in seconds)", quantity=Quantity.objects.get(value=Quantity.Choices.TIME.value))
+    ResourceType.objects.create(name="number_of_triggers", description="Number of trigger events (as integer)", quantity=Quantity.objects.get(value=Quantity.Choices.NUMBER.value))
 
 
 def populate_misc(apps, schema_editor):
diff --git a/SAS/TMSS/src/tmss/tmssapp/serializers/specification.py b/SAS/TMSS/src/tmss/tmssapp/serializers/specification.py
index 4ca0e649e23..f034bc0acc0 100644
--- a/SAS/TMSS/src/tmss/tmssapp/serializers/specification.py
+++ b/SAS/TMSS/src/tmss/tmssapp/serializers/specification.py
@@ -159,6 +159,12 @@ class DataformatSerializer(RelationalHyperlinkedModelSerializer):
         fields = '__all__'
 
 
+class QuantitySerializer(RelationalHyperlinkedModelSerializer):
+    class Meta:
+        model = models.Quantity
+        fields = '__all__'
+
+
 class CopyReasonSerializer(RelationalHyperlinkedModelSerializer):
     class Meta:
         model = models.CopyReason
@@ -177,7 +183,7 @@ class CycleSerializer(RelationalHyperlinkedModelSerializer):
     class Meta:
         model = models.Cycle
         fields = '__all__'
-        extra_fields = ['projects', 'name', 'duration', 'cycle_quota']
+        extra_fields = ['projects', 'name', 'duration', 'quota']
 
 class CycleQuotaSerializer(RelationalHyperlinkedModelSerializer):
     class Meta:
@@ -191,7 +197,7 @@ class ProjectSerializer(RelationalHyperlinkedModelSerializer):
     class Meta:
         model = models.Project
         fields = '__all__'
-        extra_fields = ['name','project_quota'] #, 'scheduling_sets']
+        extra_fields = ['name','quota'] #, 'scheduling_sets']
 
 
 class ProjectQuotaSerializer(RelationalHyperlinkedModelSerializer):
diff --git a/SAS/TMSS/src/tmss/tmssapp/viewsets/specification.py b/SAS/TMSS/src/tmss/tmssapp/viewsets/specification.py
index 8d36d92bf3f..aa83ab8741c 100644
--- a/SAS/TMSS/src/tmss/tmssapp/viewsets/specification.py
+++ b/SAS/TMSS/src/tmss/tmssapp/viewsets/specification.py
@@ -238,6 +238,11 @@ class SettingViewSet(LOFARViewSet):
     serializer_class = serializers.SettingSerializer
 
 
+class QuantityViewSet(LOFARViewSet):
+    queryset = models.Quantity.objects.all()
+    serializer_class = serializers.QuantitySerializer
+
+
 class PeriodCategoryViewSet(LOFARViewSet):
     queryset = models.PeriodCategory.objects.all()
     serializer_class = serializers.PeriodCategorySerializer
diff --git a/SAS/TMSS/src/tmss/urls.py b/SAS/TMSS/src/tmss/urls.py
index 1e6109a40d5..6f9ea2167eb 100644
--- a/SAS/TMSS/src/tmss/urls.py
+++ b/SAS/TMSS/src/tmss/urls.py
@@ -89,6 +89,7 @@ router.register(r'copy_reason', viewsets.CopyReasonViewSet)
 router.register(r'flag', viewsets.FlagViewSet)
 router.register(r'period_category', viewsets.PeriodCategoryViewSet)
 router.register(r'project_category', viewsets.ProjectCategoryViewSet)
+router.register(r'quantity', viewsets.QuantityViewSet)
 
 # templates
 router.register(r'generator_template', viewsets.GeneratorTemplateViewSet)
-- 
GitLab