diff --git a/SAS/TMSS/src/tmss/tmssapp/migrations/0001_initial.py b/SAS/TMSS/src/tmss/tmssapp/migrations/0001_initial.py index 8a7c77a6b19b7e4cca4f0c808e783a2cb0e83729..25c030c9a7a6c51a724f314f45d526699f13e5fe 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 b24c0bccf0ded89c957fa2c6c53c62dea7428c71..58b591cf062f6347698af904e25dd99bbd9e21ed 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 7973a84c2a48439ea6953442b6bd011998e94196..f8cf8e2127b1f856ebc405a667b6500b2e8a529a 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 c8ea9780f7d44872d2ab7d3859bfa6e8568cb4f6..d741eff8d8275b544bb39aae6e03708a837d50c5 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 493f8ea456bffdfee3a7a25c29e7ffead7788c2a..4ca0e649e2336da9ef66ce25f3c01cdab2c7d872 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 4a49111a9e868340ba894edf17a69223b65e2a12..8d36d92bf3fe7bffc2714bb1f27b9816594affc7 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 6163c3fc7e2089fcf9032882f32c85b91eb3033a..1e6109a40d54431db5ca0df830761382b3dbe443 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 708dcd2f4724181214093099fbc1cf96a3f883b5..2155893ec67d3da46a163cb57f800883408bfe02 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 d192f6486088fdf8d2860a7d90731150715b3b05..b089abdc0844b47afc2860ed4d995ad2017c8232 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 e9db5ec6fd350385f65e60cef32c5f3e91c18670..83c88f982ba17508930e78875044109d2233353b 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 a75637c6c484a5a4e589c3d5a2e81f25755e0dfa..5e488592d2bb8a8066f3ed7cdd5a7131e652dfd5 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()),