From 0419a847bdc8abb070e9530f82141b36a153e25b Mon Sep 17 00:00:00 2001 From: Jorrit Schaap <schaap@astron.nl> Date: Mon, 3 Aug 2020 14:38:54 +0200 Subject: [PATCH] TMSS-287: implemented CycleQuota --- .../tmss/tmssapp/migrations/0001_initial.py | 25 ++++++++++--- .../tmss/tmssapp/migrations/0002_populate.py | 2 +- .../src/tmss/tmssapp/models/specification.py | 17 ++++++--- SAS/TMSS/src/tmss/tmssapp/populate.py | 37 ++++++++++++++----- .../tmss/tmssapp/serializers/specification.py | 9 ++++- .../tmss/tmssapp/viewsets/specification.py | 17 ++++++++- SAS/TMSS/src/tmss/urls.py | 1 + SAS/TMSS/test/t_tmss_test_database.py | 4 +- .../test/t_tmssapp_specification_REST_API.py | 3 +- SAS/TMSS/test/tmss_test_data_django_models.py | 6 +-- SAS/TMSS/test/tmss_test_data_rest.py | 7 +--- 11 files changed, 92 insertions(+), 36 deletions(-) diff --git a/SAS/TMSS/src/tmss/tmssapp/migrations/0001_initial.py b/SAS/TMSS/src/tmss/tmssapp/migrations/0001_initial.py index 8a7c77a6b19..25c030c9a7a 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.6 on 2020-07-20 14:07 +# Generated by Django 2.2.12 on 2020-08-03 10:00 from django.conf import settings import django.contrib.postgres.fields @@ -76,15 +76,18 @@ class Migration(migrations.Migration): ('name', models.CharField(help_text='Human-readable name of this object.', max_length=128, primary_key=True, serialize=False)), ('start', models.DateTimeField(help_text='Moment at which the cycle starts, that is, when its projects can run.')), ('stop', models.DateTimeField(help_text='Moment at which the cycle officially ends.')), - ('number', models.IntegerField(help_text='Cycle number.')), - ('standard_hours', models.IntegerField(help_text='Number of offered hours for standard observations.')), - ('expert_hours', models.IntegerField(help_text='Number of offered hours for expert observations.')), - ('filler_hours', models.IntegerField(help_text='Number of offered hours for filler observations.')), ], options={ 'abstract': False, }, ), + migrations.CreateModel( + name='CycleQuota', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('value', models.FloatField(help_text='Resource Quota value')), + ], + ), migrations.CreateModel( name='Dataformat', fields=[ @@ -996,7 +999,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='projectquota', name='resource_type', - field=models.ForeignKey(help_text='Resource type.', on_delete=django.db.models.deletion.PROTECT, related_name='resource_type', to='tmssapp.ResourceType'), + field=models.ForeignKey(help_text='Resource type.', on_delete=django.db.models.deletion.PROTECT, to='tmssapp.ResourceType'), ), migrations.AddField( model_name='project', @@ -1105,6 +1108,16 @@ class Migration(migrations.Migration): name='specifications_template', field=models.ForeignKey(help_text='Schema used for specifications_doc.', on_delete=django.db.models.deletion.CASCADE, to='tmssapp.DataproductSpecificationsTemplate'), ), + 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'), + ), + migrations.AddField( + model_name='cyclequota', + name='resource_type', + field=models.ForeignKey(help_text='Resource type.', on_delete=django.db.models.deletion.PROTECT, to='tmssapp.ResourceType'), + ), migrations.AddField( model_name='antennaset', name='station_type', diff --git a/SAS/TMSS/src/tmss/tmssapp/migrations/0002_populate.py b/SAS/TMSS/src/tmss/tmssapp/migrations/0002_populate.py index b24c0bccf0d..58b591cf062 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_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/models/specification.py b/SAS/TMSS/src/tmss/tmssapp/models/specification.py index 7973a84c2a4..f8cf8e2127b 100644 --- a/SAS/TMSS/src/tmss/tmssapp/models/specification.py +++ b/SAS/TMSS/src/tmss/tmssapp/models/specification.py @@ -258,10 +258,11 @@ class DefaultTaskRelationSelectionTemplate(BasicCommon): class Cycle(NamedCommonPK): start = DateTimeField(help_text='Moment at which the cycle starts, that is, when its projects can run.') stop = DateTimeField(help_text='Moment at which the cycle officially ends.') - number = IntegerField(help_text='Cycle number.') - standard_hours = IntegerField(help_text='Number of offered hours for standard observations.') - expert_hours = IntegerField(help_text='Number of offered hours for expert observations.') - filler_hours = IntegerField(help_text='Number of offered hours for filler observations.') + + @property + def duration(self) -> datetime.timedelta: + '''the duration of the cycle (stop-start date)''' + return self.stop - self.start # Project to Cycle references are ManyToMany now, so cannot be protected via on_delete on db level. # Instead, explicitly check whether there are projects linked to the Cycle and prevent delete @@ -275,6 +276,12 @@ class Cycle(NamedCommonPK): super().delete(*args, **kwargs) +class CycleQuota(Model): + cycle = ForeignKey('Cycle', related_name="cycle_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.') + + class Project(NamedCommonPK): cycles = ManyToManyField('Cycle', blank=True, related_name='projects', help_text='Cycles to which this project belongs (NULLable).') priority_rank = FloatField(null=False, help_text='Priority of this project w.r.t. other projects. Projects can interrupt observations of lower-priority projects.') # todo: add if needed: validators=[MinValueValidator(0.0), MaxValueValidator(1.0)] @@ -314,7 +321,7 @@ 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 value = FloatField(help_text='Resource Quota value') - resource_type = ForeignKey('ResourceType', related_name="resource_type", on_delete=PROTECT, help_text='Resource type.') # protected to avoid accidents + resource_type = ForeignKey('ResourceType', on_delete=PROTECT, help_text='Resource type.') # protected to avoid accidents class ResourceType(NamedCommonPK): diff --git a/SAS/TMSS/src/tmss/tmssapp/populate.py b/SAS/TMSS/src/tmss/tmssapp/populate.py index c8ea9780f7d..d741eff8d82 100644 --- a/SAS/TMSS/src/tmss/tmssapp/populate.py +++ b/SAS/TMSS/src/tmss/tmssapp/populate.py @@ -186,14 +186,32 @@ def populate_test_scheduling_set(apps, schema_editor): def populate_cycles(apps, schema_editor): for nr in range(0, 18): - models.Cycle.objects.create(name="Cycle %s" % nr, - description="Lofar Cycle %s" % nr, - 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), - number=nr, - standard_hours=0, # TODO: fill in cycle hours - expert_hours=0, - filler_hours=0) + cycle = models.Cycle.objects.create(name="Cycle %02d" % nr, + description="Lofar Cycle %s" % nr, + 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"), + 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"), + value=0.8*cycle.duration.total_seconds()) + models.CycleQuota.objects.create(cycle=cycle, + resource_type=ResourceType.objects.get(name="lta_storage"), + value=0) # needs to be filled in by user (SOS) + models.CycleQuota.objects.create(cycle=cycle, + 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"), + 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"), + 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"), + value=0) # needs to be filled in by user (SOS) + def populate_projects(apps, schema_editor): tmss_project = models.Project.objects.create(name="TMSS-Commissioning", @@ -203,7 +221,7 @@ def populate_projects(apps, schema_editor): private_data=True, expert=True, filler=False) - tmss_project.cycles.set([models.Cycle.objects.get(number=14)]) + tmss_project.cycles.set([models.Cycle.objects.get(name="Cycle 14")]) def populate_resources(apps, schema_editor): @@ -213,6 +231,7 @@ def populate_resources(apps, schema_editor): 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)") diff --git a/SAS/TMSS/src/tmss/tmssapp/serializers/specification.py b/SAS/TMSS/src/tmss/tmssapp/serializers/specification.py index 493f8ea456b..4ca0e649e23 100644 --- a/SAS/TMSS/src/tmss/tmssapp/serializers/specification.py +++ b/SAS/TMSS/src/tmss/tmssapp/serializers/specification.py @@ -172,11 +172,18 @@ class TaskConnectorTypeSerializer(RelationalHyperlinkedModelSerializer): class CycleSerializer(RelationalHyperlinkedModelSerializer): + duration = FloatDurationField(read_only=True, help_text="Duration of the cycle [seconds]") + class Meta: model = models.Cycle fields = '__all__' - extra_fields = ['projects', 'name'] + extra_fields = ['projects', 'name', 'duration', 'cycle_quota'] +class CycleQuotaSerializer(RelationalHyperlinkedModelSerializer): + class Meta: + model = models.CycleQuota + fields = '__all__' + extra_fields = ['resource_type'] class ProjectSerializer(RelationalHyperlinkedModelSerializer): # scheduling_sets = serializers.PrimaryKeyRelatedField(source='scheduling_sets', read_only=True, many=True) diff --git a/SAS/TMSS/src/tmss/tmssapp/viewsets/specification.py b/SAS/TMSS/src/tmss/tmssapp/viewsets/specification.py index 4a49111a9e8..8d36d92bf3f 100644 --- a/SAS/TMSS/src/tmss/tmssapp/viewsets/specification.py +++ b/SAS/TMSS/src/tmss/tmssapp/viewsets/specification.py @@ -157,7 +157,22 @@ class TaskConnectorTypeViewSet(LOFARViewSet): class CycleViewSet(LOFARViewSet): queryset = models.Cycle.objects.all() serializer_class = serializers.CycleSerializer - ordering = ['number'] + ordering = ['start'] + + +class CycleQuotaViewSet(LOFARViewSet): + queryset = models.CycleQuota.objects.all() + serializer_class = serializers.CycleQuotaSerializer + + def get_queryset(self): + queryset = models.CycleQuota.objects.all() + + # query by project + project = self.request.query_params.get('project', None) + if project is not None: + return queryset.filter(project=project) + + return queryset class ProjectViewSet(LOFARViewSet): diff --git a/SAS/TMSS/src/tmss/urls.py b/SAS/TMSS/src/tmss/urls.py index 6163c3fc7e2..1e6109a40d5 100644 --- a/SAS/TMSS/src/tmss/urls.py +++ b/SAS/TMSS/src/tmss/urls.py @@ -103,6 +103,7 @@ router.register(r'default_task_relation_selection_template', viewsets.DefaultTas # instances router.register(r'cycle', viewsets.CycleViewSet) +router.register(r'cycle_quota', viewsets.CycleQuotaViewSet) router.register(r'project', viewsets.ProjectViewSet) router.register(r'resource_type', viewsets.ResourceTypeViewSet) router.register(r'project_quota', viewsets.ProjectQuotaViewSet) diff --git a/SAS/TMSS/test/t_tmss_test_database.py b/SAS/TMSS/test/t_tmss_test_database.py index 708dcd2f472..2155893ec67 100755 --- a/SAS/TMSS/test/t_tmss_test_database.py +++ b/SAS/TMSS/test/t_tmss_test_database.py @@ -51,8 +51,8 @@ class TMSSPostgresTestMixinTestCase(TMSSPostgresTestMixin, unittest.TestCase): now = datetime.utcnow() - db.executeQuery('''INSERT INTO tmssapp_cycle VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s);''', - qargs=([], now, now, "my_description", "my_name", now, now, 0, 1, 2, 3)) + db.executeQuery('''INSERT INTO tmssapp_cycle VALUES (%s, %s, %s, %s, %s, %s, %s);''', + qargs=([], now, now, "my_description", "my_name", now, now)) self.assertEqual(cycle_count+1, db.executeQuery("SELECT COUNT(*) FROM tmssapp_cycle;", fetch=FETCH_ONE)['count']) diff --git a/SAS/TMSS/test/t_tmssapp_specification_REST_API.py b/SAS/TMSS/test/t_tmssapp_specification_REST_API.py index d192f648608..b089abdc084 100755 --- a/SAS/TMSS/test/t_tmssapp_specification_REST_API.py +++ b/SAS/TMSS/test/t_tmssapp_specification_REST_API.py @@ -676,9 +676,10 @@ class CycleTestCase(unittest.TestCase): def test_GET_cycle_list_shows_entry(self): test_data_1 = Cycle_test_data() # uuid makes name unique - test_data_1["number"] = 32000 # cycles are ordered by number, so make this the largest numberm and hence the latest cycle + test_data_1["start"] = datetime(2999, 1, 1) # cycles are ordered by start, so make this the latest date and hence the latest cycle models.Cycle.objects.create(**test_data_1) nbr_results = models.Cycle.objects.count() + test_data_1["start"] = test_data_1["start"].isoformat() GET_and_assert_in_expected_response_result_list(self, BASE_URL + '/cycle/', test_data_1, nbr_results) def test_GET_cycle_view_returns_correct_entry(self): diff --git a/SAS/TMSS/test/tmss_test_data_django_models.py b/SAS/TMSS/test/tmss_test_data_django_models.py index e9db5ec6fd3..83c88f982ba 100644 --- a/SAS/TMSS/test/tmss_test_data_django_models.py +++ b/SAS/TMSS/test/tmss_test_data_django_models.py @@ -93,11 +93,7 @@ def Cycle_test_data() -> dict: "description": "", "tags": [], "start": datetime.utcnow().isoformat(), - "stop": datetime.utcnow().isoformat(), - "number": 1, - "standard_hours": 2, - "expert_hours": 3, - "filler_hours": 4} + "stop": datetime.utcnow().isoformat()} def Project_test_data() -> dict: return { #"cycles": [models.Cycle.objects.create(**Cycle_test_data())], # ManyToMany, use set() diff --git a/SAS/TMSS/test/tmss_test_data_rest.py b/SAS/TMSS/test/tmss_test_data_rest.py index a75637c6c48..5e488592d2b 100644 --- a/SAS/TMSS/test/tmss_test_data_rest.py +++ b/SAS/TMSS/test/tmss_test_data_rest.py @@ -127,11 +127,8 @@ class TMSSRESTTestDataCreator(): "tags": [], "start": datetime.utcnow().isoformat(), "stop": datetime.utcnow().isoformat(), - "number": 1, - "standard_hours": 2, - "expert_hours": 3, - "filler_hours": 4, - "projects": []} + "projects": [], + "cycle_quota": []} def Project(self, description="my project description"): return {"name": 'my_project_' + str(uuid.uuid4()), -- GitLab