From e1be25b5ee4057be72bf128315064da85958ddc8 Mon Sep 17 00:00:00 2001 From: UlysseFG Date: Wed, 2 Sep 2026 14:22:37 -0400 Subject: [PATCH 1/3] First draft to move principal_investigator, requestor_name and requestor_email from project to parent_project. --- backend/fms_core/migrations/0082_v5_9_0.py | 19 ++++++++++++++ backend/fms_core/models/parent_project.py | 5 +++- backend/fms_core/models/project.py | 14 +++-------- backend/fms_core/services/dataset.py | 10 +++++--- backend/fms_core/services/project.py | 25 +++++++++++-------- backend/fms_core/viewsets/_constants.py | 15 ++++++----- backend/fms_core/viewsets/project.py | 18 +++++++------ .../src/components/filters/descriptions.js | 4 +-- 8 files changed, 65 insertions(+), 45 deletions(-) diff --git a/backend/fms_core/migrations/0082_v5_9_0.py b/backend/fms_core/migrations/0082_v5_9_0.py index 10b29b9415..f008e81217 100644 --- a/backend/fms_core/migrations/0082_v5_9_0.py +++ b/backend/fms_core/migrations/0082_v5_9_0.py @@ -4,6 +4,7 @@ from django.contrib.auth import get_user_model from django.db import migrations, models import django.db.models.deletion +import django.core.validators ADMIN_USERNAME = 'biobankadmin' @@ -39,6 +40,9 @@ def populate_parent_project(apps, schema_editor): parent_project_name = project_obj.external_name or project_obj.name # use project external name in priority and defaults to the project name parent_project_obj = ParentProject.objects.create(external_id=project_obj.external_id, name=parent_project_name, + principal_investigator=project_obj.principal_investigator, + requestor_name=project_obj.requestor_name, + requestor_email=project_obj.requestor_email, created_by_id=admin_user.id, updated_by_id=admin_user.id) reversion.add_to_revision(parent_project_obj) @@ -76,6 +80,9 @@ class Migration(migrations.Migration): ('deleted', models.BooleanField(default=False, help_text='Whether this instance has been deleted.')), ('external_id', models.CharField(help_text='Identifier to connect to an external system.', max_length=200, unique=True)), ('name', models.CharField(help_text='Parent project name used by external client.', max_length=200)), + ('principal_investigator', models.CharField(blank=True, help_text='The principal investigator of the project.', max_length=200, validators=[django.core.validators.EmailValidator()])), + ('requestor_name', models.CharField(blank=True, help_text='The name of the requestor of the project.', max_length=200)), + ('requestor_email', models.CharField(blank=True, help_text='The email of the requestor of the project.', max_length=200)), ('created_by', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.PROTECT, related_name='%(app_label)s_%(class)s_creation', to=settings.AUTH_USER_MODEL)), ('updated_by', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.PROTECT, related_name='%(app_label)s_%(class)s_modification', to=settings.AUTH_USER_MODEL)), ], @@ -103,6 +110,18 @@ class Migration(migrations.Migration): model_name='project', name='external_name', ), + migrations.RemoveField( + model_name='project', + name='principal_investigator', + ), + migrations.RemoveField( + model_name='project', + name='requestor_email', + ), + migrations.RemoveField( + model_name='project', + name='requestor_name', + ), migrations.AlterField( model_name='container', name='kind', diff --git a/backend/fms_core/models/parent_project.py b/backend/fms_core/models/parent_project.py index 9e3f9b5904..f76d42d47c 100644 --- a/backend/fms_core/models/parent_project.py +++ b/backend/fms_core/models/parent_project.py @@ -4,7 +4,7 @@ from django.db import models from .tracked_model import TrackedModel - +from ._validators import email_validator from ._utils import add_error as _add_error __all__ = ["ParentProject"] @@ -13,6 +13,9 @@ class ParentProject(TrackedModel): external_id = models.CharField(max_length=200, unique=True, help_text="Identifier to connect to an external system.") name = models.CharField(max_length=200, help_text="Parent project name used by external client.") + principal_investigator = models.CharField(blank=True, max_length=200, help_text="The principal investigator of the project.") + requestor_name = models.CharField(blank=True, max_length=200, help_text="The name of the requestor of the project.") + requestor_email = models.CharField(blank=True, max_length=200, validators=[email_validator], help_text="The email of the requestor of the project.") class Meta: indexes = [ diff --git a/backend/fms_core/models/project.py b/backend/fms_core/models/project.py index bc9eb948c6..f30f258fac 100644 --- a/backend/fms_core/models/project.py +++ b/backend/fms_core/models/project.py @@ -8,24 +8,16 @@ from ._constants import STANDARD_NAME_FIELD_LENGTH, PROJECT_STATUS_CHOICES from ._utils import add_error as _add_error -from ._validators import name_validator, email_validator +from ._validators import name_validator __all__ = ["Project"] @reversion.register() class Project(TrackedModel): - name = models.CharField(unique=True, max_length=STANDARD_NAME_FIELD_LENGTH, validators=[name_validator], - help_text="The name of the project.") - principal_investigator = models.CharField(blank=True, max_length=200, help_text="The principal investigator of the project.") - requestor_name = models.CharField(blank=True, max_length=200, help_text="The name of the requestor of the project.") - requestor_email = models.CharField(blank=True, max_length=200, validators=[email_validator], - help_text="The email of the requestor of the project.") + name = models.CharField(unique=True, max_length=STANDARD_NAME_FIELD_LENGTH, validators=[name_validator], help_text="The name of the project.") targeted_end_date = models.DateField(blank=True, null=True, help_text="Targeted date to conclude the project.") - status = models.CharField(choices=((type, type) for type in PROJECT_STATUS_CHOICES), max_length=20, default="Open", - help_text="The status of the project.") - + status = models.CharField(choices=((type, type) for type in PROJECT_STATUS_CHOICES), max_length=20, default="Open", help_text="The status of the project.") parent_project = models.ForeignKey(ParentProject, blank=True, null=True, on_delete=models.PROTECT, related_name="projects", help_text="Parent project from external system.") - comment = models.TextField(blank=True, help_text="Other relevant information about the project.") class Meta: diff --git a/backend/fms_core/services/dataset.py b/backend/fms_core/services/dataset.py index c43da2ab49..6040bad31b 100644 --- a/backend/fms_core/services/dataset.py +++ b/backend/fms_core/services/dataset.py @@ -402,7 +402,8 @@ def create_validation_info_file(dataset_obj: Dataset, validator_obj: User, is_va return None, errors, warnings dataset_id = str(dataset_obj.id) - project_requestor_email = dataset_obj.project.requestor_email + project_principal_investigator = dataset_obj.project.parent_project and dataset_obj.project.parent_project.principal_investigator + project_requestor_email = dataset_obj.project.parent_project and dataset_obj.project.parent_project.requestor_email lane = str(dataset_obj.lane) filename, timestamp = make_timestamped_filename(file_prefix[is_validation_revocation] + "_" + external_project_id + "_" + dataset_id + "_" + lane + ".json") @@ -411,7 +412,7 @@ def create_validation_info_file(dataset_obj: Dataset, validator_obj: User, is_va "timestamp": timestamp, "external_project_id": external_project_id, "project_name": dataset_obj.project.name, - "project_principal_investigator": dataset_obj.project.principal_investigator, + "project_principal_investigator": project_principal_investigator, "project_requestor_email": project_requestor_email, "run_id": dataset_obj.experiment_run.id, "run_name": dataset_obj.experiment_run.name, @@ -471,7 +472,8 @@ def create_release_info_file(dataset_obj: Dataset, readsets_obj: List[Readset], return None, errors, warnings dataset_id = str(dataset_obj.id) - project_requestor_email = dataset_obj.project.requestor_email + project_principal_investigator = dataset_obj.project.parent_project and dataset_obj.project.parent_project.principal_investigator + project_requestor_email = dataset_obj.project.parent_project and dataset_obj.project.parent_project.requestor_email lane = str(dataset_obj.lane) filename, timestamp = make_timestamped_filename(file_prefix[is_release_revocation] + "_" + external_project_id + "_" + dataset_id + "_" + lane + ".json") @@ -480,7 +482,7 @@ def create_release_info_file(dataset_obj: Dataset, readsets_obj: List[Readset], "timestamp": timestamp, "external_project_id": external_project_id, "project_name": dataset_obj.project.name, - "project_principal_investigator": dataset_obj.project.principal_investigator, + "project_principal_investigator": project_principal_investigator, "project_requestor_email": project_requestor_email, "run_id": dataset_obj.experiment_run.id, "run_name": dataset_obj.experiment_run.name, diff --git a/backend/fms_core/services/project.py b/backend/fms_core/services/project.py index e6b7d39388..58df10c5b8 100644 --- a/backend/fms_core/services/project.py +++ b/backend/fms_core/services/project.py @@ -49,17 +49,19 @@ def create_full_project(name=None, principal_investigator=None, requestor_name=N warnings = [] if external_id is not None and external_name is not None: + parent_project_data = dict( + **(dict(principal_investigator=principal_investigator) if principal_investigator is not None else dict()), + **(dict(requestor_name=requestor_name) if requestor_name is not None else dict()), + **(dict(requestor_email=requestor_email) if requestor_email is not None else dict()), + ) try: - parent_project = ParentProject.objects.create(external_id=external_id, name=external_name) + parent_project = ParentProject.objects.create(external_id=external_id, name=external_name, **parent_project_data) except ValidationError as e: errors.append(str(e)) project_data = dict( name=name, # Optional attributes - **(dict(principal_investigator=principal_investigator) if principal_investigator is not None else dict()), - **(dict(requestor_name=requestor_name) if requestor_name is not None else dict()), - **(dict(requestor_email=requestor_email) if requestor_email is not None else dict()), **(dict(parent_project=parent_project) if parent_project is not None else dict()), **(dict(status=status) if status is not None else dict()), **(dict(targeted_end_date=targeted_end_date) if targeted_end_date is not None else dict()), @@ -73,8 +75,7 @@ def create_full_project(name=None, principal_investigator=None, requestor_name=N return (project, errors, warnings) -def create_project(name=None, principal_investigator=None, requestor_name=None, - requestor_email=None, parent_project=None, status=None, targeted_end_date=None, comment=None): +def create_project(name=None, parent_project=None, status=None, targeted_end_date=None, comment=None): project = None errors = [] warnings = [] @@ -82,9 +83,6 @@ def create_project(name=None, principal_investigator=None, requestor_name=None, project_data = dict( name=name, # Optional attributes - **(dict(principal_investigator=principal_investigator) if principal_investigator is not None else dict()), - **(dict(requestor_name=requestor_name) if requestor_name is not None else dict()), - **(dict(requestor_email=requestor_email) if requestor_email is not None else dict()), **(dict(parent_project=parent_project) if parent_project is not None else dict()), **(dict(status=status) if status is not None else dict()), **(dict(targeted_end_date=targeted_end_date) if targeted_end_date is not None else dict()), @@ -98,13 +96,18 @@ def create_project(name=None, principal_investigator=None, requestor_name=None, return (project, errors, warnings) -def create_parent_project(external_id, name): +def create_parent_project(external_id, name, principal_investigator=None, requestor_name=None, requestor_email=None): parent_project = None errors = [] warnings = [] + parent_project_data = dict( + **(dict(principal_investigator=principal_investigator) if principal_investigator is not None else dict()), + **(dict(requestor_name=requestor_name) if requestor_name is not None else dict()), + **(dict(requestor_email=requestor_email) if requestor_email is not None else dict()), + ) try: - parent_project = ParentProject.objects.create(external_id=external_id, name=name) + parent_project = ParentProject.objects.create(external_id=external_id, name=name, **parent_project_data) except ValidationError as e: errors.append(str(e)) diff --git a/backend/fms_core/viewsets/_constants.py b/backend/fms_core/viewsets/_constants.py index 5e21c4813c..a46b1f804e 100644 --- a/backend/fms_core/viewsets/_constants.py +++ b/backend/fms_core/viewsets/_constants.py @@ -199,23 +199,22 @@ "name": CATEGORICAL_FILTERS_LOOSE, } -_project_filterset_fields: FiltersetFields = { +_parent_project_filterset_fields: FiltersetFields = { "id": PK_FILTERS, + "external_id": CATEGORICAL_FILTERS_LOOSE, "name": CATEGORICAL_FILTERS_LOOSE, "principal_investigator": CATEGORICAL_FILTERS_LOOSE, "requestor_name": CATEGORICAL_FILTERS_LOOSE, "requestor_email": CATEGORICAL_FILTERS_LOOSE, - "status": CATEGORICAL_FILTERS, - "parent_project__external_id": CATEGORICAL_FILTERS_LOOSE, - "parent_project__name": CATEGORICAL_FILTERS_LOOSE, - "targeted_end_date": DATE_FILTERS, - **_prefix_keys("project_derived_by_samples__sample__", _sample_minimal_filterset_fields), } -_parent_project_filterset_fields: FiltersetFields = { +_project_filterset_fields: FiltersetFields = { "id": PK_FILTERS, - "external_id": CATEGORICAL_FILTERS_LOOSE, "name": CATEGORICAL_FILTERS_LOOSE, + "status": CATEGORICAL_FILTERS, + "targeted_end_date": DATE_FILTERS, + **_prefix_keys("parent_project__", _parent_project_filterset_fields), + **_prefix_keys("project_derived_by_samples__sample__", _sample_minimal_filterset_fields), } _index_filterset_fields: FiltersetFields = { diff --git a/backend/fms_core/viewsets/project.py b/backend/fms_core/viewsets/project.py index 8b8fdcfb3b..9f2aee9262 100644 --- a/backend/fms_core/viewsets/project.py +++ b/backend/fms_core/viewsets/project.py @@ -48,12 +48,13 @@ def create(self, request, *args, **kwargs): if full_project_data.get("external_id") is not None: parent_project_obj = ParentProject.objects.filter(external_id=full_project_data["external_id"]).first() if parent_project_obj is None and full_project_data.get("external_name") is not None: - parent_project_obj = ParentProject.objects.create(external_id=full_project_data["external_id"], name=full_project_data["external_name"]) + parent_project_obj = ParentProject.objects.create(external_id=full_project_data["external_id"], + name=full_project_data["external_name"], + principal_investigator=full_project_data['principal_investigator'], + requestor_name=full_project_data['requestor_name'], + requestor_email=full_project_data['requestor_email']) project_obj = Project.objects.create(name=full_project_data['name'], - principal_investigator=full_project_data['principal_investigator'], - requestor_name=full_project_data['requestor_name'], - requestor_email=full_project_data['requestor_email'], targeted_end_date=full_project_data['targeted_end_date'], status=full_project_data['status'], comment=full_project_data['comment'], @@ -86,13 +87,14 @@ def update(self, request, *args, **kwargs): parent_project_obj = ParentProject.objects.filter(external_id=full_project_data["external_id"]).first() # Case project is associated to a new Parent Project if parent_project_obj is None and full_project_data.get("external_name") is not None: - parent_project_obj = ParentProject.objects.create(external_id=full_project_data["external_id"], name=full_project_data["external_name"]) + parent_project_obj = ParentProject.objects.create(external_id=full_project_data["external_id"], + name=full_project_data["external_name"], + principal_investigator=full_project_data['principal_investigator'], + requestor_name=full_project_data['requestor_name'], + requestor_email=full_project_data['requestor_email']) project_data = dict( name=full_project_data['name'], - **(dict(principal_investigator=full_project_data['principal_investigator']) if full_project_data['principal_investigator'] is not None else dict()), - **(dict(requestor_name=full_project_data['requestor_name']) if full_project_data['requestor_name'] is not None else dict()), - **(dict(requestor_email=full_project_data['requestor_email']) if full_project_data['requestor_email'] is not None else dict()), **(dict(targeted_end_date=full_project_data['targeted_end_date']) if full_project_data['targeted_end_date'] is not None else dict()), **(dict(status=full_project_data['status']) if full_project_data['status'] is not None else dict()), **(dict(comment=full_project_data['comment']) if full_project_data['comment'] is not None else dict()), diff --git a/frontend/src/components/filters/descriptions.js b/frontend/src/components/filters/descriptions.js index bf7bb4fba2..3ebb4854a8 100644 --- a/frontend/src/components/filters/descriptions.js +++ b/frontend/src/components/filters/descriptions.js @@ -364,7 +364,7 @@ export const PROJECT_FILTERS = { }, principal_investigator: { type: FILTER_TYPE.INPUT, - key: "principal_investigator", + key: "parent_project__principal_investigator", label: "Principal Investigator", }, status: { @@ -377,7 +377,7 @@ export const PROJECT_FILTERS = { }, requestor_name: { type: FILTER_TYPE.INPUT, - key: "requestor_name", + key: "parent_project__requestor_name", label: "Requestor Name", }, targeted_end_date: { From a287f452642b20d444f2014129adacc08002f43d Mon Sep 17 00:00:00 2001 From: UlysseFG Date: Wed, 2 Sep 2026 16:13:58 -0400 Subject: [PATCH 2/3] Fix broken tests. --- .../tests/test_models/project_test.py | 36 ++++++------------- .../project_study_link_samples_test.py | 2 -- .../tests/test_services/project_test.py | 34 +++++++++++------- .../sample_add_samples_to_study_test.py | 2 -- .../tests/test_models/production_data_test.py | 2 +- 5 files changed, 32 insertions(+), 44 deletions(-) diff --git a/backend/fms_core/tests/test_models/project_test.py b/backend/fms_core/tests/test_models/project_test.py index c71352310f..ddf0ec3935 100644 --- a/backend/fms_core/tests/test_models/project_test.py +++ b/backend/fms_core/tests/test_models/project_test.py @@ -28,15 +28,9 @@ def setUp(self): def test_project(self): my_project = Project.objects.create(name=self.name, - principal_investigator=self.principal_investigator, - requestor_name=self.requestor_name, - requestor_email=self.requestor_email, targeted_end_date=self.targeted_end_date, comment=self.comment) self.assertEqual(my_project.name, self.name) - self.assertEqual(my_project.principal_investigator, self.principal_investigator) - self.assertEqual(my_project.requestor_name, self.requestor_name) - self.assertEqual(my_project.requestor_email, self.requestor_email) self.assertEqual(my_project.status, self.status) self.assertEqual(my_project.comment, self.comment) self.assertEqual(my_project.targeted_end_date, datetime.strptime(self.targeted_end_date, "%Y-%m-%d").date()) @@ -44,10 +38,7 @@ def test_project(self): def test_missing_name(self): with self.assertRaises(ValidationError): try: - er_without_et = Project.objects.create(principal_investigator=self.principal_investigator, - requestor_name=self.requestor_name, - requestor_email=self.requestor_email, - status=self.status) + er_without_et = Project.objects.create(status=self.status) except ValidationError as e: self.assertTrue("name" in e.message_dict) raise e @@ -56,16 +47,12 @@ def test_duplicate_project_with_name(self): with self.assertRaises(ValidationError): # First Project is valid Project.objects.create(name=self.duplicate_name, - principal_investigator=self.principal_investigator, status=self.status, targeted_end_date=self.targeted_end_date) try: # Second Project has the same name, should be invalid Project.objects.create(name=self.duplicate_name, - principal_investigator=self.principal_investigator, - requestor_name=self.requestor_name, - requestor_email=self.requestor_email, status=self.status, targeted_end_date=self.targeted_end_date) except ValidationError as e: @@ -76,16 +63,12 @@ def test_project_with_similar_name(self): with self.assertRaises(ValidationError): # First Project is valid Project.objects.create(name=self.name, - principal_investigator=self.principal_investigator, status=self.status, targeted_end_date=self.targeted_end_date) try: # Second Project has a similar name, but different upper/lower cases, should be invalid Project.objects.create(name=self.similar_name, - principal_investigator=self.principal_investigator, - requestor_name=self.requestor_name, - requestor_email=self.requestor_email, status=self.status, targeted_end_date=self.targeted_end_date) except ValidationError as e: @@ -94,21 +77,22 @@ def test_project_with_similar_name(self): def test_empty_parent_project(self): my_project = Project.objects.create(name=self.name, - principal_investigator=self.principal_investigator, - requestor_name=self.requestor_name, - requestor_email=self.requestor_email, targeted_end_date=self.targeted_end_date, comment=self.comment) self.assertEqual(my_project.parent_project, None) def test_not_empty_parent_project(self): - parent_project_obj = ParentProject.objects.create(external_id="P000010", name="TESTS") + parent_project_obj = ParentProject.objects.create(external_id="P000010", + name="TESTS", + principal_investigator=self.principal_investigator, + requestor_name=self.requestor_name, + requestor_email=self.requestor_email) my_project = Project.objects.create(name=self.name, - principal_investigator=self.principal_investigator, - requestor_name=self.requestor_name, - requestor_email=self.requestor_email, targeted_end_date=self.targeted_end_date, parent_project=parent_project_obj, comment=self.comment) self.assertEqual(my_project.parent_project.external_id, "P000010") - self.assertEqual(my_project.parent_project.name, "TESTS") \ No newline at end of file + self.assertEqual(my_project.parent_project.name, "TESTS") + self.assertEqual(my_project.parent_project.principal_investigator, self.principal_investigator) + self.assertEqual(my_project.parent_project.requestor_name, self.requestor_name) + self.assertEqual(my_project.parent_project.requestor_email, self.requestor_email) \ No newline at end of file diff --git a/backend/fms_core/tests/test_services/project_study_link_samples_test.py b/backend/fms_core/tests/test_services/project_study_link_samples_test.py index 4386473dc4..b509f9df4e 100644 --- a/backend/fms_core/tests/test_services/project_study_link_samples_test.py +++ b/backend/fms_core/tests/test_services/project_study_link_samples_test.py @@ -13,7 +13,6 @@ def setUp(self) -> None: self.valid_sample_name = "SampleTest" self.valid_project_name = "MyValidProject" self.invalid_project_name = "MyInvalidProject" - self.principal_investigator = "PepitoPerez" self.valid_status = "Open" self.invalid_status = "Ongoing" self.valid_target_end_date = "2030-12-21" @@ -26,7 +25,6 @@ def setUp(self) -> None: ) self.project, errors, warnings = create_project(name=self.valid_project_name, - principal_investigator=self.principal_investigator, status=self.valid_status, targeted_end_date=self.valid_target_end_date) diff --git a/backend/fms_core/tests/test_services/project_test.py b/backend/fms_core/tests/test_services/project_test.py index 030f8c30ab..e903baf67a 100644 --- a/backend/fms_core/tests/test_services/project_test.py +++ b/backend/fms_core/tests/test_services/project_test.py @@ -21,19 +21,16 @@ def setUp(self) -> None: def test_create_valid_project(self): project, errors, warnings = create_project(name=self.valid_project_name, - principal_investigator=self.principal_investigator, status=self.valid_status, targeted_end_date=self.valid_target_end_date) self.assertEqual(project.name, self.valid_project_name) - self.assertEqual(project.principal_investigator, self.principal_investigator) self.assertEqual(project.status, self.valid_status) self.assertEqual(errors, []) self.assertEqual(warnings, []) def test_create_project_invalid_name(self): project, errors, warnings = create_project(name=None, - principal_investigator=self.principal_investigator, status=self.invalid_status, targeted_end_date=self.invalid_target_end_date) @@ -42,7 +39,6 @@ def test_create_project_invalid_name(self): def test_create_project_invalid_status(self): project, errors, warnings = create_project(name=self.valid_project_name, - principal_investigator=self.principal_investigator, status=self.invalid_status, targeted_end_date=self.valid_target_end_date) @@ -51,7 +47,6 @@ def test_create_project_invalid_status(self): def test_create_project_invalid_target_date(self): project, errors, warnings = create_project(name=self.valid_project_name, - principal_investigator=self.principal_investigator, status=self.valid_status, targeted_end_date=self.invalid_target_end_date) @@ -60,14 +55,12 @@ def test_create_project_invalid_target_date(self): def test_get_valid_project(self): project, errors, warnings = create_project(name=self.valid_project_name, - principal_investigator=self.principal_investigator, status=self.valid_status, targeted_end_date=self.valid_target_end_date) project, errors, warnings = get_project(name=self.valid_project_name) self.assertEqual(project.name, self.valid_project_name) - self.assertEqual(project.principal_investigator, self.principal_investigator) self.assertEqual(project.status, self.valid_status) self.assertEqual(errors, []) self.assertEqual(warnings, []) @@ -86,23 +79,38 @@ def test_get_invalid_project(self): self.assertEqual(warnings, []) def test_create_parent_project(self): - parent_project, errors, warnings = create_parent_project(external_id=self.external_id, name=self.external_name) + parent_project, errors, warnings = create_parent_project(external_id=self.external_id, + name=self.external_name, + principal_investigator=self.principal_investigator, + requestor_name=self.requestor_name, + requestor_email=self.requestor_email) self.assertIsNotNone(parent_project) self.assertEqual(parent_project.external_id, self.external_id) self.assertEqual(parent_project.name, self.external_name) + self.assertEqual(parent_project.principal_investigator, self.principal_investigator) + self.assertEqual(parent_project.requestor_name, self.requestor_name) + self.assertEqual(parent_project.requestor_email, self.requestor_email) self.assertEqual(errors, []) self.assertEqual(warnings, []) def test_create_parent_project_without_external_id(self): - parent_project, errors, warnings = create_parent_project(external_id=None, name=self.external_name) + parent_project, errors, warnings = create_parent_project(external_id=None, + name=self.external_name, + principal_investigator=self.principal_investigator, + requestor_name=self.requestor_name, + requestor_email=self.requestor_email) self.assertIsNone(parent_project) self.assertEqual(errors, ["{'external_id': ['This field cannot be null.']}"]) self.assertEqual(warnings, []) def test_create_parent_project_without_external_name(self): - parent_project, errors, warnings = create_parent_project(external_id=self.external_id, name=None) + parent_project, errors, warnings = create_parent_project(external_id=self.external_id, + name=None, + principal_investigator=self.principal_investigator, + requestor_name=self.requestor_name, + requestor_email=self.requestor_email) self.assertIsNone(parent_project) self.assertEqual(errors, ["{'name': ['This field cannot be null.']}"]) @@ -121,9 +129,9 @@ def test_create_full_project(self): self.assertIsNotNone(project) self.assertEqual(project.name, self.valid_project_name) - self.assertEqual(project.principal_investigator, self.principal_investigator) - self.assertEqual(project.requestor_name, self.requestor_name) - self.assertEqual(project.requestor_email, self.requestor_email) + self.assertEqual(project.parent_project.principal_investigator, self.principal_investigator) + self.assertEqual(project.parent_project.requestor_name, self.requestor_name) + self.assertEqual(project.parent_project.requestor_email, self.requestor_email) self.assertEqual(project.parent_project.external_id, self.external_id) self.assertEqual(project.parent_project.name, self.external_name) self.assertEqual(project.status, self.valid_status) diff --git a/backend/fms_core/tests/test_services/sample_add_samples_to_study_test.py b/backend/fms_core/tests/test_services/sample_add_samples_to_study_test.py index 50536666ca..0eff7dc5fd 100644 --- a/backend/fms_core/tests/test_services/sample_add_samples_to_study_test.py +++ b/backend/fms_core/tests/test_services/sample_add_samples_to_study_test.py @@ -30,7 +30,6 @@ def setUp(self) -> None: sample_kind=sk_DNA, creation_date="2022-01-01") self.project1, _, _ = create_project(name="Project1", - principal_investigator=principal_investigator, status=valid_status, targeted_end_date=valid_target_end_date) create_link(self.sample_project1, self.project1) @@ -47,7 +46,6 @@ def setUp(self) -> None: # project 2 models self.project2, _, _ = create_project(name="Project2", - principal_investigator=principal_investigator, status=valid_status, targeted_end_date=valid_target_end_date) self.study_project2_A = Study.objects.create(letter='A', diff --git a/backend/fms_report/tests/test_models/production_data_test.py b/backend/fms_report/tests/test_models/production_data_test.py index 623a0ac260..f312605324 100644 --- a/backend/fms_report/tests/test_models/production_data_test.py +++ b/backend/fms_report/tests/test_models/production_data_test.py @@ -98,7 +98,7 @@ def test_production_data(self): self.assertIsNone(data.library_selection) self.assertEqual(data.project.name, self.project.name) self.assertEqual(data.project.parent_project.external_id, self.project.parent_project.external_id) - self.assertEqual(data.project.principal_investigator, self.project.principal_investigator) + self.assertEqual(data.project.parent_project.principal_investigator, self.project.parent_project.principal_investigator) self.assertEqual(data.taxon, "E.T.") self.assertEqual(data.technology, "SeqEnhancer") self.assertEqual(data.reads, 10) From 8ee3aeab0d9552a9d6d02dec82ba7821b14f8d24 Mon Sep 17 00:00:00 2001 From: UlysseFG Date: Wed, 2 Sep 2026 16:37:03 -0400 Subject: [PATCH 3/3] Fix project edit page. update frontend parent_project model. Fix filters on moved fields. --- backend/fms_core/serializers.py | 6 ++++++ .../components/projects/ProjectEditContent.tsx | 6 ++++++ .../components/projects/ProjectsTableColumns.tsx | 16 ++++++++-------- frontend/src/models/fms_api_models.ts | 3 +++ 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/backend/fms_core/serializers.py b/backend/fms_core/serializers.py index 964564951b..6470eabe46 100644 --- a/backend/fms_core/serializers.py +++ b/backend/fms_core/serializers.py @@ -599,12 +599,18 @@ class Meta: class ProjectSerializer(serializers.ModelSerializer): external_id = serializers.CharField(read_only=True, source="parent_project.external_id") external_name = serializers.CharField(read_only=True, source="parent_project.name") + principal_investigator = serializers.CharField(read_only=True, source="parent_project.principal_investigator") + requestor_name = serializers.CharField(read_only=True, source="parent_project.requestor_name") + requestor_email = serializers.CharField(read_only=True, source="parent_project.requestor_email") class Meta: model = Project fields = '__all__' class ProjectExportSerializer(serializers.ModelSerializer): + principal_investigator = serializers.CharField(read_only=True, source="parent_project.principal_investigator") + requestor_name = serializers.CharField(read_only=True, source="parent_project.requestor_name") + requestor_email = serializers.CharField(read_only=True, source="parent_project.requestor_email") class Meta: model = Project fields = ("id", "name", "principal_investigator", "requestor_name", "requestor_email", "status", "targeted_end_date", "comment") diff --git a/frontend/src/components/projects/ProjectEditContent.tsx b/frontend/src/components/projects/ProjectEditContent.tsx index 9f7b2e2ebd..d0b5e8efb6 100644 --- a/frontend/src/components/projects/ProjectEditContent.tsx +++ b/frontend/src/components/projects/ProjectEditContent.tsx @@ -86,7 +86,13 @@ const ProjectEditContent = () => { else { listParentProjects(input).then(response => { const currentExternalName = response.data.results[0]?.name + const currentPrincipalInvestigator = response.data.results[0]?.principal_investigator + const currentRequestorName = response.data.results[0]?.requestor_name + const currentRequestorEmail = response.data.results[0]?.requestor_email form.setFieldsValue({"external_name": currentExternalName}) + form.setFieldsValue({"principal_investigator": currentPrincipalInvestigator}) + form.setFieldsValue({"requestor_name": currentRequestorName}) + form.setFieldsValue({"requestor_email": currentRequestorEmail}) }) } }, []) diff --git a/frontend/src/components/projects/ProjectsTableColumns.tsx b/frontend/src/components/projects/ProjectsTableColumns.tsx index f56023c539..e1e4008d30 100644 --- a/frontend/src/components/projects/ProjectsTableColumns.tsx +++ b/frontend/src/components/projects/ProjectsTableColumns.tsx @@ -106,12 +106,12 @@ export const PROJECT_FILTERS: { [key in ProjectColumnID]: FilterDescription } = }, [ProjectColumnID.EXTERNAL_ID]: { type: FILTER_TYPE.INPUT, - key: 'external_id', + key: 'parent_project__external_id', label: 'External ID', }, [ProjectColumnID.PRINCIPAL_INVESTIGATOR]: { type: FILTER_TYPE.INPUT, - key: 'principal_investigator', + key: 'parent_project__principal_investigator', label: 'Principal Investigator', }, [ProjectColumnID.STATUS]: { @@ -124,12 +124,12 @@ export const PROJECT_FILTERS: { [key in ProjectColumnID]: FilterDescription } = }, [ProjectColumnID.REQUESTOR_NAME]: { type: FILTER_TYPE.INPUT, - key: 'requestor_name', + key: 'parent_project__requestor_name', label: 'Requestor Name', }, [ProjectColumnID.REQUESTOR_EMAIL]: { type: FILTER_TYPE.INPUT, - key: UNDEFINED_FILTER_KEY, + key: 'parent_project__requestor_email', label: 'Requestor Email' }, [ProjectColumnID.TARGETED_END_DATE]: { @@ -142,10 +142,10 @@ export const PROJECT_FILTERS: { [key in ProjectColumnID]: FilterDescription } = export const PROJECT_FILTER_KEYS: { [key in ProjectColumnID]: string } = { [ProjectColumnID.ID]: 'id', [ProjectColumnID.NAME]: 'name', - [ProjectColumnID.EXTERNAL_ID]: 'external_id', - [ProjectColumnID.PRINCIPAL_INVESTIGATOR]: 'principal_investigator', - [ProjectColumnID.REQUESTOR_NAME]: 'requestor_name', - [ProjectColumnID.REQUESTOR_EMAIL]: 'requestor_email', + [ProjectColumnID.EXTERNAL_ID]: 'parent_project__external_id', + [ProjectColumnID.PRINCIPAL_INVESTIGATOR]: 'parent_project__principal_investigator', + [ProjectColumnID.REQUESTOR_NAME]: 'parent_project__requestor_name', + [ProjectColumnID.REQUESTOR_EMAIL]: 'parent_project__requestor_email', [ProjectColumnID.STATUS]: 'status', [ProjectColumnID.TARGETED_END_DATE]: 'targeted_end_date', } diff --git a/frontend/src/models/fms_api_models.ts b/frontend/src/models/fms_api_models.ts index c13e75e466..386ed44f2d 100644 --- a/frontend/src/models/fms_api_models.ts +++ b/frontend/src/models/fms_api_models.ts @@ -346,6 +346,9 @@ export interface FMSProcessMeasurement extends FMSTrackedModel { export interface FMSParentProject extends FMSTrackedModel { name: string // The name of the project on external system external_id: string // Identifier to connect to an external system (eg. Hercules) + principal_investigator: string // The principal investigator of the project + requestor_name: string // Name of requestor + requestor_email: string // The email of the requestor of the project projects?: FMSId[] // IDs of related Freezeman projects }