...
 
Commits (7)
......@@ -315,7 +315,8 @@ class Migration(migrations.Migration):
('updated_at', models.DateTimeField(auto_now=True, help_text='Moment of last object update.')),
('description', models.CharField(help_text='A longer description of this object.', max_length=255)),
('name', models.CharField(help_text='Human-readable name of this object.', max_length=128, primary_key=True, serialize=False)),
('priority', models.IntegerField(default=0, help_text='Priority of this project w.r.t. other projects. Projects can interrupt observations of lower-priority projects.')),
('priority_rank', models.FloatField(help_text='Priority of this project w.r.t. other projects. Projects can interrupt observations of lower-priority projects.')),
('trigger_priority', models.IntegerField(default=1000, help_text='Priority of this project w.r.t. triggers.')),
('can_trigger', models.BooleanField(default=False, help_text='True if this project is allowed to supply observation requests on the fly, possibly interrupting currently running observations (responsive telescope).')),
('private_data', models.BooleanField(default=True, help_text='True if data of this project is sensitive. Sensitive data is not made public.')),
('expert', models.BooleanField(default=False, help_text='Expert projects put more responsibility on the PI.')),
......@@ -999,8 +1000,8 @@ class Migration(migrations.Migration):
),
migrations.AddField(
model_name='project',
name='cycle',
field=models.ForeignKey(help_text='Cycle(s) to which this project belongs (NULLable).', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='projects', to='tmssapp.Cycle'),
name='cycles',
field=models.ManyToManyField(help_text='Cycles to which this project belongs (NULLable).', null=True, related_name='projects', to='tmssapp.Cycle'),
),
migrations.AddConstraint(
model_name='generatortemplate',
......
......@@ -7,6 +7,7 @@ from django.contrib.postgres.fields import ArrayField, JSONField
from django.contrib.postgres.indexes import GinIndex
from enum import Enum
from django.db.models.expressions import RawSQL
from django.db.models.deletion import ProtectedError
from lofar.sas.tmss.tmss.tmssapp.validation import validate_json_against_schema
from django.core.exceptions import ValidationError
......@@ -239,11 +240,23 @@ class Cycle(NamedCommonPK):
expert_hours = IntegerField(help_text='Number of offered hours for expert observations.')
filler_hours = IntegerField(help_text='Number of offered hours for filler observations.')
# 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
# todo: verify correct behavior
# - Implemented: Cycles cannot be deleted if they have projects assigned to them
# - Also possible: Projects need at least one cycle
def delete(self, *args, **kwargs):
if len(self.projects.all()) > 0:
raise ProtectedError(protected_objects=self.projects.all(), msg='This Cycle is referenced by %s project(s) and cannot be deleted.' % len(self.projects.all()))
else:
super().delete(*args, **kwargs)
class Project(NamedCommonPK):
# cycle is protected since we have to manually decide to clean up projects with a cycle or keep them without cycle
cycle = ForeignKey('Cycle', related_name='projects', on_delete=PROTECT, null=True, help_text='Cycle(s) to which this project belongs (NULLable).')
priority = IntegerField(default=0, help_text='Priority of this project w.r.t. other projects. Projects can interrupt observations of lower-priority projects.') # todo: define a value for the default priority
# todo: cycles should be protected since we have to manually decide to clean up projects with a cycle or keep them without cycle, however, ManyToManyField does not allow for that
cycles = ManyToManyField('Cycle', related_name='projects', null=True, 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)]
trigger_priority = IntegerField(default=1000, help_text='Priority of this project w.r.t. triggers.') # todo: verify meaning and add to help_text: "Triggers with higher priority than this threshold can interrupt observations of projects."
can_trigger = BooleanField(default=False, help_text='True if this project is allowed to supply observation requests on the fly, possibly interrupting currently running observations (responsive telescope).')
private_data = BooleanField(default=True, help_text='True if data of this project is sensitive. Sensitive data is not made public.')
expert = BooleanField(default=False, help_text='Expert projects put more responsibility on the PI.')
......
......@@ -102,14 +102,14 @@ def _populate_task_draft_example():
expert_hours=0,
filler_hours=0)
tmss_project = models.Project.objects.create(cycle=models.Cycle.objects.get(number=14),
name="TMSS-Commissioning",
tmss_project = models.Project.objects.create(name="TMSS-Commissioning",
description="Project for all TMSS tests and commissioning",
priority=1,
priority_rank=1.0,
can_trigger=False,
private_data=True,
expert=True,
filler=False)
tmss_project.cycles.set([models.Cycle.objects.get(number=14)])
scheduling_set = models.SchedulingSet.objects.create(name="UC1 test set",
description="UC1 test set",
......
......@@ -698,10 +698,10 @@ class CycleTestCase(unittest.TestCase):
cycle = models.Cycle.objects.create(**cycle_test_data_1)
project1 = models.Project.objects.create(**project_test_data_1)
project1.cycle = cycle
project1.cycles.set([cycle])
project1.save()
project2 = models.Project.objects.create(**project_test_data_2)
project2.cycle = cycle
project2.cycles.set([cycle])
project2.save()
response_data = GET_OK_and_assert_equal_expected_response(self, BASE_URL + '/cycle/' + cycle.name, cycle_test_data_1)
assertUrlList(self, response_data['projects'], [project1, project2])
......@@ -749,7 +749,7 @@ class ProjectTestCase(unittest.TestCase):
url = r_dict['url']
GET_OK_and_assert_equal_expected_response(self, url, project_test_data)
test_patch = {"priority": 500,
test_patch = {"priority_rank": 1.0,
"tags": ["SUPERIMPORTANT"]}
# PATCH item and verify
......@@ -775,7 +775,7 @@ class ProjectTestCase(unittest.TestCase):
cycle_test_data = test_data_creator.Cycle()
cycle_url = POST_and_assert_expected_response(self, BASE_URL + '/cycle/', cycle_test_data, 201, cycle_test_data)['url']
test_data = dict(test_data_creator.Project())
test_data['cycle'] = cycle_url
test_data['cycles'] = [cycle_url]
url = POST_and_assert_expected_response(self, BASE_URL + '/project/', test_data, 201, test_data)['url']
# verify
......@@ -810,11 +810,12 @@ class ProjectTestCase(unittest.TestCase):
def test_nested_projects_are_filtered_according_to_cycle(self):
cycle_1 = models.Cycle.objects.create(**Cycle_test_data())
test_data_1 = dict(Project_test_data()) # uuid makes project unique
test_data_1['cycle'] = cycle_1
project_1 = models.Project.objects.create(**test_data_1)
cycle_1 = models.Cycle.objects.create(**Cycle_test_data())
project_1.cycles.set([cycle_1])
# assert the returned list contains related items, A list of length 1 is retrieved
GET_and_assert_in_expected_response_result_list(self, BASE_URL + '/cycle/%s/project/' % cycle_1.name, test_data_1, 1)
......@@ -823,7 +824,7 @@ class ResourceTypeTestCase(unittest.TestCase):
r = requests.get(BASE_URL + '/resource_type/?format=api', auth=AUTH)
self.assertEqual(r.status_code, 200)
self.assertTrue("Resource Type List" in r.content.decode('utf8'))
def test_resource_type_GET_nonexistant_raises_error(self):
GET_and_assert_equal_expected_code(self, BASE_URL + '/resource_type/1234321/', 404)
......@@ -839,7 +840,7 @@ class ProjectQuotaTestCase(unittest.TestCase):
r = requests.get(BASE_URL + '/project_quota/?format=api', auth=AUTH)
self.assertEqual(r.status_code, 200)
self.assertTrue("Project Quota List" in r.content.decode('utf8'))
def test_project_quota_GET_nonexistant_raises_error(self):
GET_and_assert_equal_expected_code(self, BASE_URL + '/project_quota/1234321/', 404)
......@@ -907,7 +908,7 @@ class ProjectQuotaTestCase(unittest.TestCase):
# POST new item with dependencies
project_test_data = test_data_creator.Project()
project_url = POST_and_assert_expected_response(self, BASE_URL + '/project/', project_test_data, 201, project_test_data)['url']
project_quota_test_data = dict(test_data_creator.ProjectQuota(project_url=project_url))
project_quota_url = POST_and_assert_expected_response(self, BASE_URL + '/project_quota/', project_quota_test_data, 201, project_quota_test_data)['url']
......@@ -1186,7 +1187,7 @@ class SchedulingUnitDraftTestCase(unittest.TestCase):
# assert the returned list contains related items, A list of length 1 is retrieved
GET_and_assert_in_expected_response_result_list(self, BASE_URL + '/scheduling_set/%s/scheduling_unit_draft/'
% scheduling_set_1.id, test_data_1, 1)
def test_SchedulingUnitDraft_contains_list_of_related_SchedulingUnitBlueprint(self):
......@@ -1590,7 +1591,7 @@ class TaskRelationDraftTestCase(unittest.TestCase):
test_data_1 = dict(test_data_1)
test_data_1['producer'] = task_draft_1
task_relation_draft_1 = models.TaskRelationDraft.objects.create(**test_data_1)
# assert the returned list contains related items, A list of length 1 is retrieved
GET_and_assert_in_expected_response_result_list(self, BASE_URL + '/task_draft/%s/task_relation_draft/' % task_draft_1.id, test_data_1, 1)
# assert an existing related producer is returned
......@@ -1725,7 +1726,7 @@ class SchedulingUnitBlueprintTestCase(unittest.TestCase):
# assert the returned list contains related items, A list of length 1 is retrieved
GET_and_assert_in_expected_response_result_list(self, BASE_URL + '/scheduling_unit_draft/%s/scheduling_unit_blueprint/' % scheduling_unit_draft_1.id, test_data_1, 1)
class TaskBlueprintTestCase(unittest.TestCase):
@classmethod
......@@ -2192,7 +2193,7 @@ class TaskRelationBlueprintTestCase(unittest.TestCase):
test_data_1 = dict(test_data_1)
test_data_1['draft'] = task_relation_draft_1
task_relation_blueprint_1 = models.TaskRelationBlueprint.objects.create(**test_data_1)
# assert the returned list contains related items, A list of length 1 is retrieved
GET_and_assert_in_expected_response_result_list(self, BASE_URL + '/task_relation_draft/%s/task_relation_blueprint/' % task_relation_draft_1.id, test_data_1, 1)
......
......@@ -100,11 +100,12 @@ def Cycle_test_data() -> dict:
"filler_hours": 4}
def Project_test_data() -> dict:
return { "cycle": models.Cycle.objects.create(**Cycle_test_data()),
return { #"cycles": [models.Cycle.objects.create(**Cycle_test_data())], # ManyToMany, use set()
"name": 'my_project_' + str(uuid.uuid4()),
"description": 'my description ' + str(uuid.uuid4()),
"tags": [],
"priority": 1,
"priority_rank": 1.0,
"trigger_priority": 1000,
"can_trigger": False,
"private_data": True,
"expert": True,
......
......@@ -137,7 +137,8 @@ class TMSSRESTTestDataCreator():
"description": description,
"tags": [],
"project_quota": [],
"priority": 1,
"priority_rank": 1.0,
"trigger_priority": 1000,
"can_trigger": False,
"private_data": True}
......