diff --git a/chord_metadata_service/chord/ingest/phenopackets.py b/chord_metadata_service/chord/ingest/phenopackets.py index b01d2ce55..938edc60f 100644 --- a/chord_metadata_service/chord/ingest/phenopackets.py +++ b/chord_metadata_service/chord/ingest/phenopackets.py @@ -60,7 +60,7 @@ def get_or_create_phenotypic_feature(pf: dict) -> pm.PhenotypicFeature: description=pf.get("description", ""), pftype=pf["type"], excluded=pf.get("excluded", False), - modifier=pf.get("modifier", []), # TODO: Validate ontology term in schema... + modifiers=pf.get("modifiers", []), # TODO: Validate ontology term in schema... severity=pf.get("severity"), onset=pf.get("onset"), evidence=pf.get("evidence"), # TODO: Separate class for evidence? @@ -208,11 +208,22 @@ def get_or_create_genomic_interpretation(gen_interp: dict) -> pm.GenomicInterpre gene_descriptor = _get_or_create_opt("gene_descriptor", gen_interp, get_or_create_gene_descriptor) variant_interpretation = _get_or_create_opt("variant_interpretation", gen_interp, get_or_create_variant_interp) + # Check if a Biosample or Individual with subject_or_biosample_id exists + subject_or_biosample_id = gen_interp["subject_or_biosample_id"] + is_biosample = pm.Biosample.objects.filter(id=subject_or_biosample_id).exists() + is_subject = pm.Individual.objects.filter(id=subject_or_biosample_id).exists() + + # Store the type the ID refers to in extra_properties + element_type = "biosample" if is_biosample and not is_subject else "subject" + extra_properties = gen_interp.get("extra_properties", {}) + extra_properties["related_type"] = element_type + gen_obj, _ = pm.GenomicInterpretation.objects.get_or_create( - subject_or_biosample_id=gen_interp["subject_or_biosample_id"], + subject_or_biosample_id=subject_or_biosample_id, interpretation_status=gen_interp["interpretation_status"], gene_descriptor=gene_descriptor, - variant_interpretation=variant_interpretation + variant_interpretation=variant_interpretation, + extra_properties=extra_properties, ) return gen_obj diff --git a/chord_metadata_service/chord/tests/example_invalid_phenopacket.json b/chord_metadata_service/chord/tests/example_invalid_phenopacket.json index d28dc51f3..383291729 100644 --- a/chord_metadata_service/chord/tests/example_invalid_phenopacket.json +++ b/chord_metadata_service/chord/tests/example_invalid_phenopacket.json @@ -14,7 +14,7 @@ "label": "Hematuria" }, "negated": false, - "modifier": [] + "modifiers": [] }, { "description": "", @@ -27,7 +27,7 @@ "id": "HP:0012828", "label": "Severe" }, - "modifier": [] + "modifiers": [] } ], "diseases": [ @@ -229,4 +229,4 @@ "is_control_sample": false } ] -} \ No newline at end of file +} diff --git a/chord_metadata_service/chord/tests/test_ingest.py b/chord_metadata_service/chord/tests/test_ingest.py index 1abe51535..3977049da 100644 --- a/chord_metadata_service/chord/tests/test_ingest.py +++ b/chord_metadata_service/chord/tests/test_ingest.py @@ -54,7 +54,7 @@ def test_create_pf(self): "label": "Hematuria" }, "excluded": False, - "modifier": [], + "modifiers": [], "evidence": [] }) diff --git a/chord_metadata_service/patients/filters.py b/chord_metadata_service/patients/filters.py index 4d7e42ad6..73c416fb7 100644 --- a/chord_metadata_service/patients/filters.py +++ b/chord_metadata_service/patients/filters.py @@ -86,7 +86,7 @@ def filter_search(self, qs, name, value): "phenopackets__phenotypic_features__description", Cast("phenopackets__phenotypic_features__pftype", TextField()), Cast("phenopackets__phenotypic_features__severity", TextField()), - Cast("phenopackets__phenotypic_features__modifier", TextField()), + Cast("phenopackets__phenotypic_features__modifiers", TextField()), Cast("phenopackets__phenotypic_features__onset", TextField()), Cast("phenopackets__phenotypic_features__evidence", TextField()), Cast("phenopackets__phenotypic_features__extra_properties", TextField()), diff --git a/chord_metadata_service/phenopackets/descriptions.py b/chord_metadata_service/phenopackets/descriptions.py index 14e6f625b..8a743c160 100644 --- a/chord_metadata_service/phenopackets/descriptions.py +++ b/chord_metadata_service/phenopackets/descriptions.py @@ -111,7 +111,7 @@ def phenotypic_feature(subject="a subject or biosample"): "type": ontology_class("which describes the phenotype"), "negated": "Whether the feature is present (false) or absent (true, feature is negated); default is false.", "severity": ontology_class("that describes the severity of the condition"), - "modifier": { # TODO: Plural? + "modifiers": { "description": "A list of ontology terms that provide more expressive / precise descriptions of a " "phenotypic feature, including e.g. positionality or external factors.", "items": ontology_class("that expounds on the phenotypic feature") diff --git a/chord_metadata_service/phenopackets/migrations/0009_rename_modifier_phenotypicfeature_modifiers.py b/chord_metadata_service/phenopackets/migrations/0009_rename_modifier_phenotypicfeature_modifiers.py new file mode 100644 index 000000000..205ad3fe5 --- /dev/null +++ b/chord_metadata_service/phenopackets/migrations/0009_rename_modifier_phenotypicfeature_modifiers.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.6 on 2023-10-16 18:02 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('phenopackets', '0008_alter_biosample_procedure_delete_procedure'), + ] + + operations = [ + migrations.RenameField( + model_name='phenotypicfeature', + old_name='modifier', + new_name='modifiers', + ), + ] diff --git a/chord_metadata_service/phenopackets/models.py b/chord_metadata_service/phenopackets/models.py index 6b876de08..0ae1b0cb2 100644 --- a/chord_metadata_service/phenopackets/models.py +++ b/chord_metadata_service/phenopackets/models.py @@ -86,8 +86,8 @@ class PhenotypicFeature(BaseTimeStamp, IndexableMixin): excluded = models.BooleanField(default=False, help_text=rec_help(d.PHENOTYPIC_FEATURE, "negated")) severity = JSONField(blank=True, null=True, validators=[ontology_validator], help_text=rec_help(d.PHENOTYPIC_FEATURE, "severity")) - modifier = JSONField(blank=True, null=True, validators=[ontology_list_validator], - help_text=rec_help(d.PHENOTYPIC_FEATURE, "modifier")) + modifiers = JSONField(blank=True, null=True, validators=[ontology_list_validator], + help_text=rec_help(d.PHENOTYPIC_FEATURE, "modifiers")) onset = JSONField(blank=True, null=True, validators=[JsonSchemaValidator(schema=TIME_ELEMENT_SCHEMA)]) resolution = JSONField(blank=True, null=True, validators=[ JsonSchemaValidator(schema=TIME_ELEMENT_SCHEMA)]) diff --git a/chord_metadata_service/phenopackets/search_schemas.py b/chord_metadata_service/phenopackets/search_schemas.py index 0031ee359..1b813bb0b 100644 --- a/chord_metadata_service/phenopackets/search_schemas.py +++ b/chord_metadata_service/phenopackets/search_schemas.py @@ -2,6 +2,7 @@ from chord_metadata_service.patients.schemas import INDIVIDUAL_SCHEMA from chord_metadata_service.resources.search_schemas import RESOURCE_SEARCH_SCHEMA from chord_metadata_service.restapi.schema_utils import ( + array_of, merge_schema_dictionaries, search_db_fk, search_db_pk, @@ -176,13 +177,11 @@ def _tag_with_database_attrs(schema: dict, db_attrs: dict): } } }), - "negated": { + "excluded": { "search": search_optional_eq(1), }, "severity": ONTOLOGY_SEARCH_SCHEMA, - "modifier": { # TODO: Plural? - "items": ONTOLOGY_SEARCH_SCHEMA - }, + "modifiers": array_of(ONTOLOGY_SEARCH_SCHEMA), "onset": TIME_ELEMENT_SEARCH_SCHEMA, "resolution": TIME_ELEMENT_SEARCH_SCHEMA, "evidence": EVIDENCE_SEARCH_SCHEMA, @@ -301,11 +300,11 @@ def _tag_with_database_attrs(schema: dict, db_attrs: dict): "onset": DISEASE_ONSET_SEARCH_SCHEMA, "disease_stage": { "items": ONTOLOGY_SEARCH_SCHEMA, - "search": {"database": {"type": "array"}} + "search": {"database": {"type": "jsonb"}} }, - "tnm_finding": { + "clinical_tnm_finding": { "items": ONTOLOGY_SEARCH_SCHEMA, - "search": {"database": {"type": "array"}} + "search": {"database": {"type": "jsonb"}} }, }, "search": { diff --git a/chord_metadata_service/phenopackets/serializers.py b/chord_metadata_service/phenopackets/serializers.py index 527ac9a1b..c8a3e5aea 100644 --- a/chord_metadata_service/phenopackets/serializers.py +++ b/chord_metadata_service/phenopackets/serializers.py @@ -177,10 +177,15 @@ class Meta: def to_representation(self, instance): response = super().to_representation(instance) - response["gene_descriptor"] = GeneDescriptorSerializer( - instance.gene_descriptor, many=False, required=False).data - response["variant_interpretation"] = VariantInterpretationSerializer( - instance.variant_interpretation, many=False, required=False).data + + # May contain a gene_descriptor or a variant_interpretation, not both + if instance.gene_descriptor: + response["gene_descriptor"] = GeneDescriptorSerializer( + instance.gene_descriptor, many=False, required=False).data + elif instance.variant_interpretation: + response["variant_interpretation"] = VariantInterpretationSerializer( + instance.variant_interpretation, many=False, required=False).data + return response diff --git a/chord_metadata_service/phenopackets/tests/constants.py b/chord_metadata_service/phenopackets/tests/constants.py index 0f0f36f8a..c33af1cf5 100644 --- a/chord_metadata_service/phenopackets/tests/constants.py +++ b/chord_metadata_service/phenopackets/tests/constants.py @@ -538,7 +538,7 @@ def valid_phenotypic_feature(biosample=None, phenopacket=None): "id": "HP: 0012825", "label": "Mild" }, - modifier=[ + modifiers=[ { "id": "HP: 0012825 ", "label": "Mild" @@ -581,7 +581,7 @@ def invalid_phenotypic_feature(): "id": "HP: 0012825", "label": "Mild" }, - modifier=[ + modifiers=[ { "label": "Mild" }, diff --git a/chord_metadata_service/restapi/fhir_utils.py b/chord_metadata_service/restapi/fhir_utils.py index 4dcfd31b0..b01890487 100644 --- a/chord_metadata_service/restapi/fhir_utils.py +++ b/chord_metadata_service/restapi/fhir_utils.py @@ -182,7 +182,7 @@ def fhir_observation(obj): ) observation.extension = [] concept_extensions = codeable_concepts_fields( - ['severity', 'modifier', 'onset'], 'phenotypic_feature', obj + ['severity', 'modifiers', 'onset'], 'phenotypic_feature', obj ) for ce in concept_extensions: observation.extension.append(ce) diff --git a/chord_metadata_service/restapi/semantic_mappings/phenopackets_on_fhir_mapping.py b/chord_metadata_service/restapi/semantic_mappings/phenopackets_on_fhir_mapping.py index 8bc1d8c29..917d1af75 100644 --- a/chord_metadata_service/restapi/semantic_mappings/phenopackets_on_fhir_mapping.py +++ b/chord_metadata_service/restapi/semantic_mappings/phenopackets_on_fhir_mapping.py @@ -81,7 +81,7 @@ "title": "Phenotypic Feature", "url": "http://ga4gh.org/fhir/phenopackets/StructureDefinition/PhenotypicFeature", "severity": "http://ga4gh.org/fhir/phenopackets/StructureDefinition/phenotypic-feature-severity", - "modifier": "http://ga4gh.org/fhir/phenopackets/StructureDefinition/phenotypic-feature-modifier", + "modifiers": "http://ga4gh.org/fhir/phenopackets/StructureDefinition/phenotypic-feature-modifier", "onset": "http://ga4gh.org/fhir/phenopackets/StructureDefinition/phenotypic-feature-onset", "evidence": { "url": "http://ga4gh.org/fhir/phenopackets/StructureDefinition/evidence", diff --git a/examples/fhir-mapping.json b/examples/fhir-mapping.json index b146cb42f..c38d86408 100644 --- a/examples/fhir-mapping.json +++ b/examples/fhir-mapping.json @@ -33,7 +33,7 @@ } ] }, - "modifier": { + "modifiers": { "url": "http://ga4gh.org/fhir/phenopackets/StructureDefinition/phenotypic-feature-modifier", "coding": [ { diff --git a/examples/single.json b/examples/single.json index b14f6c2ed..e517143bd 100644 --- a/examples/single.json +++ b/examples/single.json @@ -13,7 +13,7 @@ "label": "Hematuria" }, "negated": false, - "modifier": [], + "modifiers": [], "evidence": [] }, { @@ -27,7 +27,7 @@ "id": "HP:0012828", "label": "Severe" }, - "modifier": [], + "modifiers": [], "evidence": [] } ],