From 00a1065331176f533307bb45fa45e5fe920b6642 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Wed, 29 Apr 2020 11:32:23 -0400 Subject: [PATCH 001/190] Miscellaneous cleanup --- chord_metadata_service/phenopackets/models.py | 19 ++-- chord_metadata_service/restapi/schemas.py | 98 ++++++++++--------- chord_metadata_service/restapi/tests/utils.py | 6 +- chord_metadata_service/restapi/validators.py | 29 +++--- 4 files changed, 84 insertions(+), 68 deletions(-) diff --git a/chord_metadata_service/phenopackets/models.py b/chord_metadata_service/phenopackets/models.py index d641fa1f8..ea6efb1ae 100644 --- a/chord_metadata_service/phenopackets/models.py +++ b/chord_metadata_service/phenopackets/models.py @@ -7,12 +7,14 @@ from chord_metadata_service.restapi.models import IndexableMixin from chord_metadata_service.restapi.validators import JsonSchemaValidator from chord_metadata_service.restapi.schemas import ( - UPDATE_SCHEMA, EXTERNAL_REFERENCE, EVIDENCE, ALLELE_SCHEMA, DISEASE_ONSET + UPDATE_SCHEMA, + EXTERNAL_REFERENCE, + EVIDENCE, + ALLELE_SCHEMA, + DISEASE_ONSET, ) import chord_metadata_service.phenopackets.descriptions as d -from chord_metadata_service.restapi.validators import ( - ontology_validator, ontology_list_validator, age_or_age_range_validator -) +from chord_metadata_service.restapi.validators import ontology_validator, age_or_age_range_validator ############################################################# @@ -59,7 +61,7 @@ class MetaData(models.Model): resources = models.ManyToManyField(Resource, help_text=rec_help(d.META_DATA, "resources")) updates = ArrayField( JSONField(null=True, blank=True, - validators=[JsonSchemaValidator(schema=UPDATE_SCHEMA, format_checker=['date-time'])]), + validators=[JsonSchemaValidator(schema=UPDATE_SCHEMA, formats=['date-time'])]), blank=True, null=True, help_text=rec_help(d.META_DATA, "updates")) phenopacket_schema_version = models.CharField(max_length=200, blank=True, help_text='Schema version of the current phenopacket.') @@ -90,8 +92,7 @@ class PhenotypicFeature(models.Model, IndexableMixin): FHIR: Condition or Observation """ - description = models.CharField( - max_length=200, blank=True, help_text=rec_help(d.PHENOTYPIC_FEATURE, "description")) + description = models.CharField(max_length=200, blank=True, help_text=rec_help(d.PHENOTYPIC_FEATURE, "description")) pftype = JSONField(verbose_name='type', validators=[ontology_validator], help_text=rec_help(d.PHENOTYPIC_FEATURE, "type")) negated = models.BooleanField(default=False, help_text=rec_help(d.PHENOTYPIC_FEATURE, "negated")) @@ -152,7 +153,7 @@ class HtsFile(models.Model, IndexableMixin): ('CRAM', 'CRAM'), ('VCF', 'VCF'), ('BCF', 'BCF'), - ('GVCF', 'GVCF') + ('GVCF', 'GVCF'), ) uri = models.URLField(primary_key=True, max_length=200, help_text=rec_help(d.HTS_FILE, "uri")) description = models.CharField(max_length=200, blank=True, help_text=rec_help(d.HTS_FILE, "description")) @@ -208,7 +209,7 @@ class Variant(models.Model): ('hgvsAllele', 'hgvsAllele'), ('vcfAllele', 'vcfAllele'), ('spdiAllele', 'spdiAllele'), - ('iscnAllele', 'iscnAllele') + ('iscnAllele', 'iscnAllele'), ) allele_type = models.CharField(max_length=200, choices=ALLELE, help_text="One of four allele types.") allele = JSONField(validators=[JsonSchemaValidator(schema=ALLELE_SCHEMA)], diff --git a/chord_metadata_service/restapi/schemas.py b/chord_metadata_service/restapi/schemas.py index 00b726144..9f492fe11 100644 --- a/chord_metadata_service/restapi/schemas.py +++ b/chord_metadata_service/restapi/schemas.py @@ -143,7 +143,7 @@ "description": "The schema represents a key-value object.", "type": "object", "patternProperties": { - "^.*$": { "type": "string" } + "^.*$": {"type": "string"} }, "additionalProperties": False } @@ -330,51 +330,61 @@ def customize_schema(first_typeof: dict, second_typeof: dict, first_property: str, second_property: str, - id: str=None, title: str=None, description: str=None, additionalProperties=False, - required=None) -> dict: + schema_id: str, title: str = None, description: str = None, additional_properties: bool = False, + required=None) -> dict: if required is None: required = [] return { - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": id, - "title": title, - "description": description, - "type": "object", - "properties": { - first_property: first_typeof, - second_property: second_typeof - }, - "required": required, - "additionalProperties": additionalProperties - } - + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": schema_id, + "title": title, + "description": description, + "type": "object", + "properties": { + first_property: first_typeof, + second_property: second_typeof + }, + "required": required, + "additionalProperties": additional_properties + } -COMORBID_CONDITION = customize_schema(first_typeof=ONTOLOGY_CLASS, second_typeof=ONTOLOGY_CLASS, - first_property="clinical_status", second_property="code", - id="chord_metadata_service:comorbid_condition_schema", - title="Comorbid Condition schema", - description="Comorbid condition schema.") - -#TODO this is definitely should be changed, fhir datatypes are too complex use Ontology_ class -COMPLEX_ONTOLOGY = customize_schema(first_typeof=ONTOLOGY_CLASS, second_typeof=ONTOLOGY_CLASS, - first_property="data_value", second_property="staging_system", - id="chord_metadata_service:complex_ontology_schema", title="Complex ontology", - description="Complex object to combine data value and staging system.", - required=["data_value"]) - -#TODO this is definitely should be changed, fhir datatypes are too complex use Ontology_ class -TUMOR_MARKER_TEST = customize_schema(first_typeof=ONTOLOGY_CLASS, - second_typeof={ - "anyOf": [ - ONTOLOGY_CLASS, - QUANTITY, - RATIO - ] - }, - first_property="code", second_property="data_value", - id="chord_metadata_service:tumor_marker_test", - title="Tumor marker test", - description="Tumor marker test schema.", - required=["code"] - ) +COMORBID_CONDITION = customize_schema( + first_typeof=ONTOLOGY_CLASS, + second_typeof=ONTOLOGY_CLASS, + first_property="clinical_status", + second_property="code", + schema_id="chord_metadata_service:comorbid_condition_schema", + title="Comorbid Condition schema", + description="Comorbid condition schema." +) + +# TODO this is definitely should be changed, fhir datatypes are too complex use Ontology_ class +COMPLEX_ONTOLOGY = customize_schema( + first_typeof=ONTOLOGY_CLASS, + second_typeof=ONTOLOGY_CLASS, + first_property="data_value", + second_property="staging_system", + schema_id="chord_metadata_service:complex_ontology_schema", + title="Complex ontology", + description="Complex object to combine data value and staging system.", + required=["data_value"] +) + +# TODO this is definitely should be changed, fhir datatypes are too complex use Ontology_ class +TUMOR_MARKER_TEST = customize_schema( + first_typeof=ONTOLOGY_CLASS, + second_typeof={ + "anyOf": [ + ONTOLOGY_CLASS, + QUANTITY, + RATIO + ] + }, + first_property="code", + second_property="data_value", + schema_id="chord_metadata_service:tumor_marker_test", + title="Tumor marker test", + description="Tumor marker test schema.", + required=["code"] +) diff --git a/chord_metadata_service/restapi/tests/utils.py b/chord_metadata_service/restapi/tests/utils.py index c9dbd05c3..b2d1dcdaa 100644 --- a/chord_metadata_service/restapi/tests/utils.py +++ b/chord_metadata_service/restapi/tests/utils.py @@ -2,16 +2,14 @@ from django.urls import reverse from rest_framework.test import APIClient + # Helper functions for tests def get_response(viewname, obj): """ Generic POST function. """ - client = APIClient() - #print(json.dumps(obj)) - response = client.post( + return client.post( reverse(viewname), data=json.dumps(obj), content_type='application/json' ) - return response diff --git a/chord_metadata_service/restapi/validators.py b/chord_metadata_service/restapi/validators.py index 78e7478b1..175003f42 100644 --- a/chord_metadata_service/restapi/validators.py +++ b/chord_metadata_service/restapi/validators.py @@ -1,18 +1,25 @@ from rest_framework import serializers from jsonschema import Draft7Validator, FormatChecker from chord_metadata_service.restapi.schemas import ( - ONTOLOGY_CLASS, QUANTITY, COMPLEX_ONTOLOGY, TIME_OR_PERIOD, TUMOR_MARKER_TEST, - ONTOLOGY_CLASS_LIST, KEY_VALUE_OBJECT, AGE_OR_AGE_RANGE, COMORBID_CONDITION + ONTOLOGY_CLASS, + QUANTITY, + COMPLEX_ONTOLOGY, + TIME_OR_PERIOD, + TUMOR_MARKER_TEST, + ONTOLOGY_CLASS_LIST, + KEY_VALUE_OBJECT, + AGE_OR_AGE_RANGE, + COMORBID_CONDITION, ) -class JsonSchemaValidator(object): +class JsonSchemaValidator: """ Custom class based validator to validate against Json schema for JSONField """ - def __init__(self, schema, format_checker=None): + def __init__(self, schema, formats=None): self.schema = schema - self.format_checker = format_checker - self.validator = Draft7Validator(self.schema, format_checker=FormatChecker(formats=self.format_checker)) + self.formats = formats + self.validator = Draft7Validator(self.schema, format_checker=FormatChecker(formats=self.formats)) def __call__(self, value): if not self.validator.is_valid(value): @@ -26,7 +33,7 @@ def deconstruct(self): return ( 'chord_metadata_service.restapi.validators.JsonSchemaValidator', [self.schema], - {} + {"formats": self.formats} ) @@ -34,8 +41,8 @@ def deconstruct(self): ontology_list_validator = JsonSchemaValidator(ONTOLOGY_CLASS_LIST) key_value_validator = JsonSchemaValidator(KEY_VALUE_OBJECT) age_or_age_range_validator = JsonSchemaValidator(AGE_OR_AGE_RANGE) -quantity_validator = JsonSchemaValidator(schema=QUANTITY, format_checker=['uri']) +quantity_validator = JsonSchemaValidator(schema=QUANTITY, formats=['uri']) tumor_marker_test_validator = JsonSchemaValidator(schema=TUMOR_MARKER_TEST) -complex_ontology_validator = JsonSchemaValidator(schema=COMPLEX_ONTOLOGY, format_checker=['uri']) -time_or_period_validator = JsonSchemaValidator(schema=TIME_OR_PERIOD, format_checker=['date-time']) -comorbid_condition_validator = JsonSchemaValidator(COMORBID_CONDITION) \ No newline at end of file +complex_ontology_validator = JsonSchemaValidator(schema=COMPLEX_ONTOLOGY, formats=['uri']) +time_or_period_validator = JsonSchemaValidator(schema=TIME_OR_PERIOD, formats=['date-time']) +comorbid_condition_validator = JsonSchemaValidator(COMORBID_CONDITION) From 6210a10b1bd7d75102597e888b167d36f9a570e2 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Wed, 29 Apr 2020 15:08:48 -0400 Subject: [PATCH 002/190] Code styling tweaks --- chord_metadata_service/mcode/api_views.py | 5 ++--- chord_metadata_service/mcode/descriptions.py | 3 ++- chord_metadata_service/mcode/models.py | 14 ++++++++------ chord_metadata_service/mcode/serializers.py | 13 +++++++++++++ chord_metadata_service/patients/api_views.py | 7 ++----- chord_metadata_service/patients/models.py | 9 ++++++--- chord_metadata_service/patients/serializers.py | 12 +++--------- chord_metadata_service/patients/tests/test_api.py | 12 ++++++------ chord_metadata_service/phenopackets/api_views.py | 2 +- .../phenopackets/tests/test_descriptions.py | 10 +++++----- chord_metadata_service/restapi/tests/test_fhir.py | 5 +++-- 11 files changed, 51 insertions(+), 41 deletions(-) diff --git a/chord_metadata_service/mcode/api_views.py b/chord_metadata_service/mcode/api_views.py index 4e3c26105..63284430d 100644 --- a/chord_metadata_service/mcode/api_views.py +++ b/chord_metadata_service/mcode/api_views.py @@ -1,9 +1,8 @@ from rest_framework import viewsets from rest_framework.settings import api_settings +from .models import * from .serializers import * -from chord_metadata_service.restapi.api_renderers import ( - PhenopacketsRenderer -) +from chord_metadata_service.restapi.api_renderers import PhenopacketsRenderer from chord_metadata_service.restapi.pagination import LargeResultsSetPagination diff --git a/chord_metadata_service/mcode/descriptions.py b/chord_metadata_service/mcode/descriptions.py index 550c39ef6..b154964bf 100644 --- a/chord_metadata_service/mcode/descriptions.py +++ b/chord_metadata_service/mcode/descriptions.py @@ -1,5 +1,6 @@ # Most parts of this text are taken from the mCODE:Minimal Common Oncology Data Elements Data Dictionary. -# The mCODE is made available under the Creative Commons 0 "No Rights Reserved" license https://creativecommons.org/share-your-work/public-domain/cc0/ +# The mCODE is made available under the Creative Commons 0 "No Rights Reserved" license +# https://creativecommons.org/share-your-work/public-domain/cc0/ # Portions of this text copyright (c) 2019-2020 the Canadian Centre for Computational Genomics; licensed under the # GNU Lesser General Public License version 3. diff --git a/chord_metadata_service/mcode/models.py b/chord_metadata_service/mcode/models.py index a38e5d1ab..0122ddd50 100644 --- a/chord_metadata_service/mcode/models.py +++ b/chord_metadata_service/mcode/models.py @@ -66,8 +66,8 @@ class GeneticVariantFound(models.Model, IndexableMixin): help_text=rec_help(d.GENETIC_VARIANT_FOUND, "variant_found_identifier")) variant_found_hgvs_name = ArrayField(models.CharField(max_length=200), blank=True, null=True, help_text=rec_help(d.GENETIC_VARIANT_FOUND, "variant_found_hgvs_name")) - variant_found_description = models.CharField(max_length=200, blank=True, - help_text=rec_help(d.GENETIC_VARIANT_FOUND, "variant_found_description")) + variant_found_description = models.CharField( + max_length=200, blank=True, help_text=rec_help(d.GENETIC_VARIANT_FOUND, "variant_found_description")) # loinc value set https://loinc.org/48002-0/ genomic_source_class = JSONField(blank=True, null=True, validators=[ontology_validator], help_text=rec_help(d.GENETIC_VARIANT_FOUND, "genomic_source_class")) @@ -95,8 +95,8 @@ class GenomicsReport(models.Model, IndexableMixin): id = models.CharField(primary_key=True, max_length=200, help_text=rec_help(d.GENOMICS_REPORT, "id")) test_name = JSONField(validators=[ontology_validator], help_text=rec_help(d.GENOMICS_REPORT, "test_name")) - performing_organization_name = models.CharField(max_length=200, blank=True, - help_text=rec_help(d.GENOMICS_REPORT, "performing_organization_name")) + performing_organization_name = models.CharField( + max_length=200, blank=True, help_text=rec_help(d.GENOMICS_REPORT, "performing_organization_name")) specimen_type = JSONField(blank=True, null=True, validators=[ontology_validator], help_text=rec_help(d.GENOMICS_REPORT, "specimen_type")) genetic_variant_tested = models.ManyToManyField(GeneticVariantTested, blank=True, @@ -138,7 +138,7 @@ class LabsVital(models.Model, IndexableMixin): help_text=rec_help(d.LABS_VITAL, "blood_pressure_diastolic")) blood_pressure_systolic = JSONField(blank=True, null=True, validators=[quantity_validator], help_text=rec_help(d.LABS_VITAL, "blood_pressure_systolic")) - #TODO Change CodeableConcept to Ontology class + # TODO Change CodeableConcept to Ontology class tumor_marker_test = JSONField(validators=[tumor_marker_test_validator], help_text=rec_help(d.LABS_VITAL, "tumor_marker_test")) extra_properties = JSONField(blank=True, null=True, @@ -247,7 +247,7 @@ class CancerRelatedProcedure(models.Model, IndexableMixin): occurence_time_or_period = JSONField(validators=[time_or_period_validator], help_text=rec_help(d.CANCER_RELATED_PROCEDURE, "occurence_time_or_period")) target_body_site = JSONField(null=True, validators=[ontology_list_validator], - help_text=rec_help(d.CANCER_RELATED_PROCEDURE, 'target_body_site')) + help_text=rec_help(d.CANCER_RELATED_PROCEDURE, 'target_body_site')) treatment_intent = JSONField(blank=True, null=True, validators=[ontology_validator], help_text=rec_help(d.CANCER_RELATED_PROCEDURE, "treatment_intent")) extra_properties = JSONField(blank=True, null=True, @@ -261,8 +261,10 @@ class Meta: def __str__(self): return str(self.id) + ###### Medication Statement ###### + class MedicationStatement(models.Model, IndexableMixin): """ Class to record the use of a medication. diff --git a/chord_metadata_service/mcode/serializers.py b/chord_metadata_service/mcode/serializers.py index 0ddc501ae..a6ca83737 100644 --- a/chord_metadata_service/mcode/serializers.py +++ b/chord_metadata_service/mcode/serializers.py @@ -3,6 +3,19 @@ from .models import * +__all__ = [ + "GeneticVariantTestedSerializer", + "GeneticVariantFoundSerializer", + "GenomicsReportSerializer", + "LabsVitalSerializer", + "TNMStagingSerializer", + "CancerConditionSerializer", + "CancerRelatedProcedureSerializer", + "MedicationStatementSerializer", + "MCodePacketSerializer", +] + + class GeneticVariantTestedSerializer(GenericSerializer): class Meta: diff --git a/chord_metadata_service/patients/api_views.py b/chord_metadata_service/patients/api_views.py index 35a3f6d4d..bcd3e8228 100644 --- a/chord_metadata_service/patients/api_views.py +++ b/chord_metadata_service/patients/api_views.py @@ -3,10 +3,7 @@ from .serializers import IndividualSerializer from .models import Individual from chord_metadata_service.phenopackets.api_views import BIOSAMPLE_PREFETCH, PHENOPACKET_PREFETCH -from chord_metadata_service.restapi.api_renderers import ( - FHIRRenderer, - PhenopacketsRenderer -) +from chord_metadata_service.restapi.api_renderers import FHIRRenderer, PhenopacketsRenderer from chord_metadata_service.restapi.pagination import LargeResultsSetPagination @@ -25,4 +22,4 @@ class IndividualViewSet(viewsets.ModelViewSet): ).order_by("id") serializer_class = IndividualSerializer pagination_class = LargeResultsSetPagination - renderer_classes = tuple(api_settings.DEFAULT_RENDERER_CLASSES) + (FHIRRenderer, PhenopacketsRenderer) + renderer_classes = (*api_settings.DEFAULT_RENDERER_CLASSES, FHIRRenderer, PhenopacketsRenderer) diff --git a/chord_metadata_service/patients/models.py b/chord_metadata_service/patients/models.py index a2fcb4b5e..2aa0b425e 100644 --- a/chord_metadata_service/patients/models.py +++ b/chord_metadata_service/patients/models.py @@ -2,7 +2,9 @@ from django.contrib.postgres.fields import JSONField, ArrayField from chord_metadata_service.restapi.models import IndexableMixin from chord_metadata_service.restapi.validators import ( - ontology_validator, age_or_age_range_validator, comorbid_condition_validator + ontology_validator, + age_or_age_range_validator, + comorbid_condition_validator, ) @@ -48,11 +50,12 @@ class Individual(models.Model, IndexableMixin): active = models.BooleanField(default=False, help_text='Whether this patient\'s record is in active use.') deceased = models.BooleanField(default=False, help_text='Indicates if the individual is deceased or not.') # mCode specific - # this field should be complex Ontology - clinical status and code - two Codeable concept - single, cl status has enum list of values + # this field should be complex Ontology - clinical status and code - two Codeable concept - single, cl status has + # enum list of values # TODO add these fields to FHIR converter ? comorbid_condition = JSONField(blank=True, null=True, validators=[comorbid_condition_validator], help_text='One or more conditions that occur with primary condition.') - #TODO decide use ONTOLOGY_CLASS vs. CODEABLE_CONCEPT - currently Ontology class + # TODO decide use ONTOLOGY_CLASS vs. CODEABLE_CONCEPT - currently Ontology class ecog_performance_status = JSONField(blank=True, null=True, validators=[ontology_validator], help_text='Value representing the Eastern Cooperative ' 'Oncology Group performance status.') diff --git a/chord_metadata_service/patients/serializers.py b/chord_metadata_service/patients/serializers.py index 2505d1e22..d66863456 100644 --- a/chord_metadata_service/patients/serializers.py +++ b/chord_metadata_service/patients/serializers.py @@ -1,18 +1,12 @@ -from chord_metadata_service.phenopackets.serializers import ( - BiosampleSerializer, - SimplePhenopacketSerializer -) +from chord_metadata_service.phenopackets.serializers import BiosampleSerializer, SimplePhenopacketSerializer from chord_metadata_service.restapi.serializers import GenericSerializer from chord_metadata_service.restapi.fhir_utils import fhir_patient from .models import Individual class IndividualSerializer(GenericSerializer): - biosamples = BiosampleSerializer( - read_only=True, many=True, exclude_when_nested=['individual']) - - phenopackets = SimplePhenopacketSerializer( - read_only=True, many=True, exclude_when_nested=['subject']) + biosamples = BiosampleSerializer(read_only=True, many=True, exclude_when_nested=['individual']) + phenopackets = SimplePhenopacketSerializer(read_only=True, many=True, exclude_when_nested=['subject']) class Meta: model = Individual diff --git a/chord_metadata_service/patients/tests/test_api.py b/chord_metadata_service/patients/tests/test_api.py index 3773ffaab..10cc9660b 100644 --- a/chord_metadata_service/patients/tests/test_api.py +++ b/chord_metadata_service/patients/tests/test_api.py @@ -3,7 +3,7 @@ from rest_framework import status from rest_framework.test import APITestCase from ..models import Individual -from .constants import * +from . import constants as c class CreateIndividualTest(APITestCase): @@ -11,8 +11,8 @@ class CreateIndividualTest(APITestCase): def setUp(self): - self.valid_payload = VALID_INDIVIDUAL - self.invalid_payload = INVALID_INDIVIDUAL + self.valid_payload = c.VALID_INDIVIDUAL + self.invalid_payload = c.INVALID_INDIVIDUAL def test_create_individual(self): """ POST a new individual. """ @@ -42,7 +42,7 @@ class UpdateIndividualTest(APITestCase): """ Test module for updating an existing Individual record. """ def setUp(self): - self.individual_one = Individual.objects.create(**VALID_INDIVIDUAL) + self.individual_one = Individual.objects.create(**c.VALID_INDIVIDUAL) self.put_valid_payload = { "id": "patient:1", @@ -64,7 +64,7 @@ def setUp(self): "active": False } - self.invalid_payload = INVALID_INDIVIDUAL + self.invalid_payload = c.INVALID_INDIVIDUAL def test_update_individual(self): """ PUT new data in an existing Individual record. """ @@ -97,7 +97,7 @@ class DeleteIndividualTest(APITestCase): """ Test module for deleting an existing Individual record. """ def setUp(self): - self.individual_one = Individual.objects.create(**VALID_INDIVIDUAL) + self.individual_one = Individual.objects.create(**c.VALID_INDIVIDUAL) def test_delete_individual(self): """ DELETE an existing Individual record. """ diff --git a/chord_metadata_service/phenopackets/api_views.py b/chord_metadata_service/phenopackets/api_views.py index 4ecc99b5d..46e086dc7 100644 --- a/chord_metadata_service/phenopackets/api_views.py +++ b/chord_metadata_service/phenopackets/api_views.py @@ -3,8 +3,8 @@ from chord_metadata_service.restapi.api_renderers import * from chord_metadata_service.restapi.pagination import LargeResultsSetPagination -from .serializers import * from .models import * +from .serializers import * class PhenopacketsModelViewSet(viewsets.ModelViewSet): diff --git a/chord_metadata_service/phenopackets/tests/test_descriptions.py b/chord_metadata_service/phenopackets/tests/test_descriptions.py index dfbcb7d11..fb271aaf0 100644 --- a/chord_metadata_service/phenopackets/tests/test_descriptions.py +++ b/chord_metadata_service/phenopackets/tests/test_descriptions.py @@ -1,5 +1,5 @@ -from chord_metadata_service.restapi.description_utils import * from django.test import TestCase +from chord_metadata_service.restapi import description_utils as du TEST_SCHEMA_1 = {"type": "string"} @@ -16,14 +16,14 @@ class TestDescriptions(TestCase): def test_descriptions(self): - assert isinstance(describe_schema(None, "Test"), dict) - self.assertDictEqual(describe_schema(TEST_SCHEMA_1, None), TEST_SCHEMA_1) + assert isinstance(du.describe_schema(None, "Test"), dict) + self.assertDictEqual(du.describe_schema(TEST_SCHEMA_1, None), TEST_SCHEMA_1) - d = describe_schema(TEST_SCHEMA_2, TEST_HELP_2) + d = du.describe_schema(TEST_SCHEMA_2, TEST_HELP_2) assert d["description"] == d["help"] assert d["help"] == "1" assert d["items"]["description"] == d["items"]["help"] assert d["items"]["description"] == "2" def test_help_get(self): - assert rec_help(TEST_HELP_2, "[item]") == "2" + assert du.rec_help(TEST_HELP_2, "[item]") == "2" diff --git a/chord_metadata_service/restapi/tests/test_fhir.py b/chord_metadata_service/restapi/tests/test_fhir.py index ab4731802..176bbe7f9 100644 --- a/chord_metadata_service/restapi/tests/test_fhir.py +++ b/chord_metadata_service/restapi/tests/test_fhir.py @@ -2,11 +2,13 @@ from chord_metadata_service.phenopackets.tests.constants import * from chord_metadata_service.patients.tests.constants import * from chord_metadata_service.restapi.tests.utils import get_response -from chord_metadata_service.phenopackets.serializers import * +from chord_metadata_service.phenopackets.models import * from rest_framework import status + # Tests for FHIR conversion functions + class FHIRPhenopacketTest(APITestCase): def setUp(self): @@ -227,4 +229,3 @@ def test_get_fhir(self): self.assertEqual(get_resp_obj['conditions'][0]['extension'][0]['url'], 'http://ga4gh.org/fhir/phenopackets/StructureDefinition/disease-tumor-stage') self.assertEqual(get_resp_obj['conditions'][0]['subject']['reference'], 'unknown') - \ No newline at end of file From 1bf1e2b43b2e189910834e32a5e7f8d4f29111a5 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Thu, 30 Apr 2020 10:07:09 -0400 Subject: [PATCH 003/190] Code cleanup and small tweaks --- chord_metadata_service/chord/views_search.py | 28 +++++----- .../phenopackets/descriptions.py | 2 +- .../phenopackets/serializers.py | 54 +++++++++++++++---- .../phenopackets/tests/test_api.py | 3 +- .../phenopackets/tests/test_models.py | 4 +- 5 files changed, 62 insertions(+), 29 deletions(-) diff --git a/chord_metadata_service/chord/views_search.py b/chord_metadata_service/chord/views_search.py index 506a10978..1e5a89558 100644 --- a/chord_metadata_service/chord/views_search.py +++ b/chord_metadata_service/chord/views_search.py @@ -9,7 +9,7 @@ from rest_framework.permissions import AllowAny from rest_framework.response import Response -from chord_lib.responses.errors import * +from chord_lib.responses import errors from chord_lib.search import build_search_response, postgres from chord_metadata_service.metadata.settings import DEBUG from chord_metadata_service.patients.models import Individual @@ -63,7 +63,7 @@ def data_type_phenopacket_metadata_schema(_request): def table_list(request): data_types = request.query_params.getlist("data-type") if PHENOPACKET_DATA_TYPE_ID not in data_types: - return Response(bad_request_error(f"Missing or invalid data type (Specified: {data_types})"), status=400) + return Response(errors.bad_request_error(f"Missing or invalid data type (Specified: {data_types})"), status=400) return Response([{ "id": d.identifier, @@ -89,7 +89,7 @@ def table_detail(request, table_id): # pragma: no cover try: table = Dataset.objects.get(identifier=table_id) except Dataset.DoesNotExist: - return Response(not_found_error(f"Table with ID {table_id} not found"), status=404) + return Response(errors.not_found_error(f"Table with ID {table_id} not found"), status=404) if request.method == "DELETE": table.delete() @@ -167,7 +167,7 @@ def count_individual(ind): }) except Dataset.DoesNotExist: - return Response(not_found_error(f"Table with ID {table_id} not found"), status=404) + return Response(errors.not_found_error(f"Table with ID {table_id} not found"), status=404) # TODO: CHORD-standardized logging @@ -202,21 +202,23 @@ def phenopacket_query_results(query, params): def search(request, internal_data=False): if "data_type" not in request.data: - return Response(bad_request_error("Missing data_type in request body"), status=400) + return Response(errors.bad_request_error("Missing data_type in request body"), status=400) if "query" not in request.data: - return Response(bad_request_error("Missing query in request body"), status=400) + return Response(errors.bad_request_error("Missing query in request body"), status=400) start = datetime.now() if request.data["data_type"] != PHENOPACKET_DATA_TYPE_ID: - return Response(bad_request_error(f"Missing or invalid data type (Specified: {request.data['data_type']})"), - status=400) + return Response( + errors.bad_request_error(f"Missing or invalid data type (Specified: {request.data['data_type']})"), + status=400 + ) try: compiled_query, params = postgres.search_query_to_psycopg2_sql(request.data["query"], PHENOPACKET_SCHEMA) except (SyntaxError, TypeError, ValueError) as e: - return Response(bad_request_error(f"Error compiling query (message: {str(e)})"), status=400) + return Response(errors.bad_request_error(f"Error compiling query (message: {str(e)})"), status=400) if not internal_data: datasets = Dataset.objects.filter(identifier__in=phenopacket_results( @@ -254,7 +256,7 @@ def chord_private_search(request): def phenopacket_filter_results(subject_ids, htsfile_ids, disease_ids, biosample_ids, - phenotypicfeature_ids, phenopacket_ids, prefetch=False): + phenotypicfeature_ids, phenopacket_ids): query = Phenopacket.objects.get_queryset() @@ -286,7 +288,7 @@ def fhir_search(request, internal_data=False): # TODO: not all that sure about the query format we'll want # keep it simple for now if "query" not in request.data: - return Response(bad_request_error("Missing query in request body"), status=400) + return Response(errors.bad_request_error("Missing query in request body"), status=400) query = request.data["query"] start = datetime.now() @@ -353,7 +355,7 @@ def chord_table_search(request, table_id, internal=False): if request.data is None or "query" not in request.data: # TODO: Better error - return Response(bad_request_error("Missing query in request body"), status=400) + return Response(errors.bad_request_error("Missing query in request body"), status=400) # Check that dataset exists dataset = Dataset.objects.get(identifier=table_id) @@ -362,7 +364,7 @@ def chord_table_search(request, table_id, internal=False): compiled_query, params = postgres.search_query_to_psycopg2_sql(request.data["query"], PHENOPACKET_SCHEMA) except (SyntaxError, TypeError, ValueError) as e: print("[CHORD Metadata] Error encountered compiling query {}:\n {}".format(request.data["query"], str(e))) - return Response(bad_request_error(f"Error compiling query (message: {str(e)})"), status=400) + return Response(errors.bad_request_error(f"Error compiling query (message: {str(e)})"), status=400) debug_log(f"Finished compiling query in {datetime.now() - start}") diff --git a/chord_metadata_service/phenopackets/descriptions.py b/chord_metadata_service/phenopackets/descriptions.py index 1b0299ecf..63b4f92e1 100644 --- a/chord_metadata_service/phenopackets/descriptions.py +++ b/chord_metadata_service/phenopackets/descriptions.py @@ -81,7 +81,7 @@ "description": "An update event for a record (e.g. a phenopacket.)", "properties": { "timestamp": { - "description": "ISO8601 timestamp specifying when when this update occurred.", + "description": "ISO8601 UTC timestamp specifying when when this update occurred.", "help": "Timestamp specifying when when this update occurred.", }, "updated_by": "Information about the person/organization/network that performed the update.", diff --git a/chord_metadata_service/phenopackets/serializers.py b/chord_metadata_service/phenopackets/serializers.py index f25bc72d2..4c1a0fe16 100644 --- a/chord_metadata_service/phenopackets/serializers.py +++ b/chord_metadata_service/phenopackets/serializers.py @@ -1,8 +1,40 @@ import re from rest_framework import serializers -from .models import * +from .models import ( + Resource, + MetaData, + PhenotypicFeature, + Procedure, + HtsFile, + Gene, + Variant, + Disease, + Biosample, + Phenopacket, + GenomicInterpretation, + Diagnosis, + Interpretation, +) +from chord_metadata_service.restapi import fhir_utils from chord_metadata_service.restapi.serializers import GenericSerializer -from chord_metadata_service.restapi.fhir_utils import * + + +__all__ = [ + "ResourceSerializer", + "MetaDataSerializer", + "PhenotypicFeatureSerializer", + "ProcedureSerializer", + "HtsFileSerializer", + "GeneSerializer", + "VariantSerializer", + "DiseaseSerializer", + "BiosampleSerializer", + "SimplePhenopacketSerializer", + "PhenopacketSerializer", + "GenomicInterpretationSerializer", + "DiagnosisSerializer", + "InterpretationSerializer", +] ############################################################# @@ -39,7 +71,7 @@ class Meta: exclude = ['pftype'] # meta info for converting to FHIR fhir_datatype_plural = 'observations' - class_converter = fhir_observation + class_converter = fhir_utils.fhir_observation class ProcedureSerializer(GenericSerializer): @@ -49,7 +81,7 @@ class Meta: fields = '__all__' # meta info for converting to FHIR fhir_datatype_plural = 'specimen.collections' - class_converter = fhir_specimen_collection + class_converter = fhir_utils.fhir_specimen_collection def create(self, validated_data): if validated_data.get('body_site'): @@ -67,7 +99,7 @@ class Meta: fields = '__all__' # meta info for converting to FHIR fhir_datatype_plural = 'document_references' - class_converter = fhir_document_reference + class_converter = fhir_utils.fhir_document_reference class GeneSerializer(GenericSerializer): @@ -80,7 +112,7 @@ class Meta: fields = '__all__' # meta info for converting to FHIR fhir_datatype_plural = 'observations' - class_converter = fhir_obs_component_region_studied + class_converter = fhir_utils.fhir_obs_component_region_studied class VariantSerializer(GenericSerializer): @@ -90,7 +122,7 @@ class Meta: fields = '__all__' # meta info for converting to FHIR fhir_datatype_plural = 'observations' - class_converter = fhir_obs_component_variant + class_converter = fhir_utils.fhir_obs_component_variant def to_representation(self, obj): """ Change 'allele_type' field name to allele type value. """ @@ -103,7 +135,7 @@ def to_internal_value(self, data): """ When writing back to db change field name back to 'allele'. """ if 'allele' not in data.keys(): - allele_type = data.get('allele_type') # e.g. spdiAllele + allele_type = data.get('allele_type') # e.g. spdiAllele # split by uppercase normilize = filter(None, re.split("([A-Z][^A-Z]*)", allele_type)) normilized_allele_type = '_'.join([i.lower() for i in normilize]) @@ -118,7 +150,7 @@ class Meta: fields = '__all__' # meta info for converting to FHIR fhir_datatype_plural = 'conditions' - class_converter = fhir_condition + class_converter = fhir_utils.fhir_condition class BiosampleSerializer(GenericSerializer): @@ -131,7 +163,7 @@ class Meta: fields = '__all__' # meta info for converting to FHIR fhir_datatype_plural = 'specimens' - class_converter = fhir_specimen + class_converter = fhir_utils.fhir_specimen def create(self, validated_data): procedure_data = validated_data.pop('procedure') @@ -163,7 +195,7 @@ class Meta: fields = '__all__' # meta info for converting to FHIR fhir_datatype_plural = 'compositions' - class_converter = fhir_composition + class_converter = fhir_utils.fhir_composition def to_representation(self, instance): """" diff --git a/chord_metadata_service/phenopackets/tests/test_api.py b/chord_metadata_service/phenopackets/tests/test_api.py index cdd2e4451..e17298021 100644 --- a/chord_metadata_service/phenopackets/tests/test_api.py +++ b/chord_metadata_service/phenopackets/tests/test_api.py @@ -1,6 +1,7 @@ from rest_framework import status from rest_framework.test import APITestCase from .constants import * +from ..models import * from ..serializers import * from chord_metadata_service.restapi.tests.utils import get_response @@ -74,7 +75,7 @@ class CreatePhenotypicFeatureTest(APITestCase): def setUp(self): valid_payload = valid_phenotypic_feature() - removed_pftype = valid_payload.pop('pftype', None) + valid_payload.pop('pftype', None) valid_payload['type'] = { "id": "HP:0000520", "label": "Proptosis" diff --git a/chord_metadata_service/phenopackets/tests/test_models.py b/chord_metadata_service/phenopackets/tests/test_models.py index 4032c5d2f..6f1fc36e5 100644 --- a/chord_metadata_service/phenopackets/tests/test_models.py +++ b/chord_metadata_service/phenopackets/tests/test_models.py @@ -1,8 +1,6 @@ +from django.db.utils import IntegrityError from django.test import TestCase from ..models import * -from chord_metadata_service.patients.models import Individual -from django.db.utils import IntegrityError -from django.core.exceptions import ValidationError from .constants import * From e3fe926b34b660cb14ed141f5e9c279a8031f340 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 4 May 2020 11:23:18 -0400 Subject: [PATCH 004/190] Start refactoring search schemas to be separate Deduplicate and move around shared schemas --- .../chord/tests/test_api_search.py | 9 +- .../chord/tests/test_search.py | 5 +- chord_metadata_service/chord/views_search.py | 16 +- .../phenopackets/descriptions.py | 14 - chord_metadata_service/phenopackets/models.py | 27 +- .../phenopackets/schemas.py | 402 +++------------ .../phenopackets/search_schemas.py | 478 ++++++++++++++++++ .../restapi/descriptions.py | 20 + .../restapi/schema_utils.py | 38 ++ chord_metadata_service/restapi/schemas.py | 146 ++---- chord_metadata_service/restapi/validators.py | 4 +- 11 files changed, 678 insertions(+), 481 deletions(-) create mode 100644 chord_metadata_service/phenopackets/search_schemas.py create mode 100644 chord_metadata_service/restapi/descriptions.py create mode 100644 chord_metadata_service/restapi/schema_utils.py diff --git a/chord_metadata_service/chord/tests/test_api_search.py b/chord_metadata_service/chord/tests/test_api_search.py index 24ffb6d49..47bb44db1 100644 --- a/chord_metadata_service/chord/tests/test_api_search.py +++ b/chord_metadata_service/chord/tests/test_api_search.py @@ -10,11 +10,12 @@ from chord_metadata_service.phenopackets.tests.constants import * from chord_metadata_service.phenopackets.models import * +from chord_metadata_service.phenopackets.search_schemas import PHENOPACKET_SEARCH_SCHEMA from chord_metadata_service.chord.tests.es_mocks import SEARCH_SUCCESS from .constants import * from ..models import * -from ..views_search import PHENOPACKET_DATA_TYPE_ID, PHENOPACKET_SCHEMA, PHENOPACKET_METADATA_SCHEMA +from ..views_search import PHENOPACKET_DATA_TYPE_ID, PHENOPACKET_METADATA_SCHEMA class DataTypeTest(APITestCase): @@ -31,7 +32,7 @@ def test_data_type_detail(self): c = r.json() self.assertDictEqual(c, { "id": PHENOPACKET_DATA_TYPE_ID, - "schema": PHENOPACKET_SCHEMA, + "schema": PHENOPACKET_SEARCH_SCHEMA, "metadata_schema": PHENOPACKET_METADATA_SCHEMA }) @@ -39,7 +40,7 @@ def test_data_type_schema(self): r = self.client.get(reverse("data-type-schema")) # Only mounted with phenopacket right now self.assertEqual(r.status_code, status.HTTP_200_OK) c = r.json() - self.assertDictEqual(c, PHENOPACKET_SCHEMA) + self.assertDictEqual(c, PHENOPACKET_SEARCH_SCHEMA) def test_data_type_metadata_schema(self): r = self.client.get(reverse("data-type-metadata-schema")) # Only mounted with phenopacket right now @@ -60,7 +61,7 @@ def dataset_rep(dataset, created, updated): "created": created, "updated": updated }, - "schema": PHENOPACKET_SCHEMA + "schema": PHENOPACKET_SEARCH_SCHEMA } @override_settings(AUTH_OVERRIDE=True) # For permissions diff --git a/chord_metadata_service/chord/tests/test_search.py b/chord_metadata_service/chord/tests/test_search.py index 76ef26715..96f5137c6 100644 --- a/chord_metadata_service/chord/tests/test_search.py +++ b/chord_metadata_service/chord/tests/test_search.py @@ -1,13 +1,14 @@ from django.test import TestCase from jsonschema import Draft7Validator -from ..views_search import PHENOPACKET_SCHEMA, PHENOPACKET_METADATA_SCHEMA +from chord_metadata_service.phenopackets.search_schemas import PHENOPACKET_SEARCH_SCHEMA +from ..views_search import PHENOPACKET_METADATA_SCHEMA class SchemaTest(TestCase): @staticmethod def test_phenopacket_schema(): - Draft7Validator.check_schema(PHENOPACKET_SCHEMA) + Draft7Validator.check_schema(PHENOPACKET_SEARCH_SCHEMA) @staticmethod def test_phenopacket_metadata_schema(): diff --git a/chord_metadata_service/chord/views_search.py b/chord_metadata_service/chord/views_search.py index 1e5a89558..bede263dd 100644 --- a/chord_metadata_service/chord/views_search.py +++ b/chord_metadata_service/chord/views_search.py @@ -15,7 +15,7 @@ from chord_metadata_service.patients.models import Individual from chord_metadata_service.phenopackets.api_views import PHENOPACKET_PREFETCH from chord_metadata_service.phenopackets.models import Phenopacket -from chord_metadata_service.phenopackets.schemas import PHENOPACKET_SCHEMA +from chord_metadata_service.phenopackets.search_schemas import PHENOPACKET_SEARCH_SCHEMA from chord_metadata_service.phenopackets.serializers import PhenopacketSerializer from chord_metadata_service.metadata.elastic import es @@ -33,7 +33,7 @@ @api_view(["GET"]) @permission_classes([AllowAny]) def data_type_list(_request): - return Response([{"id": PHENOPACKET_DATA_TYPE_ID, "schema": PHENOPACKET_SCHEMA}]) + return Response([{"id": PHENOPACKET_DATA_TYPE_ID, "schema": PHENOPACKET_SEARCH_SCHEMA}]) @api_view(["GET"]) @@ -41,7 +41,7 @@ def data_type_list(_request): def data_type_phenopacket(_request): return Response({ "id": PHENOPACKET_DATA_TYPE_ID, - "schema": PHENOPACKET_SCHEMA, + "schema": PHENOPACKET_SEARCH_SCHEMA, "metadata_schema": PHENOPACKET_METADATA_SCHEMA }) @@ -49,7 +49,7 @@ def data_type_phenopacket(_request): @api_view(["GET"]) @permission_classes([AllowAny]) def data_type_phenopacket_schema(_request): - return Response(PHENOPACKET_SCHEMA) + return Response(PHENOPACKET_SEARCH_SCHEMA) @api_view(["GET"]) @@ -74,7 +74,7 @@ def table_list(request): "created": d.created.isoformat(), "updated": d.updated.isoformat() }, - "schema": PHENOPACKET_SCHEMA + "schema": PHENOPACKET_SEARCH_SCHEMA } for d in Dataset.objects.all()]) @@ -216,7 +216,8 @@ def search(request, internal_data=False): ) try: - compiled_query, params = postgres.search_query_to_psycopg2_sql(request.data["query"], PHENOPACKET_SCHEMA) + compiled_query, params = postgres.search_query_to_psycopg2_sql(request.data["query"], + PHENOPACKET_SEARCH_SCHEMA) except (SyntaxError, TypeError, ValueError) as e: return Response(errors.bad_request_error(f"Error compiling query (message: {str(e)})"), status=400) @@ -361,7 +362,8 @@ def chord_table_search(request, table_id, internal=False): dataset = Dataset.objects.get(identifier=table_id) try: - compiled_query, params = postgres.search_query_to_psycopg2_sql(request.data["query"], PHENOPACKET_SCHEMA) + compiled_query, params = postgres.search_query_to_psycopg2_sql(request.data["query"], + PHENOPACKET_SEARCH_SCHEMA) except (SyntaxError, TypeError, ValueError) as e: print("[CHORD Metadata] Error encountered compiling query {}:\n {}".format(request.data["query"], str(e))) return Response(errors.bad_request_error(f"Error compiling query (message: {str(e)})"), status=400) diff --git a/chord_metadata_service/phenopackets/descriptions.py b/chord_metadata_service/phenopackets/descriptions.py index 63b4f92e1..2ab3485c6 100644 --- a/chord_metadata_service/phenopackets/descriptions.py +++ b/chord_metadata_service/phenopackets/descriptions.py @@ -225,20 +225,6 @@ def phenotypic_feature(subject="a subject or biosample"): } } -AGE = { - "description": "An ISO8601 duration string (e.g. P40Y10M05D for 40 years, 10 months, 5 days) representing an age " - "of a subject.", - "help": "Age of a subject." -} - -AGE_RANGE = "Age range (e.g. when a subject's age falls into a bin)" # TODO - -AGE_NESTED = { - "description": AGE["description"], - "properties": { - "age": AGE - } -} BIOSAMPLE = { "description": ("A unit of biological material from which the substrate molecules (e.g. genomic DNA, RNA, " diff --git a/chord_metadata_service/phenopackets/models.py b/chord_metadata_service/phenopackets/models.py index ea6efb1ae..25521bf72 100644 --- a/chord_metadata_service/phenopackets/models.py +++ b/chord_metadata_service/phenopackets/models.py @@ -5,16 +5,19 @@ from chord_metadata_service.patients.models import Individual from chord_metadata_service.restapi.description_utils import rec_help from chord_metadata_service.restapi.models import IndexableMixin -from chord_metadata_service.restapi.validators import JsonSchemaValidator -from chord_metadata_service.restapi.schemas import ( - UPDATE_SCHEMA, - EXTERNAL_REFERENCE, - EVIDENCE, +from chord_metadata_service.restapi.validators import ( + JsonSchemaValidator, + age_or_age_range_validator, + ontology_validator, +) +from . import descriptions as d +from .schemas import ( ALLELE_SCHEMA, - DISEASE_ONSET, + PHENOPACKET_DISEASE_ONSET_SCHEMA, + PHENOPACKET_EVIDENCE_SCHEMA, + PHENOPACKET_EXTERNAL_REFERENCE_SCHEMA, + PHENOPACKET_UPDATE_SCHEMA, ) -import chord_metadata_service.phenopackets.descriptions as d -from chord_metadata_service.restapi.validators import ontology_validator, age_or_age_range_validator ############################################################# @@ -61,12 +64,12 @@ class MetaData(models.Model): resources = models.ManyToManyField(Resource, help_text=rec_help(d.META_DATA, "resources")) updates = ArrayField( JSONField(null=True, blank=True, - validators=[JsonSchemaValidator(schema=UPDATE_SCHEMA, formats=['date-time'])]), + validators=[JsonSchemaValidator(schema=PHENOPACKET_UPDATE_SCHEMA, formats=['date-time'])]), blank=True, null=True, help_text=rec_help(d.META_DATA, "updates")) phenopacket_schema_version = models.CharField(max_length=200, blank=True, help_text='Schema version of the current phenopacket.') external_references = ArrayField( - JSONField(null=True, blank=True, validators=[JsonSchemaValidator(EXTERNAL_REFERENCE)]), + JSONField(null=True, blank=True, validators=[JsonSchemaValidator(PHENOPACKET_EXTERNAL_REFERENCE_SCHEMA)]), blank=True, null=True, help_text=rec_help(d.META_DATA, "external_references")) extra_properties = JSONField(blank=True, null=True, help_text=rec_help(d.META_DATA, "extra_properties")) updated = models.DateTimeField(auto_now_add=True) @@ -106,7 +109,7 @@ class PhenotypicFeature(models.Model, IndexableMixin): # evidence can stay here because evidence is given for an observation of PF # JSON schema to check evidence_code is present # FHIR: Condition.evidence - evidence = JSONField(blank=True, null=True, validators=[JsonSchemaValidator(schema=EVIDENCE)], + evidence = JSONField(blank=True, null=True, validators=[JsonSchemaValidator(schema=PHENOPACKET_EVIDENCE_SCHEMA)], help_text=rec_help(d.PHENOTYPIC_FEATURE, "evidence")) biosample = models.ForeignKey( "Biosample", on_delete=models.SET_NULL, blank=True, null=True, related_name='phenotypic_features') @@ -241,7 +244,7 @@ class Disease(models.Model, IndexableMixin): # "id": "HP:0003581", # "label": "Adult onset" # } - onset = JSONField(blank=True, null=True, validators=[JsonSchemaValidator(schema=DISEASE_ONSET)], + onset = JSONField(blank=True, null=True, validators=[JsonSchemaValidator(schema=PHENOPACKET_DISEASE_ONSET_SCHEMA)], help_text=rec_help(d.DISEASE, "onset")) disease_stage = ArrayField(JSONField(null=True, blank=True, validators=[ontology_validator]), blank=True, null=True, help_text=rec_help(d.DISEASE, "disease_stage")) diff --git a/chord_metadata_service/phenopackets/schemas.py b/chord_metadata_service/phenopackets/schemas.py index 46610d4fe..a6182c34d 100644 --- a/chord_metadata_service/phenopackets/schemas.py +++ b/chord_metadata_service/phenopackets/schemas.py @@ -1,14 +1,34 @@ # Individual schemas for validation of JSONField values import chord_metadata_service.phenopackets.descriptions as descriptions -from chord_metadata_service.phenopackets.models import * from chord_metadata_service.patients.descriptions import INDIVIDUAL -from chord_metadata_service.patients.models import Individual -from chord_metadata_service.restapi.description_utils import describe_schema, ONTOLOGY_CLASS +from chord_metadata_service.restapi.description_utils import describe_schema, ONTOLOGY_CLASS as ONTOLOGY_CLASS_DESC +from chord_metadata_service.restapi.schemas import AGE, AGE_RANGE, AGE_OR_AGE_RANGE, ONTOLOGY_CLASS + + +__all__ = [ + "ALLELE_SCHEMA", + "PHENOPACKET_ONTOLOGY_SCHEMA", + "PHENOPACKET_EXTERNAL_REFERENCE_SCHEMA", + "PHENOPACKET_INDIVIDUAL_SCHEMA", + "PHENOPACKET_RESOURCE_SCHEMA", + "PHENOPACKET_UPDATE_SCHEMA", + "PHENOPACKET_META_DATA_SCHEMA", + "PHENOPACKET_EVIDENCE_SCHEMA", + "PHENOPACKET_PHENOTYPIC_FEATURE_SCHEMA", + "PHENOPACKET_GENE_SCHEMA", + "PHENOPACKET_HTS_FILE_SCHEMA", + "PHENOPACKET_VARIANT_SCHEMA", + "PHENOPACKET_BIOSAMPLE_SCHEMA", + "PHENOPACKET_DISEASE_ONSET_SCHEMA", + "PHENOPACKET_DISEASE_SCHEMA", + "PHENOPACKET_SCHEMA", +] + ALLELE_SCHEMA = { "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "todo", + "$id": "chord_metadata_service:allele_schema", "title": "Allele schema", "description": "Variant allele types", "type": "object", @@ -31,6 +51,7 @@ "iscn": {"type": "string"} }, + "additionalProperties": False, "oneOf": [ {"required": ["hgvs"]}, {"required": ["genome_assembly"]}, @@ -41,148 +62,52 @@ "genome_assembly": ["chr", "pos", "re", "alt", "info"], "seq_id": ["position", "deleted_sequence", "inserted_sequence"] } -} - - -UPDATE_SCHEMA = describe_schema({ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "todo", - "title": "Updates schema", - "description": "Schema to check incoming updates format", - "type": "object", - "properties": { - "timestamp": { - "type": "string", - "format": "date-time", - }, - "updated_by": {"type": "string"}, - "comment": {"type": "string"} - }, - "required": ["timestamp", "comment"] -}, descriptions.UPDATE) - - -def _single_optional_eq_search(order, queryable: str = "all"): - return { - "operations": ["eq"], - "queryable": queryable, - "canNegate": True, - "required": False, - "type": "single", - "order": order - } - - -def _optional_str_search(order, queryable: str = "all"): - return { - "operations": ["eq", "co"], - "queryable": queryable, - "canNegate": True, - "required": False, - "order": order - } - - -def _single_optional_str_search(order, queryable: str = "all"): - return {**_optional_str_search(order, queryable), "type": "single"} - - -def _multiple_optional_str_search(order, queryable: str = "all"): - return {**_optional_str_search(order, queryable), "type": "multiple"} - - -def _tag_with_database_attrs(schema: dict, db_attrs: dict): - return { - **schema, - "search": { - **schema.get("search", {}), - "database": { - **schema.get("search", {}).get("database", {}), - **db_attrs - } - } - } +} # TODO: Descriptions -PHENOPACKET_ONTOLOGY_SCHEMA = describe_schema({ - "type": "object", - "properties": { - "id": { - "type": "string", - "search": _multiple_optional_str_search(0) - }, - "label": { - "type": "string", - "search": _multiple_optional_str_search(1) - }, - }, - "required": ["id", "label"], - "search": { - "database": { - "type": "jsonb" # TODO: parameterize? - } - } -}, ONTOLOGY_CLASS) +PHENOPACKET_ONTOLOGY_SCHEMA = describe_schema(ONTOLOGY_CLASS, ONTOLOGY_CLASS_DESC) PHENOPACKET_EXTERNAL_REFERENCE_SCHEMA = describe_schema({ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "chord_metadata_service:external_reference_schema", + "title": "External reference schema", "type": "object", "properties": { "id": { "type": "string", - "search": _single_optional_str_search(0) }, "description": { "type": "string", - "search": _multiple_optional_str_search(1) # TODO: Searchable? may leak } }, - "required": ["id"], - "search": { - "database": { - "type": "jsonb" # TODO: parameterize? - } - } + "required": ["id"] }, descriptions.EXTERNAL_REFERENCE) PHENOPACKET_INDIVIDUAL_SCHEMA = describe_schema({ + "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { "id": { "type": "string", "description": "Unique researcher-specified identifier for the individual.", - "search": { - **_single_optional_eq_search(0, queryable="internal"), - "database": { - "field": Individual._meta.pk.column - } - } }, "alternate_ids": { "type": "array", "items": { "type": "string", - "search": _multiple_optional_str_search(0, queryable="internal") }, "description": "A list of alternative identifiers for the individual.", # TODO: More specific - "search": { - "database": { - "type": "array" - } - } }, "date_of_birth": { # TODO: This is a special ISO format... need UI for this - # TODO: Internal? "type": "string", - "search": _single_optional_eq_search(1, queryable="internal") }, - # TODO: Age + "age": AGE_OR_AGE_RANGE, "sex": { "type": "string", "enum": ["UNKNOWN_SEX", "FEMALE", "MALE", "OTHER_SEX"], "description": "An individual's phenotypic sex.", - "search": _single_optional_eq_search(2) }, "karyotypic_sex": { "type": "string", @@ -200,118 +125,83 @@ def _tag_with_database_attrs(schema: dict, db_attrs: dict): "OTHER_KARYOTYPE" ], "description": "An individual's karyotypic sex.", - "search": _single_optional_eq_search(3) }, "taxonomy": PHENOPACKET_ONTOLOGY_SCHEMA, }, - "search": { - "database": { - "relation": Individual._meta.db_table, - "primary_key": Individual._meta.pk.column, - } - }, "required": ["id"] }, INDIVIDUAL) PHENOPACKET_RESOURCE_SCHEMA = describe_schema({ + "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", # TODO "properties": { "id": { "type": "string", - "search": _single_optional_str_search(0) }, "name": { "type": "string", - "search": _multiple_optional_str_search(1) }, "namespace_prefix": { "type": "string", - "search": _multiple_optional_str_search(2) }, "url": { "type": "string", - "search": _multiple_optional_str_search(3) }, "version": { "type": "string", - "search": _multiple_optional_str_search(4) }, "iri_prefix": { "type": "string", - "search": _multiple_optional_str_search(5) } }, "required": ["id", "name", "namespace_prefix", "url", "version", "iri_prefix"], - "search": { - "database": { - "relationship": { - "type": "MANY_TO_ONE", - "foreign_key": "resource_id" # TODO: No hard-code, from M2M - } - } - } }, descriptions.RESOURCE) PHENOPACKET_UPDATE_SCHEMA = describe_schema({ - "type": "object", # TODO + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "chord_metadata_service:update_schema", + "title": "Updates schema", + "type": "object", "properties": { "timestamp": { "type": "string", + "format": "date-time" }, "updated_by": { "type": "string", - "search": _multiple_optional_str_search(0), }, "comment": { "type": "string", - "search": _multiple_optional_str_search(1) } }, + "additionalProperties": False, "required": ["timestamp", "comment"], - "search": { - "database": { - "type": "jsonb" - } - } }, descriptions.UPDATE) # noinspection PyProtectedMember PHENOPACKET_META_DATA_SCHEMA = describe_schema({ + "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { - "created": {"type": "string"}, + "created": { + "type": "string", + "format": "date-time" + }, "created_by": { "type": "string", - "search": _multiple_optional_str_search(0) }, "submitted_by": { "type": "string", - "search": _multiple_optional_str_search(1) }, "resources": { "type": "array", "items": PHENOPACKET_RESOURCE_SCHEMA, - "search": { - "database": { - "relation": MetaData._meta.get_field("resources").remote_field.through._meta.db_table, - "relationship": { - "type": "ONE_TO_MANY", - "parent_foreign_key": "metadata_id", # TODO: No hard-code - "parent_primary_key": MetaData._meta.pk.column # TODO: Redundant? - } - } - } }, "updates": { "type": "array", "items": PHENOPACKET_UPDATE_SCHEMA, - "search": { - "database": { - "type": "array" - } - } }, "phenopacket_schema_version": { "type": "string", @@ -321,26 +211,19 @@ def _tag_with_database_attrs(schema: dict, db_attrs: dict): "items": PHENOPACKET_EXTERNAL_REFERENCE_SCHEMA } }, - "search": { - "database": { - "relation": MetaData._meta.db_table, - "primary_key": MetaData._meta.pk.column - } - } }, descriptions.META_DATA) PHENOPACKET_EVIDENCE_SCHEMA = describe_schema({ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "chord_metadata_service:evidence_schema", + "title": "Evidence schema", "type": "object", "properties": { "evidence_code": PHENOPACKET_ONTOLOGY_SCHEMA, "reference": PHENOPACKET_EXTERNAL_REFERENCE_SCHEMA }, + "additionalProperties": False, "required": ["evidence_code"], - "search": { - "database": { - "type": "jsonb" - } - } }, descriptions.EVIDENCE) PHENOPACKET_PHENOTYPIC_FEATURE_SCHEMA = describe_schema({ @@ -348,39 +231,20 @@ def _tag_with_database_attrs(schema: dict, db_attrs: dict): "properties": { "description": { "type": "string", - "search": _multiple_optional_str_search(0), # TODO: Searchable? may leak }, "type": PHENOPACKET_ONTOLOGY_SCHEMA, "negated": { "type": "boolean", - "search": _single_optional_eq_search(1) }, "severity": PHENOPACKET_ONTOLOGY_SCHEMA, "modifier": { # TODO: Plural? "type": "array", - "items": PHENOPACKET_ONTOLOGY_SCHEMA }, "onset": PHENOPACKET_ONTOLOGY_SCHEMA, "evidence": PHENOPACKET_EVIDENCE_SCHEMA, }, - "search": { - "database": { - "relation": PhenotypicFeature._meta.db_table, - "primary_key": PhenotypicFeature._meta.pk.column - } - } }, descriptions.PHENOTYPIC_FEATURE) -PHENOPACKET_AGE_SCHEMA = describe_schema({ - "type": "object", - "properties": { - "age": { - "type": "string", - } - }, - "required": ["age"] -}, descriptions.AGE_NESTED) - # TODO: search PHENOPACKET_GENE_SCHEMA = describe_schema({ @@ -388,18 +252,15 @@ def _tag_with_database_attrs(schema: dict, db_attrs: dict): "properties": { "id": { "type": "string", - "search": _single_optional_str_search(0) }, "alternate_ids": { "type": "array", "items": { "type": "string", - "search": _single_optional_str_search(1) } }, "symbol": { "type": "string", - "search": _single_optional_str_search(2) } }, "required": ["id", "symbol"] @@ -407,7 +268,6 @@ def _tag_with_database_attrs(schema: dict, db_attrs: dict): PHENOPACKET_HTS_FILE_SCHEMA = describe_schema({ - # TODO: Search? Probably not "type": "object", "properties": { "uri": { @@ -445,61 +305,26 @@ def _tag_with_database_attrs(schema: dict, db_attrs: dict): "properties": { "id": { "type": "string", - "search": { - **_single_optional_eq_search(0, queryable="internal"), - "database": {"field": Biosample._meta.pk.column} - } }, "individual_id": { "type": "string", }, "description": { "type": "string", - "search": _multiple_optional_str_search(1), # TODO: Searchable? may leak }, "sampled_tissue": PHENOPACKET_ONTOLOGY_SCHEMA, "phenotypic_features": { "type": "array", "items": PHENOPACKET_PHENOTYPIC_FEATURE_SCHEMA, - "search": { - "database": { - **PHENOPACKET_PHENOTYPIC_FEATURE_SCHEMA["search"]["database"], - "relationship": { - "type": "ONE_TO_MANY", - "parent_foreign_key": PhenotypicFeature._meta.get_field("biosample").column, - "parent_primary_key": Biosample._meta.pk.column # TODO: Redundant - } - } - } }, "taxonomy": PHENOPACKET_ONTOLOGY_SCHEMA, - "individual_age_at_collection": { - "type": "object", - "oneOf": [ # TODO: Front end will need to deal with this - { - "properties": PHENOPACKET_AGE_SCHEMA["properties"], - "description": PHENOPACKET_AGE_SCHEMA["description"], - "required": ["age"], - "additionalProperties": False - }, - { - "properties": { - "start": PHENOPACKET_AGE_SCHEMA, - "end": PHENOPACKET_AGE_SCHEMA, - }, - "description": d.AGE_RANGE, # TODO: annotated - "required": ["start", "end"], - "additionalProperties": False - } - ] - }, + "individual_age_at_collection": AGE_OR_AGE_RANGE, "histological_diagnosis": PHENOPACKET_ONTOLOGY_SCHEMA, "tumor_progression": PHENOPACKET_ONTOLOGY_SCHEMA, "tumor_grade": PHENOPACKET_ONTOLOGY_SCHEMA, # TODO: Is this a list? "diagnostic_markers": { "type": "array", "items": PHENOPACKET_ONTOLOGY_SCHEMA, - "search": {"database": {"type": "array"}} }, "procedure": { "type": "object", @@ -508,121 +333,76 @@ def _tag_with_database_attrs(schema: dict, db_attrs: dict): "body_site": PHENOPACKET_ONTOLOGY_SCHEMA }, "required": ["code"], - "search": { - "database": { - "primary_key": Procedure._meta.pk.column, - "relation": Procedure._meta.db_table, - "relationship": { - "type": "MANY_TO_ONE", - "foreign_key": Biosample._meta.get_field("procedure").column - } - } - } }, "hts_files": { "type": "array", - "items": PHENOPACKET_HTS_FILE_SCHEMA # TODO + "items": PHENOPACKET_HTS_FILE_SCHEMA }, "variants": { "type": "array", - "items": PHENOPACKET_VARIANT_SCHEMA, # TODO: search? + "items": PHENOPACKET_VARIANT_SCHEMA }, "is_control_sample": { - "type": "boolean", # TODO: Boolean search - "search": _single_optional_eq_search(1), + "type": "boolean" }, }, "required": ["id", "sampled_tissue", "procedure"], - "search": { - "database": { - "primary_key": Biosample._meta.pk.column, - "relation": Biosample._meta.db_table, - } - } }, descriptions.BIOSAMPLE) + +PHENOPACKET_DISEASE_ONSET_SCHEMA = { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "chord_metadata_service:disease_onset_schema", + "title": "Onset age", + "description": "Schema for the age of the onset of the disease.", + "type": "object", + "anyOf": [ + AGE, + AGE_RANGE, + PHENOPACKET_ONTOLOGY_SCHEMA + ] +} + PHENOPACKET_DISEASE_SCHEMA = describe_schema({ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "chord_metadata_service:disease_schema", + "title": "Disease schema", "type": "object", "properties": { "term": PHENOPACKET_ONTOLOGY_SCHEMA, - "onset": PHENOPACKET_AGE_SCHEMA, + "onset": PHENOPACKET_DISEASE_ONSET_SCHEMA, "disease_stage": { "type": "array", "items": PHENOPACKET_ONTOLOGY_SCHEMA, - "search": {"database": {"type": "array"}} }, "tnm_finding": { "type": "array", "items": PHENOPACKET_ONTOLOGY_SCHEMA, - "search": {"database": {"type": "array"}} }, }, "required": ["term"], - "search": { - "database": { - "primary_key": Disease._meta.pk.column, - "relation": Disease._meta.db_table, - } - } }, descriptions.DISEASE) # Deduplicate with other phenopacket representations # noinspection PyProtectedMember PHENOPACKET_SCHEMA = describe_schema({ "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "TODO", - "title": "Dataset Table Schema", + "$id": "chord_metadata_service:phenopacket_schema", + "title": "Phenopacket schema", "description": "Schema for metadata service datasets", "type": "object", "properties": { "id": { "type": "string", - "search": {"database": {"field": Phenopacket._meta.pk.column}} }, - "subject": _tag_with_database_attrs(PHENOPACKET_INDIVIDUAL_SCHEMA, { - "relationship": { - "type": "MANY_TO_ONE", - "foreign_key": Phenopacket._meta.get_field("subject").column - } - }), + "subject": PHENOPACKET_INDIVIDUAL_SCHEMA, "phenotypic_features": { "type": "array", - "items": PHENOPACKET_PHENOTYPIC_FEATURE_SCHEMA, - "search": { - "database": { - **PHENOPACKET_PHENOTYPIC_FEATURE_SCHEMA["search"]["database"], - "relationship": { - "type": "ONE_TO_MANY", - "parent_foreign_key": "phenopacket_id", # TODO: No hard-code - "parent_primary_key": Phenopacket._meta.pk.column # TODO: Redundant? - } - } - } + "items": PHENOPACKET_PHENOTYPIC_FEATURE_SCHEMA }, "biosamples": { "type": "array", - "items": { - **PHENOPACKET_BIOSAMPLE_SCHEMA, - "search": { - "database": { - **PHENOPACKET_BIOSAMPLE_SCHEMA["search"]["database"], - "relationship": { - "type": "MANY_TO_ONE", - "foreign_key": "biosample_id" # TODO: No hard-code, from M2M - } - } - } - }, - "search": { - "database": { - "relation": Phenopacket._meta.get_field("biosamples").remote_field.through._meta.db_table, - "relationship": { - "type": "ONE_TO_MANY", - "parent_foreign_key": "phenopacket_id", # TODO: No hard-code - "parent_primary_key": Phenopacket._meta.pk.column # TODO: Redundant? - } - } - } + "items": PHENOPACKET_BIOSAMPLE_SCHEMA }, "genes": { "type": "array", @@ -634,29 +414,7 @@ def _tag_with_database_attrs(schema: dict, db_attrs: dict): }, "diseases": { # TODO: Too sensitive for search? "type": "array", - "items": { - **PHENOPACKET_DISEASE_SCHEMA, - "search": { - **PHENOPACKET_DISEASE_SCHEMA["search"], - "database": { - **PHENOPACKET_DISEASE_SCHEMA["search"]["database"], - "relationship": { - "type": "MANY_TO_ONE", - "foreign_key": "disease_id" # TODO: No hard-code, from M2M - } - } - } - }, - "search": { - "database": { - "relation": Phenopacket._meta.get_field("diseases").remote_field.through._meta.db_table, - "relationship": { - "type": "ONE_TO_MANY", - "parent_foreign_key": "phenopacket_id", # TODO: No hard-code - "parent_primary_key": Phenopacket._meta.pk.column # TODO: Redundant? - } - } - } + "items": PHENOPACKET_DISEASE_SCHEMA, }, # TODO "hts_files": { "type": "array", @@ -665,10 +423,4 @@ def _tag_with_database_attrs(schema: dict, db_attrs: dict): "meta_data": PHENOPACKET_META_DATA_SCHEMA }, "required": ["id", "meta_data"], - "search": { - "database": { - "relation": Phenopacket._meta.db_table, - "primary_key": Phenopacket._meta.pk.column - } - } }, descriptions.PHENOPACKET) diff --git a/chord_metadata_service/phenopackets/search_schemas.py b/chord_metadata_service/phenopackets/search_schemas.py new file mode 100644 index 000000000..c04381d3a --- /dev/null +++ b/chord_metadata_service/phenopackets/search_schemas.py @@ -0,0 +1,478 @@ +from . import models, schemas +from chord_metadata_service.restapi.schema_utils import tag_schema_with_search_properties + + +__all__ = [ + "ONTOLOGY_SEARCH_SCHEMA", + "EXTERNAL_REFERENCE_SEARCH_SCHEMA", + "PHENOPACKET_SEARCH_SCHEMA", +] + + +def _single_optional_eq_search(order, queryable: str = "all"): + return { + "operations": ["eq"], + "queryable": queryable, + "canNegate": True, + "required": False, + "type": "single", + "order": order + } + + +def _optional_str_search(order, queryable: str = "all"): + return { + "operations": ["eq", "co"], + "queryable": queryable, + "canNegate": True, + "required": False, + "order": order + } + + +def _single_optional_str_search(order, queryable: str = "all"): + return {**_optional_str_search(order, queryable), "type": "single"} + + +def _multiple_optional_str_search(order, queryable: str = "all"): + return {**_optional_str_search(order, queryable), "type": "multiple"} + + +# TODO: Rewrite and use +def _tag_with_database_attrs(schema: dict, db_attrs: dict): + return { + **schema, + "search": { + **schema.get("search", {}), + "database": { + **schema.get("search", {}).get("database", {}), + **db_attrs + } + } + } + + +ONTOLOGY_SEARCH_SCHEMA = tag_schema_with_search_properties(schemas.PHENOPACKET_ONTOLOGY_SCHEMA, { + "properties": { + "id": { + "search": _multiple_optional_str_search(0) + }, + "label": { + "search": _multiple_optional_str_search(1) + } + }, + "search": { + "database": { + "type": "jsonb" # TODO: parameterize? + } + } +}) + +EXTERNAL_REFERENCE_SEARCH_SCHEMA = tag_schema_with_search_properties(schemas.PHENOPACKET_EXTERNAL_REFERENCE_SCHEMA, { + "properties": { + "id": { + "search": _single_optional_str_search(0) + }, + "description": { + "search": _multiple_optional_str_search(1) # TODO: Searchable? may leak + } + }, + "search": { + "database": { + "type": "jsonb" # TODO: parameterize? + } + } +}) + +INDIVIDUAL_SEARCH_SCHEMA = tag_schema_with_search_properties(schemas.PHENOPACKET_INDIVIDUAL_SCHEMA, { + "properties": { + "id": { + "search": { + **_single_optional_eq_search(0, queryable="internal"), + "database": { + "field": models.Individual._meta.pk.column + } + } + }, + "alternate_ids": { + "items": { + "search": _multiple_optional_str_search(0, queryable="internal") + }, + "search": { + "database": { + "type": "array" + } + } + }, + "date_of_birth": { + # TODO: Internal? + "search": _single_optional_eq_search(1, queryable="internal") + }, + # TODO: Age + "sex": { + "search": _single_optional_eq_search(2) + }, + "karyotypic_sex": { + "search": _single_optional_eq_search(3) + }, + "taxonomy": ONTOLOGY_SEARCH_SCHEMA, + }, + "search": { + "database": { + "relation": models.Individual._meta.db_table, + "primary_key": models.Individual._meta.pk.column, + } + }, +}) + +RESOURCE_SEARCH_SCHEMA = tag_schema_with_search_properties(schemas.PHENOPACKET_RESOURCE_SCHEMA, { + "properties": { + "id": { + "search": _single_optional_str_search(0) + }, + "name": { + "search": _multiple_optional_str_search(1) + }, + "namespace_prefix": { + "search": _multiple_optional_str_search(2) + }, + "url": { + "search": _multiple_optional_str_search(3) + }, + "version": { + "search": _multiple_optional_str_search(4) + }, + "iri_prefix": { + "search": _multiple_optional_str_search(5) + } + }, + "search": { + "database": { + "relationship": { + "type": "MANY_TO_ONE", + "foreign_key": "resource_id" # TODO: No hard-code, from M2M + } + } + } +}) + +UPDATE_SEARCH_SCHEMA = tag_schema_with_search_properties(schemas.PHENOPACKET_UPDATE_SCHEMA, { + "properties": { + # TODO: timestamp + "updated_by": { + "search": _multiple_optional_str_search(0), + }, + "comment": { + "search": _multiple_optional_str_search(1) + } + }, + "search": { + "database": { + "type": "jsonb" + } + } +}) + +# noinspection PyProtectedMember +META_DATA_SEARCH_SCHEMA = tag_schema_with_search_properties(schemas.PHENOPACKET_META_DATA_SCHEMA, { + "properties": { + # TODO: created + "created_by": { + "search": _multiple_optional_str_search(0) + }, + "submitted_by": { + "search": _multiple_optional_str_search(1) + }, + "resources": { + "items": RESOURCE_SEARCH_SCHEMA, + "search": { + "database": { + "relation": models.MetaData._meta.get_field("resources").remote_field.through._meta.db_table, + "relationship": { + "type": "ONE_TO_MANY", + "parent_foreign_key": "metadata_id", # TODO: No hard-code + "parent_primary_key": models.MetaData._meta.pk.column # TODO: Redundant? + } + } + } + }, + "updates": { + "items": UPDATE_SEARCH_SCHEMA, + "search": { + "database": { + "type": "array" + } + } + }, + # TODO: phenopacket_schema_version + "external_references": { + "items": EXTERNAL_REFERENCE_SEARCH_SCHEMA + } + }, + "search": { + "database": { + "relation": models.MetaData._meta.db_table, + "primary_key": models.MetaData._meta.pk.column + } + } +}) + +EVIDENCE_SEARCH_SCHEMA = tag_schema_with_search_properties(schemas.PHENOPACKET_EVIDENCE_SCHEMA, { + "properties": { + "evidence_code": ONTOLOGY_SEARCH_SCHEMA, + "reference": EXTERNAL_REFERENCE_SEARCH_SCHEMA, + }, + "search": { + "database": { + "type": "jsonb" + } + } +}) + +PHENOTYPIC_FEATURE_SEARCH_SCHEMA = tag_schema_with_search_properties(schemas.PHENOPACKET_PHENOTYPIC_FEATURE_SCHEMA, { + "properties": { + "description": { + "search": _multiple_optional_str_search(0), # TODO: Searchable? may leak + }, + "type": ONTOLOGY_SEARCH_SCHEMA, + "negated": { + "search": _single_optional_eq_search(1) + }, + "severity": ONTOLOGY_SEARCH_SCHEMA, + "modifier": { # TODO: Plural? + "items": ONTOLOGY_SEARCH_SCHEMA + }, + "onset": ONTOLOGY_SEARCH_SCHEMA, + "evidence": EVIDENCE_SEARCH_SCHEMA, + }, + "search": { + "database": { + "relation": models.PhenotypicFeature._meta.db_table, + "primary_key": models.PhenotypicFeature._meta.pk.column + } + } +}) + +# TODO: Fix +GENE_SEARCH_SCHEMA = tag_schema_with_search_properties(schemas.PHENOPACKET_GENE_SCHEMA, { + "properties": { + "id": { + "search": _single_optional_str_search(0) + }, + "alternate_ids": { + "items": { + "search": _single_optional_str_search(1) + } + }, + "symbol": { + "search": _single_optional_str_search(2) + } + }, +}) + +# TODO: Search? Probably not +HTS_FILE_SEARCH_SCHEMA = tag_schema_with_search_properties(schemas.PHENOPACKET_HTS_FILE_SCHEMA, {}) + +# TODO: search?? +VARIANT_SEARCH_SCHEMA = tag_schema_with_search_properties(schemas.PHENOPACKET_VARIANT_SCHEMA, { + "properties": { + "allele": {"search": {}}, # TODO + "zygosity": ONTOLOGY_SEARCH_SCHEMA, + } +}) + +BIOSAMPLE_SEARCH_SCHEMA = tag_schema_with_search_properties(schemas.PHENOPACKET_BIOSAMPLE_SCHEMA, { + "properties": { + "id": { + "search": { + **_single_optional_eq_search(0, queryable="internal"), + "database": {"field": models.Biosample._meta.pk.column} + } + }, + "individual_id": { # TODO: Does this work? + "search": _single_optional_eq_search(1, queryable="internal"), + }, + "description": { + "search": _multiple_optional_str_search(2), # TODO: Searchable? may leak + }, + "sampled_tissue": ONTOLOGY_SEARCH_SCHEMA, + "phenotypic_features": { + "items": PHENOTYPIC_FEATURE_SEARCH_SCHEMA, + "search": { + "database": { + **PHENOTYPIC_FEATURE_SEARCH_SCHEMA["search"]["database"], + "relationship": { + "type": "ONE_TO_MANY", + "parent_foreign_key": models.PhenotypicFeature._meta.get_field("biosample").column, + "parent_primary_key": models.Biosample._meta.pk.column # TODO: Redundant + } + } + } + }, + "taxonomy": ONTOLOGY_SEARCH_SCHEMA, + # TODO: Front end will need to deal with this: + # TODO: individual_age_at_collection + "histological_diagnosis": ONTOLOGY_SEARCH_SCHEMA, + "tumor_progression": ONTOLOGY_SEARCH_SCHEMA, + "tumor_grade": ONTOLOGY_SEARCH_SCHEMA, # TODO: Is this a list? + "diagnostic_markers": { + "items": ONTOLOGY_SEARCH_SCHEMA, + "search": {"database": {"type": "array"}} + }, + "procedure": { + "properties": { + "code": ONTOLOGY_SEARCH_SCHEMA, + "body_site": ONTOLOGY_SEARCH_SCHEMA + }, + "search": { + "database": { + "primary_key": models.Procedure._meta.pk.column, + "relation": models.Procedure._meta.db_table, + "relationship": { + "type": "MANY_TO_ONE", + "foreign_key": models.Biosample._meta.get_field("procedure").column + } + } + } + }, + "hts_files": { + "items": HTS_FILE_SEARCH_SCHEMA # TODO + }, + "variants": { + "items": VARIANT_SEARCH_SCHEMA, # TODO: search? + }, + "is_control_sample": { + "search": _single_optional_eq_search(1), # TODO: Boolean search + }, + }, + "search": { + "database": { + "primary_key": models.Biosample._meta.pk.column, + "relation": models.Biosample._meta.db_table, + } + } +}) + +# TODO +DISEASE_ONSET_SEARCH_SCHEMA = tag_schema_with_search_properties(schemas.PHENOPACKET_DISEASE_ONSET_SCHEMA, {}) + +DISEASE_SEARCH_SCHEMA = tag_schema_with_search_properties(schemas.PHENOPACKET_DISEASE_SCHEMA, { + "properties": { + "term": ONTOLOGY_SEARCH_SCHEMA, + "onset": DISEASE_ONSET_SEARCH_SCHEMA, + "disease_stage": { + "items": ONTOLOGY_SEARCH_SCHEMA, + "search": {"database": {"type": "array"}} + }, + "tnm_finding": { + "items": ONTOLOGY_SEARCH_SCHEMA, + "search": {"database": {"type": "array"}} + }, + }, + "search": { + "database": { + "primary_key": models.Disease._meta.pk.column, + "relation": models.Disease._meta.db_table, + } + } +}) + +# noinspection PyProtectedMember +PHENOPACKET_SEARCH_SCHEMA = tag_schema_with_search_properties(schemas.PHENOPACKET_SCHEMA, { + "properties": { + "id": { + "search": {"database": {"field": models.Phenopacket._meta.pk.column}} + }, + "subject": { + **INDIVIDUAL_SEARCH_SCHEMA, + "search": { + **INDIVIDUAL_SEARCH_SCHEMA["search"], + "database": { + **INDIVIDUAL_SEARCH_SCHEMA["search"]["database"], + "relationship": { + "type": "MANY_TO_ONE", + "foreign_key": models.Phenopacket._meta.get_field("subject").column + } + } + } + }, + "phenotypic_features": { + "items": PHENOTYPIC_FEATURE_SEARCH_SCHEMA, + "search": { + "database": { + **PHENOTYPIC_FEATURE_SEARCH_SCHEMA["search"]["database"], + "relationship": { + "type": "ONE_TO_MANY", + "parent_foreign_key": "phenopacket_id", # TODO: No hard-code + "parent_primary_key": models.Phenopacket._meta.pk.column # TODO: Redundant? + } + } + } + }, + "biosamples": { + "items": { + **BIOSAMPLE_SEARCH_SCHEMA, + "search": { + "database": { + **BIOSAMPLE_SEARCH_SCHEMA["search"]["database"], + "relationship": { + "type": "MANY_TO_ONE", + "foreign_key": "biosample_id" # TODO: No hard-code, from M2M + } + } + } + }, + "search": { + "database": { + "relation": models.Phenopacket._meta.get_field("biosamples").remote_field.through._meta.db_table, + "relationship": { + "type": "ONE_TO_MANY", + "parent_foreign_key": "phenopacket_id", # TODO: No hard-code + "parent_primary_key": models.Phenopacket._meta.pk.column # TODO: Redundant? + } + } + } + }, + "genes": { + "items": GENE_SEARCH_SCHEMA + }, + "variants": { + "items": VARIANT_SEARCH_SCHEMA + }, + "diseases": { # TODO: Too sensitive for search? + "items": { + **DISEASE_SEARCH_SCHEMA, + "search": { + **DISEASE_SEARCH_SCHEMA["search"], + "database": { + **DISEASE_SEARCH_SCHEMA["search"]["database"], + "relationship": { + "type": "MANY_TO_ONE", + "foreign_key": "disease_id" # TODO: No hard-code, from M2M + } + } + } + }, + "search": { + "database": { + "relation": models.Phenopacket._meta.get_field("diseases").remote_field.through._meta.db_table, + "relationship": { + "type": "ONE_TO_MANY", + "parent_foreign_key": "phenopacket_id", # TODO: No hard-code + "parent_primary_key": models.Phenopacket._meta.pk.column # TODO: Redundant? + } + } + } + }, # TODO + "hts_files": { + "items": HTS_FILE_SEARCH_SCHEMA # TODO + }, + "meta_data": META_DATA_SEARCH_SCHEMA + }, + "search": { + "database": { + "relation": models.Phenopacket._meta.db_table, + "primary_key": models.Phenopacket._meta.pk.column + } + } +}) diff --git a/chord_metadata_service/restapi/descriptions.py b/chord_metadata_service/restapi/descriptions.py new file mode 100644 index 000000000..c2fd08c15 --- /dev/null +++ b/chord_metadata_service/restapi/descriptions.py @@ -0,0 +1,20 @@ +AGE = { + "description": "An ISO8601 duration string (e.g. P40Y10M05D for 40 years, 10 months, 5 days) representing an age " + "of a subject.", + "help": "Age of a subject." +} + +AGE_RANGE = { + "description": "Age range of a subject (e.g. when a subject's age falls into a bin.)", + "properties": { + "start": "An ISO8601 duration string representing the start of the age range bin.", + "end": "An ISO8601 duration string representing the end of the age range bin." + } +} + +AGE_NESTED = { + "description": AGE["description"], + "properties": { + "age": AGE + } +} diff --git a/chord_metadata_service/restapi/schema_utils.py b/chord_metadata_service/restapi/schema_utils.py new file mode 100644 index 000000000..a6797fc48 --- /dev/null +++ b/chord_metadata_service/restapi/schema_utils.py @@ -0,0 +1,38 @@ +from typing import Optional + + +__all__ = ["tag_schema_with_search_properties"] + + +def tag_schema_with_search_properties(schema, search_descriptions: Optional[dict]): + if not isinstance(schema, dict) or not search_descriptions: + return schema + + if "type" not in schema: + # TODO: handle oneOf, allOf, etc. + return schema + + schema_with_search = { + **schema, + **({"search": search_descriptions["search"]} if "search" in search_descriptions else {}), + } + + if schema["type"] == "object": + return { + **schema_with_search, + **({ + "properties": { + p: tag_schema_with_search_properties(s, search_descriptions["properties"].get(p)) + for p, s in schema["properties"].items() + } + } if "properties" in schema and "properties" in search_descriptions else {}) + } + + if schema["type"] == "array": + return { + **schema_with_search, + **({"items": tag_schema_with_search_properties(schema["items"], search_descriptions["items"])} + if "items" in schema and "items" in search_descriptions else {}) + } + + return schema_with_search diff --git a/chord_metadata_service/restapi/schemas.py b/chord_metadata_service/restapi/schemas.py index 170f4bc28..68bfaecc6 100644 --- a/chord_metadata_service/restapi/schemas.py +++ b/chord_metadata_service/restapi/schemas.py @@ -1,70 +1,32 @@ -# Individual schemas for validation of JSONField values +from . import descriptions +from .description_utils import describe_schema -################################ Phenopackets based schemas ################################ +# Individual schemas for validation of JSONField values -ALLELE_SCHEMA = { - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "chord_metadata_service:allele_schema", - "title": "Allele schema", - "description": "Variant allele types", - "type": "object", - "properties": { - "id": {"type": "string"}, +__all__ = [ + "ONTOLOGY_CLASS", + "ONTOLOGY_CLASS_LIST", + "KEY_VALUE_OBJECT", + "AGE_STRING", + "AGE", + "AGE_RANGE", + "AGE_OR_AGE_RANGE", - "hgvs": {"type": "string"}, + "QUANTITY", + "CODEABLE_CONCEPT", + "PERIOD", + "RATIO", - "genome_assembly": {"type": "string"}, - "chr": {"type": "string"}, - "pos": {"type": "integer"}, - "re": {"type": "string"}, - "alt": {"type": "string"}, - "info": {"type": "string"}, + "TIME_OR_PERIOD", + "COMORBID_CONDITION", + "COMPLEX_ONTOLOGY", + "TUMOR_MARKER_TEST", +] - "seq_id": {"type": "string"}, - "position": {"type": "integer"}, - "deleted_sequence": {"type": "string"}, - "inserted_sequence": {"type": "string"}, - "iscn": {"type": "string"} - }, - "additionalProperties": False, - "oneOf": [ - { - "required": ["hgvs"] - }, - { - "required": ["genome_assembly"] - }, - { - "required": ["seq_id"] - }, - { - "required": ["iscn"] - } +################################ Phenopackets based schemas ################################ - ], - "dependencies": { - "genome_assembly": ["chr", "pos", "re", "alt", "info"], - "seq_id": ["position", "deleted_sequence", "inserted_sequence"] - } -} - -UPDATE_SCHEMA = { - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "chord_metadata_service:update_schema", - "title": "Updates schema", - "description": "Schema to check incoming updates format", - "type": "object", - "properties": { - "timestamp": {"type": "string", "format": "date-time", - "description": "ISO8601 UTC timestamp at which this record was updated."}, - "updated_by": {"type": "string", "description": "Who updated the phenopacket"}, - "comment": {"type": "string", "description": "Comment about updates or reasons for an update."} - }, - "additionalProperties": False, - "required": ["timestamp", "comment"] -} ONTOLOGY_CLASS = { "$schema": "http://json-schema.org/draft-07/schema#", @@ -82,63 +44,17 @@ ONTOLOGY_CLASS_LIST = { "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "ONTOLOGY_CLASS_LIST", + "$id": "chord_metadata_service:ontology_class_list_schema", "title": "Ontology class list", "description": "Ontology class list", "type": "array", "items": ONTOLOGY_CLASS, } -EXTERNAL_REFERENCE = { - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "chord_metadata_service:external_reference_schema", - "title": "External reference schema", - "description": "The schema encodes information about an external reference.", - "type": "object", - "properties": { - "id": {"type": "string", "description": "An application specific identifier."}, - "description": {"type": "string", "description": "An application specific description."} - }, - "additionalProperties": False, - "required": ["id"] -} - -EVIDENCE = { - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "chord_metadata_service:evidence_schema", - "title": "Evidence schema", - "description": "The schema represents the evidence for an assertion such as an observation of a PhenotypicFeature.", - "type": "object", - "properties": { - "evidence_code": { - "type": "object", - "description": "An ontology class that represents the evidence type.", - "properties": { - "id": {"type": "string", "description": "CURIE style identifier."}, - "label": {"type": "string", "description": "Human-readable class name."} - }, - "additionalProperties": False, - "required": ["id", "label"] - }, - "reference": { - "type": "object", - "description": "Representation of the source of the evidence.", - "properties": { - "id": {"type": "string", "description": "An application specific identifier."}, - "description": {"type": "string", "description": "An application specific description."} - }, - "additionalProperties": False, - "required": ["id"] - } - }, - "additionalProperties": False, - "required": ["evidence_code"] -} - KEY_VALUE_OBJECT = { "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "KEY_VALUE_OBJECT", + "$id": "chord_metadata_service:key_value_object_schema", "title": "Key-value object", "description": "The schema represents a key-value object.", "type": "object", @@ -149,34 +65,34 @@ } -AGE_STRING = {"type": "string", "description": "An ISO8601 string represent age."} +AGE_STRING = describe_schema({"type": "string"}, descriptions.AGE) -AGE = { +AGE = describe_schema({ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "chord_metadata_service:age_schema", "title": "Age schema", - "description": "An age of a subject.", "type": "object", "properties": { "age": AGE_STRING }, "additionalProperties": False, "required": ["age"] -} +}, descriptions.AGE_NESTED) + -AGE_RANGE = { +AGE_RANGE = describe_schema({ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "chord_metadata_service:age_range_schema", "title": "Age range schema", - "description": "An age range of a subject.", "type": "object", "properties": { "start": AGE, - "end": AGE + "end": AGE, }, "additionalProperties": False, "required": ["start", "end"] -} +}, descriptions.AGE_RANGE) + AGE_OR_AGE_RANGE = { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/chord_metadata_service/restapi/validators.py b/chord_metadata_service/restapi/validators.py index 175003f42..6e0ae8bc2 100644 --- a/chord_metadata_service/restapi/validators.py +++ b/chord_metadata_service/restapi/validators.py @@ -1,6 +1,7 @@ from rest_framework import serializers from jsonschema import Draft7Validator, FormatChecker from chord_metadata_service.restapi.schemas import ( + AGE_OR_AGE_RANGE, ONTOLOGY_CLASS, QUANTITY, COMPLEX_ONTOLOGY, @@ -8,7 +9,6 @@ TUMOR_MARKER_TEST, ONTOLOGY_CLASS_LIST, KEY_VALUE_OBJECT, - AGE_OR_AGE_RANGE, COMORBID_CONDITION, ) @@ -37,10 +37,10 @@ def deconstruct(self): ) +age_or_age_range_validator = JsonSchemaValidator(AGE_OR_AGE_RANGE) ontology_validator = JsonSchemaValidator(ONTOLOGY_CLASS) ontology_list_validator = JsonSchemaValidator(ONTOLOGY_CLASS_LIST) key_value_validator = JsonSchemaValidator(KEY_VALUE_OBJECT) -age_or_age_range_validator = JsonSchemaValidator(AGE_OR_AGE_RANGE) quantity_validator = JsonSchemaValidator(schema=QUANTITY, formats=['uri']) tumor_marker_test_validator = JsonSchemaValidator(schema=TUMOR_MARKER_TEST) complex_ontology_validator = JsonSchemaValidator(schema=COMPLEX_ONTOLOGY, formats=['uri']) From 45a0ee693548d1a23c3d42fe4dac89f13fd4f9d6 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Mon, 4 May 2020 21:42:21 -0400 Subject: [PATCH 005/190] small fixes to ingest --- chord_metadata_service/chord/views_ingest.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/chord_metadata_service/chord/views_ingest.py b/chord_metadata_service/chord/views_ingest.py index 041741309..139f410e2 100644 --- a/chord_metadata_service/chord/views_ingest.py +++ b/chord_metadata_service/chord/views_ingest.py @@ -153,13 +153,14 @@ def ingest(request): def _query_and_check_nulls(obj: dict, key: str, transform: Callable = lambda x: x): value = obj.get(key, None) - return {f"{key}__isnull": True} if value is None else {key: transform(value)} + return {f"{key}__isnull": True} if value is None or value == "" else {key: transform(value)} def ingest_phenopacket(phenopacket_data, table_id): """ Ingests one phenopacket. """ - new_phenopacket_id = str(uuid.uuid4()) # TODO: Is this provided? + #new_phenopacket_id = str(uuid.uuid4()) # TODO: Is this provided? + new_phenopacket_id = phenopacket_data.get("id", str(uuid.uuid4())) subject = phenopacket_data.get("subject", None) phenotypic_features = phenopacket_data.get("phenotypic_features", []) @@ -222,7 +223,8 @@ def ingest_phenopacket(phenopacket_data, table_id): term=disease["term"], disease_stage=disease.get("disease_stage", []), tnm_finding=disease.get("tnm_finding", []), - **_query_and_check_nulls(disease, "onset") + #**_query_and_check_nulls(disease, "onset") + onset=disease.get("onset", None) ) diseases_db.append(d_obj.id) From 3973e27218fccac13e29240c8460cad40089b0b2 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Tue, 12 May 2020 15:44:55 -0400 Subject: [PATCH 006/190] move schemas and validators to their respective app --- .../experiments/api_views.py | 16 + chord_metadata_service/experiments/schemas.py | 57 +++ chord_metadata_service/mcode/api_views.py | 15 + chord_metadata_service/mcode/models.py | 9 +- chord_metadata_service/mcode/schemas.py | 395 ++++++++++++++++++ chord_metadata_service/mcode/validators.py | 8 + .../restapi/schema_utils.py | 21 +- chord_metadata_service/restapi/schemas.py | 182 +------- chord_metadata_service/restapi/urls.py | 6 +- chord_metadata_service/restapi/validators.py | 12 +- 10 files changed, 532 insertions(+), 189 deletions(-) create mode 100644 chord_metadata_service/experiments/schemas.py create mode 100644 chord_metadata_service/mcode/schemas.py create mode 100644 chord_metadata_service/mcode/validators.py diff --git a/chord_metadata_service/experiments/api_views.py b/chord_metadata_service/experiments/api_views.py index 157980c75..bf71d7e28 100644 --- a/chord_metadata_service/experiments/api_views.py +++ b/chord_metadata_service/experiments/api_views.py @@ -1,7 +1,12 @@ from rest_framework import viewsets from rest_framework.settings import api_settings +from rest_framework.decorators import api_view, permission_classes +from rest_framework.permissions import AllowAny +from rest_framework.response import Response + from .serializers import ExperimentSerializer from .models import Experiment +from .schemas import EXPERIMENT_SCHEMA from chord_metadata_service.restapi.pagination import LargeResultsSetPagination @@ -18,3 +23,14 @@ class ExperimentViewSet(viewsets.ModelViewSet): serializer_class = ExperimentSerializer pagination_class = LargeResultsSetPagination renderer_classes = tuple(api_settings.DEFAULT_RENDERER_CLASSES) + + +@api_view(["GET"]) +@permission_classes([AllowAny]) +def get_experiment_schema(_request): + """ + get: + Experiment schema + """ + return Response(EXPERIMENT_SCHEMA) + diff --git a/chord_metadata_service/experiments/schemas.py b/chord_metadata_service/experiments/schemas.py new file mode 100644 index 000000000..ce3f8c27c --- /dev/null +++ b/chord_metadata_service/experiments/schemas.py @@ -0,0 +1,57 @@ +from .descriptions import EXPERIMENT +from chord_metadata_service.restapi.description_utils import describe_schema +from chord_metadata_service.restapi.schemas import ONTOLOGY_CLASS_LIST, KEY_VALUE_OBJECT + + +EXPERIMENT_SCHEMA = describe_schema({ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "chord_metadata_service:experiment_schema", + "title": "Experiment schema", + "description": "Schema for describing an experiment.", + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "reference_registry_id": { + "type": "string" + }, + "qc_flags": { + "type": "array", + "items": { + "type": "string" + } + }, + "experiment_type": { + "type": "string" + }, + "experiment_ontology": ONTOLOGY_CLASS_LIST, + "molecule": { + "type": "string" + }, + "molecule_ontology": ONTOLOGY_CLASS_LIST, + "library_strategy": { + "type": "string", + "enum": [ + "DNase-Hypersensitivity", + "ATAC-seq", + "NOME-Seq", + "Bisulfite-Seq", + "MeDIP-Seq", + "MRE-Seq", + "ChIP-Seq", + "RNA-Seq", + "miRNA-Seq", + "WGS" + ] + }, + "other_fields": KEY_VALUE_OBJECT, + "biosample": { + "type": "string" + }, + "individual": { + "type": "string" + } + }, + "required": ["id", "experiment_type", "library_strategy"] +}, EXPERIMENT) diff --git a/chord_metadata_service/mcode/api_views.py b/chord_metadata_service/mcode/api_views.py index 63284430d..6c0f38968 100644 --- a/chord_metadata_service/mcode/api_views.py +++ b/chord_metadata_service/mcode/api_views.py @@ -1,5 +1,10 @@ from rest_framework import viewsets from rest_framework.settings import api_settings +from rest_framework.decorators import api_view, permission_classes +from rest_framework.permissions import AllowAny +from rest_framework.response import Response +from .serializers import * +from .schemas import MCODE_SCHEMA from .models import * from .serializers import * from chord_metadata_service.restapi.api_renderers import PhenopacketsRenderer @@ -54,3 +59,13 @@ class MedicationStatementViewSet(McodeModelViewSet): class MCodePacketViewSet(McodeModelViewSet): queryset = MCodePacket.objects.all() serializer_class = MCodePacketSerializer + + +@api_view(["GET"]) +@permission_classes([AllowAny]) +def get_mcode_schema(_request): + """ + get: + Mcodepacket schema + """ + return Response(MCODE_SCHEMA) diff --git a/chord_metadata_service/mcode/models.py b/chord_metadata_service/mcode/models.py index 0122ddd50..5691ba33b 100644 --- a/chord_metadata_service/mcode/models.py +++ b/chord_metadata_service/mcode/models.py @@ -6,9 +6,12 @@ from django.core.exceptions import ValidationError from chord_metadata_service.restapi.description_utils import rec_help import chord_metadata_service.mcode.descriptions as d -from chord_metadata_service.restapi.validators import ( - ontology_validator, quantity_validator, tumor_marker_test_validator, - complex_ontology_validator, time_or_period_validator, ontology_list_validator +from chord_metadata_service.restapi.validators import ontology_validator, ontology_list_validator +from .validators import ( + quantity_validator, + tumor_marker_test_validator, + complex_ontology_validator, + time_or_period_validator ) diff --git a/chord_metadata_service/mcode/schemas.py b/chord_metadata_service/mcode/schemas.py new file mode 100644 index 000000000..45455ada3 --- /dev/null +++ b/chord_metadata_service/mcode/schemas.py @@ -0,0 +1,395 @@ +from chord_metadata_service.restapi.schema_utils import customize_schema +from chord_metadata_service.restapi.schemas import ONTOLOGY_CLASS, ONTOLOGY_CLASS_LIST, EXTRA_PROPERTIES_SCHEMA +from chord_metadata_service.restapi.description_utils import describe_schema +from .descriptions import * +from chord_metadata_service.phenopackets.schemas import PHENOPACKET_GENE_SCHEMA + + +################################## mCode/FHIR based schemas ################################## + +### FHIR datatypes + +# FHIR Quantity https://www.hl7.org/fhir/datatypes.html#Quantity +QUANTITY = { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "chord_metadata_service:quantity_schema", + "title": "Quantity schema", + "description": "Schema for the datatype Quantity.", + "type": "object", + "properties": { + "value": { + "type": "number" + }, + "comparator": { + "enum": ["<", ">", "<=", ">=", "="] + }, + "unit": { + "type": "string" + }, + "system": { + "type": "string", + "format": "uri" + }, + "code": { + "type": "string" + } + }, + "additionalProperties": False +} + + +# FHIR CodeableConcept https://www.hl7.org/fhir/datatypes.html#CodeableConcept +CODEABLE_CONCEPT = { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "chord_metadata_service:codeable_concept_schema", + "title": "Codeable Concept schema", + "description": "Schema for the datatype Concept.", + "type": "object", + "properties": { + "coding": { + "type": "array", + "items": { + "type": "object", + "properties": { + "system": {"type": "string", "format": "uri"}, + "version": {"type": "string"}, + "code": {"type": "string"}, + "display": {"type": "string"}, + "user_selected": {"type": "boolean"} + } + } + }, + "text": { + "type": "string" + } + }, + "additionalProperties": False +} + + +# FHIR Period https://www.hl7.org/fhir/datatypes.html#Period +PERIOD = { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "chord_metadata_service:period_schema", + "title": "Period", + "description": "Period schema.", + "type": "object", + "properties": { + "start": { + "type": "string", + "format": "date-time" + }, + "end": { + "type": "string", + "format": "date-time" + } + }, + "additionalProperties": False +} + + +# FHIR Ratio https://www.hl7.org/fhir/datatypes.html#Ratio +RATIO = { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "chord_metadata_service:ratio", + "title": "Ratio", + "description": "Ratio schema.", + "type": "object", + "properties": { + "numerator": QUANTITY, + "denominator": QUANTITY + }, + "additionalProperties": False +} + + +### FHIR based mCode elements + +TIME_OR_PERIOD = { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "chord_metadata_service:time_or_period", + "title": "Time of Period", + "description": "Time of Period schema.", + "type": "object", + "properties": { + "value": { + "anyOf": [ + {"type": "string", "format": "date-time"}, + PERIOD + ] + } + }, + "additionalProperties": False +} + +# TODO this is definitely should be changed, fhir datatypes are too complex use Ontology_ class +COMPLEX_ONTOLOGY = customize_schema( + first_typeof=ONTOLOGY_CLASS, + second_typeof=ONTOLOGY_CLASS, + first_property="data_value", + second_property="staging_system", + schema_id="chord_metadata_service:complex_ontology_schema", + title="Complex ontology", + description="Complex object to combine data value and staging system.", + required=["data_value"] +) + +# TODO this is definitely should be changed, fhir datatypes are too complex use Ontology_ class +TUMOR_MARKER_TEST = customize_schema( + first_typeof=ONTOLOGY_CLASS, + second_typeof={ + "anyOf": [ + ONTOLOGY_CLASS, + QUANTITY, + RATIO + ] + }, + first_property="code", + second_property="data_value", + schema_id="chord_metadata_service:tumor_marker_test", + title="Tumor marker test", + description="Tumor marker test schema.", + required=["code"] +) + +############################## Metadata service mCode based schemas ############################## + + +MCODE_GENETIC_VARIANT_TESTED_SCHEMA = describe_schema({ + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "gene_studied": { + "type": "string" + }, + "method": ONTOLOGY_CLASS, + "variant_tested_identifier": ONTOLOGY_CLASS, + "variant_tested_hgvs_name": { + "type": "array", + "items": { + "type": "string" + } + }, + "variant_tested_description": { + "type": "string" + }, + "data_value": ONTOLOGY_CLASS, + "extra_properties": EXTRA_PROPERTIES_SCHEMA + }, + "required": ["id"] +}, GENETIC_VARIANT_TESTED) + +MCODE_GENETIC_VARIANT_FOUND_SCHEMA = describe_schema({ + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "method": ONTOLOGY_CLASS, + "variant_found_identifier": ONTOLOGY_CLASS, + "variant_found_hgvs_name": { + "type": "array", + "items": { + "type": "string" + } + }, + "variant_found_description": { + "type": "string" + }, + "genomic_source_class": ONTOLOGY_CLASS, + "extra_properties": EXTRA_PROPERTIES_SCHEMA + }, + "required": ["id"] +}, GENETIC_VARIANT_FOUND) + +MCODE_GENOMICS_REPORT_SCHEMA = describe_schema({ + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "test_name": ONTOLOGY_CLASS, + "performing_organization_name": { + "type": "string" + }, + "specimen_type": ONTOLOGY_CLASS, + "genetic_variant_tested": { + "type": "array", + "items": { + "string" + } + }, + "genetic_variant_found": { + "type": "array", + "items": { + "string" + } + }, + "extra_properties": EXTRA_PROPERTIES_SCHEMA + }, + "required": ["id", "test_name"] +}, GENOMICS_REPORT) + +MCODE_LABS_VITAL_SCHEMA = describe_schema({ + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "subject": { + "type": "string" + }, + "body_height": QUANTITY, + "body_weight": QUANTITY, + "cbc_with_auto_differential_panel": { + "type": "array", + "items": { + "type": "string" + } + }, + "comprehensive_metabolic_2000": { + "type": "array", + "items": { + "type": "string" + } + }, + "blood_pressure_diastolic": QUANTITY, + "blood_pressure_systolic": QUANTITY, + "tumor_marker_test": TUMOR_MARKER_TEST, + "extra_properties": EXTRA_PROPERTIES_SCHEMA + }, + "required": ["id", "individual", "body_height", "body_weight", "tumor_marker_test"] +}, LABS_VITAL) + +# TODO check required inb data dictionary +MCODE_CANCER_CONDITION_SCHEMA = describe_schema({ + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "condition_type": { + "type": "string", + "enum": [ + "primary", + "secondary" + ] + }, + "body_location_code": ONTOLOGY_CLASS_LIST, + "clinical_status": ONTOLOGY_CLASS, + "condition_code": ONTOLOGY_CLASS, + "date_of_diagnosis": { + "type": "string", + "format": "date-time" + }, + "histology_morphology_behavior": ONTOLOGY_CLASS, + "extra_properties": EXTRA_PROPERTIES_SCHEMA + }, + "required": ["id", "condition_type", "condition_code"] +}, LABS_VITAL) + + +MCODE_TNM_STAGING_SCHEMA = describe_schema({ + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "tnm_type": { + "type": "string", + "enum": [ + "clinical", + "pathologic" + ] + }, + "stage_group": COMPLEX_ONTOLOGY, + "primary_tumor_category": COMPLEX_ONTOLOGY, + "regional_nodes_category": COMPLEX_ONTOLOGY, + "distant_metastases_category": COMPLEX_ONTOLOGY, + "cancer_condition": { + "type": "string" + }, + "extra_properties": EXTRA_PROPERTIES_SCHEMA + }, + "required": [ + "id", + "tnm_type", + "stage_group", + "primary_tumor_category", + "regional_nodes_category", + "distant_metastases_category", + "cancer_condition" + ] +}, TNM_STAGING) + + +MCODE_CANCER_RELATED_PROCEDURE_SCHEMA = describe_schema({ + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "procedure_type": { + "type": "string", + "enum": [ + "radiation", + "surgical" + ] + }, + "code": ONTOLOGY_CLASS, + "occurence_time_or_period": TIME_OR_PERIOD, + "target_body_site": ONTOLOGY_CLASS_LIST, + "treatment_intent": ONTOLOGY_CLASS, + "extra_properties": EXTRA_PROPERTIES_SCHEMA + }, + "required": ["id", "procedure_type", "code", "occurence_time_or_period"] +}, CANCER_RELATED_PROCEDURE) + + +MCODE_MEDICATION_STATEMENT_SCHEMA = describe_schema({ + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "medication_code": ONTOLOGY_CLASS, + "termination_reason": ONTOLOGY_CLASS_LIST, + "treatment_intent": ONTOLOGY_CLASS, + "start_date": { + "type": "string", + "format": "date-time" + }, + "end_date": { + "type": "string", + "format": "date-time" + }, + "date_time": { + "type": "string", + "format": "date-time" + }, + + "extra_properties": EXTRA_PROPERTIES_SCHEMA + }, + "required": ["id", "medication_code"] +}, MEDICATION_STATEMENT) + +# TODO add subject fk to individual +MCODE_SCHEMA = describe_schema({ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "chord_metadata_service:mcode_schema", + "title": "Metadata service customized mcode schema", + "description": "Schema for describe mcode data elements in metadata service.", + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "genomics_report": MCODE_GENOMICS_REPORT_SCHEMA, + "cancer_condition": MCODE_CANCER_CONDITION_SCHEMA, + "cancer_related_procedures": MCODE_CANCER_RELATED_PROCEDURE_SCHEMA, + "medication_statement": MCODE_MEDICATION_STATEMENT_SCHEMA, + "extra_properties": EXTRA_PROPERTIES_SCHEMA + } +}, MCODEPACKET) diff --git a/chord_metadata_service/mcode/validators.py b/chord_metadata_service/mcode/validators.py new file mode 100644 index 000000000..9fcb9cea1 --- /dev/null +++ b/chord_metadata_service/mcode/validators.py @@ -0,0 +1,8 @@ +from chord_metadata_service.restapi.validators import JsonSchemaValidator +from .schemas import * + + +quantity_validator = JsonSchemaValidator(schema=QUANTITY, formats=['uri']) +tumor_marker_test_validator = JsonSchemaValidator(schema=TUMOR_MARKER_TEST) +complex_ontology_validator = JsonSchemaValidator(schema=COMPLEX_ONTOLOGY, formats=['uri']) +time_or_period_validator = JsonSchemaValidator(schema=TIME_OR_PERIOD, formats=['date-time']) diff --git a/chord_metadata_service/restapi/schema_utils.py b/chord_metadata_service/restapi/schema_utils.py index a6797fc48..3bf8158f4 100644 --- a/chord_metadata_service/restapi/schema_utils.py +++ b/chord_metadata_service/restapi/schema_utils.py @@ -1,6 +1,5 @@ from typing import Optional - __all__ = ["tag_schema_with_search_properties"] @@ -36,3 +35,23 @@ def tag_schema_with_search_properties(schema, search_descriptions: Optional[dict } return schema_with_search + + +def customize_schema(first_typeof: dict, second_typeof: dict, first_property: str, second_property: str, + schema_id: str = None, title: str = None, description: str = None, additional_properties: bool = False, + required=None) -> dict: + if required is None: + required = [] + return { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": schema_id, + "title": title, + "description": description, + "type": "object", + "properties": { + first_property: first_typeof, + second_property: second_typeof + }, + "required": required, + "additionalProperties": additional_properties + } diff --git a/chord_metadata_service/restapi/schemas.py b/chord_metadata_service/restapi/schemas.py index 68bfaecc6..6b95abe2b 100644 --- a/chord_metadata_service/restapi/schemas.py +++ b/chord_metadata_service/restapi/schemas.py @@ -1,5 +1,6 @@ from . import descriptions -from .description_utils import describe_schema +from .description_utils import describe_schema, EXTRA_PROPERTIES +from .schema_utils import customize_schema # Individual schemas for validation of JSONField values @@ -13,15 +14,7 @@ "AGE_RANGE", "AGE_OR_AGE_RANGE", - "QUANTITY", - "CODEABLE_CONCEPT", - "PERIOD", - "RATIO", - - "TIME_OR_PERIOD", "COMORBID_CONDITION", - "COMPLEX_ONTOLOGY", - "TUMOR_MARKER_TEST", ] @@ -64,6 +57,10 @@ "additionalProperties": False } +EXTRA_PROPERTIES_SCHEMA = describe_schema({ + "type": "object" +}, EXTRA_PROPERTIES) + AGE_STRING = describe_schema({"type": "string"}, descriptions.AGE) @@ -120,143 +117,6 @@ ] } -################################## mCode/FHIR based schemas ################################## - -### FHIR datatypes - -# FHIR Quantity https://www.hl7.org/fhir/datatypes.html#Quantity -QUANTITY = { - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "chord_metadata_service:quantity_schema", - "title": "Quantity schema", - "description": "Schema for the datatype Quantity.", - "type": "object", - "properties": { - "value": { - "type": "number" - }, - "comparator": { - "enum": ["<", ">", "<=", ">=", "="] - }, - "unit": { - "type": "string" - }, - "system": { - "type": "string", - "format": "uri" - }, - "code": { - "type": "string" - } - }, - "additionalProperties": False -} - - -# FHIR CodeableConcept https://www.hl7.org/fhir/datatypes.html#CodeableConcept -CODEABLE_CONCEPT = { - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "chord_metadata_service:codeable_concept_schema", - "title": "Codeable Concept schema", - "description": "Schema for the datatype Concept.", - "type": "object", - "properties": { - "coding": { - "type": "array", - "items": { - "type": "object", - "properties": { - "system": {"type": "string", "format": "uri"}, - "version": {"type": "string"}, - "code": {"type": "string"}, - "display": {"type": "string"}, - "user_selected": {"type": "boolean"} - } - } - }, - "text": { - "type": "string" - } - }, - "additionalProperties": False -} - - -# FHIR Period https://www.hl7.org/fhir/datatypes.html#Period -PERIOD = { - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "chord_metadata_service:period_schema", - "title": "Period", - "description": "Period schema.", - "type": "object", - "properties": { - "start": { - "type": "string", - "format": "date-time" - }, - "end": { - "type": "string", - "format": "date-time" - } - }, - "additionalProperties": False -} - - -# FHIR Ratio https://www.hl7.org/fhir/datatypes.html#Ratio -RATIO = { - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "chord_metadata_service:ratio", - "title": "Ratio", - "description": "Ratio schema.", - "type": "object", - "properties": { - "numerator": QUANTITY, - "denominator": QUANTITY - }, - "additionalProperties": False -} - - -### FHIR based mCode elements - -TIME_OR_PERIOD = { - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "chord_metadata_service:time_or_period", - "title": "Time of Period", - "description": "Time of Period schema.", - "type": "object", - "properties": { - "value": { - "anyOf": [ - {"type": "string", "format": "date-time"}, - PERIOD - ] - } - }, - "additionalProperties": False -} - - -def customize_schema(first_typeof: dict, second_typeof: dict, first_property: str, second_property: str, - schema_id: str, title: str = None, description: str = None, additional_properties: bool = False, - required=None) -> dict: - if required is None: - required = [] - return { - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": schema_id, - "title": title, - "description": description, - "type": "object", - "properties": { - first_property: first_typeof, - second_property: second_typeof - }, - "required": required, - "additionalProperties": additional_properties - } - COMORBID_CONDITION = customize_schema( first_typeof=ONTOLOGY_CLASS, @@ -267,33 +127,3 @@ def customize_schema(first_typeof: dict, second_typeof: dict, first_property: st title="Comorbid Condition schema", description="Comorbid condition schema." ) - -# TODO this is definitely should be changed, fhir datatypes are too complex use Ontology_ class -COMPLEX_ONTOLOGY = customize_schema( - first_typeof=ONTOLOGY_CLASS, - second_typeof=ONTOLOGY_CLASS, - first_property="data_value", - second_property="staging_system", - schema_id="chord_metadata_service:complex_ontology_schema", - title="Complex ontology", - description="Complex object to combine data value and staging system.", - required=["data_value"] -) - -# TODO this is definitely should be changed, fhir datatypes are too complex use Ontology_ class -TUMOR_MARKER_TEST = customize_schema( - first_typeof=ONTOLOGY_CLASS, - second_typeof={ - "anyOf": [ - ONTOLOGY_CLASS, - QUANTITY, - RATIO - ] - }, - first_property="code", - second_property="data_value", - schema_id="chord_metadata_service:tumor_marker_test", - title="Tumor marker test", - description="Tumor marker test schema.", - required=["code"] -) diff --git a/chord_metadata_service/restapi/urls.py b/chord_metadata_service/restapi/urls.py index 327d0ba2d..c3a578934 100644 --- a/chord_metadata_service/restapi/urls.py +++ b/chord_metadata_service/restapi/urls.py @@ -6,7 +6,6 @@ from chord_metadata_service.phenopackets import api_views as phenopacket_views from chord_metadata_service.mcode import api_views as mcode_views - # from .settings import DEBUG @@ -49,4 +48,9 @@ urlpatterns = [ path('', include(router.urls)), + # apps schemas + path('experiment_schema', experiment_views.get_experiment_schema, + name="experiment-schema"), + path('mcode_schema', mcode_views.get_mcode_schema, + name="mcode-schema"), ] diff --git a/chord_metadata_service/restapi/validators.py b/chord_metadata_service/restapi/validators.py index 6e0ae8bc2..e4ffaca9c 100644 --- a/chord_metadata_service/restapi/validators.py +++ b/chord_metadata_service/restapi/validators.py @@ -3,10 +3,6 @@ from chord_metadata_service.restapi.schemas import ( AGE_OR_AGE_RANGE, ONTOLOGY_CLASS, - QUANTITY, - COMPLEX_ONTOLOGY, - TIME_OR_PERIOD, - TUMOR_MARKER_TEST, ONTOLOGY_CLASS_LIST, KEY_VALUE_OBJECT, COMORBID_CONDITION, @@ -41,8 +37,8 @@ def deconstruct(self): ontology_validator = JsonSchemaValidator(ONTOLOGY_CLASS) ontology_list_validator = JsonSchemaValidator(ONTOLOGY_CLASS_LIST) key_value_validator = JsonSchemaValidator(KEY_VALUE_OBJECT) -quantity_validator = JsonSchemaValidator(schema=QUANTITY, formats=['uri']) -tumor_marker_test_validator = JsonSchemaValidator(schema=TUMOR_MARKER_TEST) -complex_ontology_validator = JsonSchemaValidator(schema=COMPLEX_ONTOLOGY, formats=['uri']) -time_or_period_validator = JsonSchemaValidator(schema=TIME_OR_PERIOD, formats=['date-time']) +# quantity_validator = JsonSchemaValidator(schema=QUANTITY, formats=['uri']) +# tumor_marker_test_validator = JsonSchemaValidator(schema=TUMOR_MARKER_TEST) +# complex_ontology_validator = JsonSchemaValidator(schema=COMPLEX_ONTOLOGY, formats=['uri']) +# time_or_period_validator = JsonSchemaValidator(schema=TIME_OR_PERIOD, formats=['date-time']) comorbid_condition_validator = JsonSchemaValidator(COMORBID_CONDITION) From ab54d0f0ad6e34a07885d6acf09111a0437f1c2a Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Tue, 12 May 2020 17:50:21 -0400 Subject: [PATCH 007/190] add patients schemas and validators --- .../patients/descriptions.py | 38 ++++++++++++++----- chord_metadata_service/patients/models.py | 7 +--- chord_metadata_service/patients/schemas.py | 13 +++++++ chord_metadata_service/patients/validators.py | 5 +++ chord_metadata_service/restapi/schemas.py | 14 ------- chord_metadata_service/restapi/validators.py | 6 --- 6 files changed, 49 insertions(+), 34 deletions(-) create mode 100644 chord_metadata_service/patients/schemas.py create mode 100644 chord_metadata_service/patients/validators.py diff --git a/chord_metadata_service/patients/descriptions.py b/chord_metadata_service/patients/descriptions.py index c895b5835..21938c7e6 100644 --- a/chord_metadata_service/patients/descriptions.py +++ b/chord_metadata_service/patients/descriptions.py @@ -12,23 +12,43 @@ "items": "One of possibly many alternative identifiers for an individual." }, "date_of_birth": "A timestamp representing an individual's date of birth; either exactly or imprecisely.", - "age": None, # TODO: Age or AgeRange + "age": "The age or age range of the individual.", "sex": "The phenotypic sex of an individual, as would be determined by a midwife or physician at birth.", "karyotypic_sex": "The karyotypic sex of an individual.", "taxonomy": ontology_class("specified when more than one organism may be studied. It is advised that codes" "from the NCBI Taxonomy resource are used, e.g. NCBITaxon:9606 for humans"), # FHIR-specific - "active": "Whether a patient's record is in active use.", - "deceased": "Whether a patient is deceased.", + "active": { + "description": "Whether a patient's record is in active use.", + "help": "FHIR-specific property." + }, + "deceased": { + "description": "Whether a patient is deceased.", + "help": "FHIR-specific property." + }, # mCode-specific - "race": "A code for a person's race (mCode).", - "ethnicity": "A code for a person's ethnicity (mCode).", - "comorbid_condition": "One or more conditions that occur with primary condition.", - "ecog_performance_status": "Value representing the Eastern Cooperative Oncology Group performance status.", - "karnofsky": "Value representing the Karnofsky Performance status.", - + "race": { + "description": "A code for a person's race (mCode).", + "help": "mCode-specific property." + }, + "ethnicity": { + "description": "A code for a person's ethnicity (mCode).", + "help": "mCode-specific property." + }, + "comorbid_condition": { + "description": "One or more conditions that occur with primary condition.", + "help": "mCode-specific property." + }, + "ecog_performance_status": { + "description": "Value representing the Eastern Cooperative Oncology Group performance status.", + "help": "mCode-specific property." + }, + "karnofsky": { + "description": "Value representing the Karnofsky Performance status.", + "help": "mCode-specific property." + }, **EXTRA_PROPERTIES } } diff --git a/chord_metadata_service/patients/models.py b/chord_metadata_service/patients/models.py index 2aa0b425e..7c473614b 100644 --- a/chord_metadata_service/patients/models.py +++ b/chord_metadata_service/patients/models.py @@ -1,11 +1,8 @@ from django.db import models from django.contrib.postgres.fields import JSONField, ArrayField from chord_metadata_service.restapi.models import IndexableMixin -from chord_metadata_service.restapi.validators import ( - ontology_validator, - age_or_age_range_validator, - comorbid_condition_validator, -) +from chord_metadata_service.restapi.validators import ontology_validator, age_or_age_range_validator +from .validators import comorbid_condition_validator class Individual(models.Model, IndexableMixin): diff --git a/chord_metadata_service/patients/schemas.py b/chord_metadata_service/patients/schemas.py new file mode 100644 index 000000000..b586a936a --- /dev/null +++ b/chord_metadata_service/patients/schemas.py @@ -0,0 +1,13 @@ +from chord_metadata_service.restapi.schema_utils import customize_schema +from chord_metadata_service.restapi.schemas import ONTOLOGY_CLASS + + +COMORBID_CONDITION = customize_schema( + first_typeof=ONTOLOGY_CLASS, + second_typeof=ONTOLOGY_CLASS, + first_property="clinical_status", + second_property="code", + schema_id="chord_metadata_service:comorbid_condition_schema", + title="Comorbid Condition schema", + description="Comorbid condition schema." +) diff --git a/chord_metadata_service/patients/validators.py b/chord_metadata_service/patients/validators.py new file mode 100644 index 000000000..5f22ad29d --- /dev/null +++ b/chord_metadata_service/patients/validators.py @@ -0,0 +1,5 @@ +from chord_metadata_service.restapi.validators import JsonSchemaValidator +from .schemas import COMORBID_CONDITION + + +comorbid_condition_validator = JsonSchemaValidator(COMORBID_CONDITION) diff --git a/chord_metadata_service/restapi/schemas.py b/chord_metadata_service/restapi/schemas.py index 6b95abe2b..270faa815 100644 --- a/chord_metadata_service/restapi/schemas.py +++ b/chord_metadata_service/restapi/schemas.py @@ -1,6 +1,5 @@ from . import descriptions from .description_utils import describe_schema, EXTRA_PROPERTIES -from .schema_utils import customize_schema # Individual schemas for validation of JSONField values @@ -13,8 +12,6 @@ "AGE", "AGE_RANGE", "AGE_OR_AGE_RANGE", - - "COMORBID_CONDITION", ] @@ -116,14 +113,3 @@ ONTOLOGY_CLASS ] } - - -COMORBID_CONDITION = customize_schema( - first_typeof=ONTOLOGY_CLASS, - second_typeof=ONTOLOGY_CLASS, - first_property="clinical_status", - second_property="code", - schema_id="chord_metadata_service:comorbid_condition_schema", - title="Comorbid Condition schema", - description="Comorbid condition schema." -) diff --git a/chord_metadata_service/restapi/validators.py b/chord_metadata_service/restapi/validators.py index e4ffaca9c..20a017160 100644 --- a/chord_metadata_service/restapi/validators.py +++ b/chord_metadata_service/restapi/validators.py @@ -5,7 +5,6 @@ ONTOLOGY_CLASS, ONTOLOGY_CLASS_LIST, KEY_VALUE_OBJECT, - COMORBID_CONDITION, ) @@ -37,8 +36,3 @@ def deconstruct(self): ontology_validator = JsonSchemaValidator(ONTOLOGY_CLASS) ontology_list_validator = JsonSchemaValidator(ONTOLOGY_CLASS_LIST) key_value_validator = JsonSchemaValidator(KEY_VALUE_OBJECT) -# quantity_validator = JsonSchemaValidator(schema=QUANTITY, formats=['uri']) -# tumor_marker_test_validator = JsonSchemaValidator(schema=TUMOR_MARKER_TEST) -# complex_ontology_validator = JsonSchemaValidator(schema=COMPLEX_ONTOLOGY, formats=['uri']) -# time_or_period_validator = JsonSchemaValidator(schema=TIME_OR_PERIOD, formats=['date-time']) -comorbid_condition_validator = JsonSchemaValidator(COMORBID_CONDITION) From e107b0724a3615070e580c3542a5886496890c3a Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Wed, 13 May 2020 10:59:26 -0400 Subject: [PATCH 008/190] add extra properties to schemas --- .../phenopackets/schemas.py | 42 +++++++++++++++---- chord_metadata_service/restapi/schemas.py | 1 + 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/chord_metadata_service/phenopackets/schemas.py b/chord_metadata_service/phenopackets/schemas.py index a6182c34d..2f027a423 100644 --- a/chord_metadata_service/phenopackets/schemas.py +++ b/chord_metadata_service/phenopackets/schemas.py @@ -3,7 +3,10 @@ import chord_metadata_service.phenopackets.descriptions as descriptions from chord_metadata_service.patients.descriptions import INDIVIDUAL from chord_metadata_service.restapi.description_utils import describe_schema, ONTOLOGY_CLASS as ONTOLOGY_CLASS_DESC -from chord_metadata_service.restapi.schemas import AGE, AGE_RANGE, AGE_OR_AGE_RANGE, ONTOLOGY_CLASS +from chord_metadata_service.restapi.schemas import ( + AGE, AGE_RANGE, AGE_OR_AGE_RANGE, ONTOLOGY_CLASS, EXTRA_PROPERTIES_SCHEMA +) +from chord_metadata_service.patients.schemas import COMORBID_CONDITION __all__ = [ @@ -127,6 +130,22 @@ "description": "An individual's karyotypic sex.", }, "taxonomy": PHENOPACKET_ONTOLOGY_SCHEMA, + "active": { + "type": "boolean" + }, + "deceased": { + "type": "boolean" + }, + "race": { + "type": "string" + }, + "ethnicity": { + "type": "string" + }, + "comorbid_condition": COMORBID_CONDITION, + "ecog_performance_status": PHENOPACKET_ONTOLOGY_SCHEMA, + "karnofsky": PHENOPACKET_ONTOLOGY_SCHEMA, + "extra_properties": EXTRA_PROPERTIES_SCHEMA, }, "required": ["id"] }, INDIVIDUAL) @@ -152,7 +171,8 @@ }, "iri_prefix": { "type": "string", - } + }, + "extra_properties": EXTRA_PROPERTIES_SCHEMA }, "required": ["id", "name", "namespace_prefix", "url", "version", "iri_prefix"], }, descriptions.RESOURCE) @@ -209,7 +229,8 @@ "external_references": { "type": "array", "items": PHENOPACKET_EXTERNAL_REFERENCE_SCHEMA - } + }, + "extra_properties": EXTRA_PROPERTIES_SCHEMA }, }, descriptions.META_DATA) @@ -242,6 +263,7 @@ }, "onset": PHENOPACKET_ONTOLOGY_SCHEMA, "evidence": PHENOPACKET_EVIDENCE_SCHEMA, + "extra_properties": EXTRA_PROPERTIES_SCHEMA }, }, descriptions.PHENOTYPIC_FEATURE) @@ -261,7 +283,8 @@ }, "symbol": { "type": "string", - } + }, + "extra_properties": EXTRA_PROPERTIES_SCHEMA }, "required": ["id", "symbol"] }, descriptions.GENE) @@ -285,7 +308,8 @@ }, "individual_to_sample_identifiers": { "type": "object" # TODO - } + }, + "extra_properties": EXTRA_PROPERTIES_SCHEMA } }, descriptions.HTS_FILE) @@ -295,7 +319,8 @@ "type": "object", # TODO "properties": { "allele": ALLELE_SCHEMA, # TODO - "zygosity": PHENOPACKET_ONTOLOGY_SCHEMA + "zygosity": PHENOPACKET_ONTOLOGY_SCHEMA, + "extra_properties": EXTRA_PROPERTIES_SCHEMA } }, descriptions.VARIANT) @@ -345,6 +370,7 @@ "is_control_sample": { "type": "boolean" }, + "extra_properties": EXTRA_PROPERTIES_SCHEMA }, "required": ["id", "sampled_tissue", "procedure"], }, descriptions.BIOSAMPLE) @@ -379,6 +405,7 @@ "type": "array", "items": PHENOPACKET_ONTOLOGY_SCHEMA, }, + "extra_properties": EXTRA_PROPERTIES_SCHEMA }, "required": ["term"], }, descriptions.DISEASE) @@ -420,7 +447,8 @@ "type": "array", "items": PHENOPACKET_HTS_FILE_SCHEMA # TODO }, - "meta_data": PHENOPACKET_META_DATA_SCHEMA + "meta_data": PHENOPACKET_META_DATA_SCHEMA, + "extra_properties": EXTRA_PROPERTIES_SCHEMA }, "required": ["id", "meta_data"], }, descriptions.PHENOPACKET) diff --git a/chord_metadata_service/restapi/schemas.py b/chord_metadata_service/restapi/schemas.py index 270faa815..d8160edf1 100644 --- a/chord_metadata_service/restapi/schemas.py +++ b/chord_metadata_service/restapi/schemas.py @@ -12,6 +12,7 @@ "AGE", "AGE_RANGE", "AGE_OR_AGE_RANGE", + "EXTRA_PROPERTIES_SCHEMA", ] From c82d8c958920008298cdcc9ffc7980e05050da4b Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Wed, 13 May 2020 13:33:38 -0400 Subject: [PATCH 009/190] add view for chord phenopacket schema --- chord_metadata_service/phenopackets/api_views.py | 14 ++++++++++++++ chord_metadata_service/restapi/urls.py | 2 ++ 2 files changed, 16 insertions(+) diff --git a/chord_metadata_service/phenopackets/api_views.py b/chord_metadata_service/phenopackets/api_views.py index 46e086dc7..57b2ad0a8 100644 --- a/chord_metadata_service/phenopackets/api_views.py +++ b/chord_metadata_service/phenopackets/api_views.py @@ -1,8 +1,12 @@ from rest_framework import viewsets from rest_framework.settings import api_settings +from rest_framework.decorators import api_view, permission_classes +from rest_framework.permissions import AllowAny +from rest_framework.response import Response from chord_metadata_service.restapi.api_renderers import * from chord_metadata_service.restapi.pagination import LargeResultsSetPagination +from chord_metadata_service.phenopackets.schemas import PHENOPACKET_SCHEMA from .models import * from .serializers import * @@ -207,3 +211,13 @@ class InterpretationViewSet(PhenopacketsModelViewSet): """ queryset = Interpretation.objects.all().order_by("id") serializer_class = InterpretationSerializer + + +@api_view(["GET"]) +@permission_classes([AllowAny]) +def get_chord_phenopacket_schema(_request): + """ + get: + Chord phenopacket schema that can be shared with data providers. + """ + return Response(PHENOPACKET_SCHEMA) diff --git a/chord_metadata_service/restapi/urls.py b/chord_metadata_service/restapi/urls.py index c3a578934..bc03d97ed 100644 --- a/chord_metadata_service/restapi/urls.py +++ b/chord_metadata_service/restapi/urls.py @@ -49,6 +49,8 @@ urlpatterns = [ path('', include(router.urls)), # apps schemas + path('chord_phenopacket_schema', phenopacket_views.get_chord_phenopacket_schema, + name="chord-phenopacket-schema"), path('experiment_schema', experiment_views.get_experiment_schema, name="experiment-schema"), path('mcode_schema', mcode_views.get_mcode_schema, From 252aafb9328cc38864fb5f1a6d7e6393090c6edd Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Wed, 13 May 2020 13:55:52 -0400 Subject: [PATCH 010/190] move individual schema to patients app --- chord_metadata_service/mcode/schemas.py | 3 +- chord_metadata_service/patients/schemas.py | 69 ++++++++++++++++++- .../phenopackets/schemas.py | 69 +------------------ .../phenopackets/search_schemas.py | 3 +- 4 files changed, 74 insertions(+), 70 deletions(-) diff --git a/chord_metadata_service/mcode/schemas.py b/chord_metadata_service/mcode/schemas.py index 45455ada3..2e01f76af 100644 --- a/chord_metadata_service/mcode/schemas.py +++ b/chord_metadata_service/mcode/schemas.py @@ -1,8 +1,8 @@ from chord_metadata_service.restapi.schema_utils import customize_schema from chord_metadata_service.restapi.schemas import ONTOLOGY_CLASS, ONTOLOGY_CLASS_LIST, EXTRA_PROPERTIES_SCHEMA from chord_metadata_service.restapi.description_utils import describe_schema +from chord_metadata_service.patients.schemas import INDIVIDUAL_SCHEMA from .descriptions import * -from chord_metadata_service.phenopackets.schemas import PHENOPACKET_GENE_SCHEMA ################################## mCode/FHIR based schemas ################################## @@ -386,6 +386,7 @@ "id": { "type": "string" }, + "subject": INDIVIDUAL_SCHEMA, "genomics_report": MCODE_GENOMICS_REPORT_SCHEMA, "cancer_condition": MCODE_CANCER_CONDITION_SCHEMA, "cancer_related_procedures": MCODE_CANCER_RELATED_PROCEDURE_SCHEMA, diff --git a/chord_metadata_service/patients/schemas.py b/chord_metadata_service/patients/schemas.py index b586a936a..922684d34 100644 --- a/chord_metadata_service/patients/schemas.py +++ b/chord_metadata_service/patients/schemas.py @@ -1,5 +1,8 @@ from chord_metadata_service.restapi.schema_utils import customize_schema -from chord_metadata_service.restapi.schemas import ONTOLOGY_CLASS +from chord_metadata_service.restapi.schemas import ONTOLOGY_CLASS, AGE_OR_AGE_RANGE, EXTRA_PROPERTIES_SCHEMA +from chord_metadata_service.restapi.description_utils import describe_schema + +from .descriptions import INDIVIDUAL COMORBID_CONDITION = customize_schema( @@ -11,3 +14,67 @@ title="Comorbid Condition schema", description="Comorbid condition schema." ) + + +INDIVIDUAL_SCHEMA = describe_schema({ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique researcher-specified identifier for the individual.", + }, + "alternate_ids": { + "type": "array", + "items": { + "type": "string", + }, + "description": "A list of alternative identifiers for the individual.", # TODO: More specific + }, + "date_of_birth": { + # TODO: This is a special ISO format... need UI for this + "type": "string", + }, + "age": AGE_OR_AGE_RANGE, + "sex": { + "type": "string", + "enum": ["UNKNOWN_SEX", "FEMALE", "MALE", "OTHER_SEX"], + "description": "An individual's phenotypic sex.", + }, + "karyotypic_sex": { + "type": "string", + "enum": [ + "UNKNOWN_KARYOTYPE", + "XX", + "XY", + "XO", + "XXY", + "XXX", + "XXYY", + "XXXY", + "XXXX", + "XYY", + "OTHER_KARYOTYPE" + ], + "description": "An individual's karyotypic sex.", + }, + "taxonomy": ONTOLOGY_CLASS, + "active": { + "type": "boolean" + }, + "deceased": { + "type": "boolean" + }, + "race": { + "type": "string" + }, + "ethnicity": { + "type": "string" + }, + "comorbid_condition": COMORBID_CONDITION, + "ecog_performance_status": ONTOLOGY_CLASS, + "karnofsky": ONTOLOGY_CLASS, + "extra_properties": EXTRA_PROPERTIES_SCHEMA, + }, + "required": ["id"] +}, INDIVIDUAL) diff --git a/chord_metadata_service/phenopackets/schemas.py b/chord_metadata_service/phenopackets/schemas.py index 2f027a423..ff76c1f87 100644 --- a/chord_metadata_service/phenopackets/schemas.py +++ b/chord_metadata_service/phenopackets/schemas.py @@ -1,19 +1,17 @@ # Individual schemas for validation of JSONField values import chord_metadata_service.phenopackets.descriptions as descriptions -from chord_metadata_service.patients.descriptions import INDIVIDUAL +from chord_metadata_service.patients.schemas import INDIVIDUAL_SCHEMA from chord_metadata_service.restapi.description_utils import describe_schema, ONTOLOGY_CLASS as ONTOLOGY_CLASS_DESC from chord_metadata_service.restapi.schemas import ( AGE, AGE_RANGE, AGE_OR_AGE_RANGE, ONTOLOGY_CLASS, EXTRA_PROPERTIES_SCHEMA ) -from chord_metadata_service.patients.schemas import COMORBID_CONDITION __all__ = [ "ALLELE_SCHEMA", "PHENOPACKET_ONTOLOGY_SCHEMA", "PHENOPACKET_EXTERNAL_REFERENCE_SCHEMA", - "PHENOPACKET_INDIVIDUAL_SCHEMA", "PHENOPACKET_RESOURCE_SCHEMA", "PHENOPACKET_UPDATE_SCHEMA", "PHENOPACKET_META_DATA_SCHEMA", @@ -87,69 +85,6 @@ }, descriptions.EXTERNAL_REFERENCE) -PHENOPACKET_INDIVIDUAL_SCHEMA = describe_schema({ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Unique researcher-specified identifier for the individual.", - }, - "alternate_ids": { - "type": "array", - "items": { - "type": "string", - }, - "description": "A list of alternative identifiers for the individual.", # TODO: More specific - }, - "date_of_birth": { - # TODO: This is a special ISO format... need UI for this - "type": "string", - }, - "age": AGE_OR_AGE_RANGE, - "sex": { - "type": "string", - "enum": ["UNKNOWN_SEX", "FEMALE", "MALE", "OTHER_SEX"], - "description": "An individual's phenotypic sex.", - }, - "karyotypic_sex": { - "type": "string", - "enum": [ - "UNKNOWN_KARYOTYPE", - "XX", - "XY", - "XO", - "XXY", - "XXX", - "XXYY", - "XXXY", - "XXXX", - "XYY", - "OTHER_KARYOTYPE" - ], - "description": "An individual's karyotypic sex.", - }, - "taxonomy": PHENOPACKET_ONTOLOGY_SCHEMA, - "active": { - "type": "boolean" - }, - "deceased": { - "type": "boolean" - }, - "race": { - "type": "string" - }, - "ethnicity": { - "type": "string" - }, - "comorbid_condition": COMORBID_CONDITION, - "ecog_performance_status": PHENOPACKET_ONTOLOGY_SCHEMA, - "karnofsky": PHENOPACKET_ONTOLOGY_SCHEMA, - "extra_properties": EXTRA_PROPERTIES_SCHEMA, - }, - "required": ["id"] -}, INDIVIDUAL) - PHENOPACKET_RESOURCE_SCHEMA = describe_schema({ "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", # TODO @@ -422,7 +357,7 @@ "id": { "type": "string", }, - "subject": PHENOPACKET_INDIVIDUAL_SCHEMA, + "subject": INDIVIDUAL_SCHEMA, "phenotypic_features": { "type": "array", "items": PHENOPACKET_PHENOTYPIC_FEATURE_SCHEMA diff --git a/chord_metadata_service/phenopackets/search_schemas.py b/chord_metadata_service/phenopackets/search_schemas.py index c04381d3a..71127bfb5 100644 --- a/chord_metadata_service/phenopackets/search_schemas.py +++ b/chord_metadata_service/phenopackets/search_schemas.py @@ -1,4 +1,5 @@ from . import models, schemas +from chord_metadata_service.patients.schemas import INDIVIDUAL_SCHEMA from chord_metadata_service.restapi.schema_utils import tag_schema_with_search_properties @@ -84,7 +85,7 @@ def _tag_with_database_attrs(schema: dict, db_attrs: dict): } }) -INDIVIDUAL_SEARCH_SCHEMA = tag_schema_with_search_properties(schemas.PHENOPACKET_INDIVIDUAL_SCHEMA, { +INDIVIDUAL_SEARCH_SCHEMA = tag_schema_with_search_properties(INDIVIDUAL_SCHEMA, { "properties": { "id": { "search": { From 4b95f358e7f851421c041d9e286a99635bb7820f Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Wed, 13 May 2020 14:01:05 -0400 Subject: [PATCH 011/190] style fixes --- .../experiments/descriptions.py | 2 +- chord_metadata_service/experiments/models.py | 29 ++++++++++++------- chord_metadata_service/mcode/schemas.py | 3 ++ 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/chord_metadata_service/experiments/descriptions.py b/chord_metadata_service/experiments/descriptions.py index 993acf257..96d22280c 100644 --- a/chord_metadata_service/experiments/descriptions.py +++ b/chord_metadata_service/experiments/descriptions.py @@ -15,7 +15,7 @@ "other_fields": "The other fields for the experiment", - "biosample": "Biosamples on which this experiment was done", + "biosample": "Biosample on which this experiment was done", "individual": "Donor on which this experiment was done", **EXTRA_PROPERTIES diff --git a/chord_metadata_service/experiments/models.py b/chord_metadata_service/experiments/models.py index 8e0a0bc22..d557c582c 100644 --- a/chord_metadata_service/experiments/models.py +++ b/chord_metadata_service/experiments/models.py @@ -38,22 +38,31 @@ class Experiment(models.Model, IndexableMixin): id = CharField(primary_key=True, max_length=200, help_text=rec_help(d.EXPERIMENT, 'id')) - reference_registry_id = CharField(max_length=30, blank=True, null=True, help_text=rec_help(d.EXPERIMENT, 'reference_registry_id')) - qc_flags = ArrayField(CharField(max_length=100, help_text=rec_help(d.EXPERIMENT, 'qc_flags')), null=True, blank=True, default=list) + reference_registry_id = CharField(max_length=30, blank=True, null=True, + help_text=rec_help(d.EXPERIMENT, 'reference_registry_id')) + qc_flags = ArrayField(CharField(max_length=100, help_text=rec_help(d.EXPERIMENT, 'qc_flags')), + null=True, blank=True, default=list) experiment_type = CharField(max_length=30, help_text=rec_help(d.EXPERIMENT, 'experiment_type')) - experiment_ontology = JSONField(blank=True, null=True, validators=[ontology_list_validator], help_text=rec_help(d.EXPERIMENT, 'experiment_ontology')) - molecule_ontology = JSONField(blank=True, null=True, validators=[ontology_list_validator], help_text=rec_help(d.EXPERIMENT, 'molecule_ontology')) - molecule = CharField(choices=MOLECULE, max_length=20, blank=True, null=True, help_text=rec_help(d.EXPERIMENT, 'molecule')) - library_strategy = CharField(choices=LIBRARY_STRATEGY, max_length=25, help_text=rec_help(d.EXPERIMENT, 'library_strategy')) + experiment_ontology = JSONField(blank=True, null=True, validators=[ontology_list_validator], + help_text=rec_help(d.EXPERIMENT, 'experiment_ontology')) + molecule_ontology = JSONField(blank=True, null=True, validators=[ontology_list_validator], + help_text=rec_help(d.EXPERIMENT, 'molecule_ontology')) + molecule = CharField(choices=MOLECULE, max_length=20, blank=True, null=True, + help_text=rec_help(d.EXPERIMENT, 'molecule')) + library_strategy = CharField(choices=LIBRARY_STRATEGY, max_length=25, + help_text=rec_help(d.EXPERIMENT, 'library_strategy')) - other_fields = JSONField(blank=True, null=True, validators=[key_value_validator], help_text=rec_help(d.EXPERIMENT, 'other_fields')) + other_fields = JSONField(blank=True, null=True, validators=[key_value_validator], + help_text=rec_help(d.EXPERIMENT, 'other_fields')) - biosample = models.ForeignKey(Biosample, on_delete=models.SET_NULL, blank=True, null=True, help_text=rec_help(d.EXPERIMENT, 'biosample')) - individual = models.ForeignKey(Individual, on_delete=models.SET_NULL, blank=True, null=True, help_text=rec_help(d.EXPERIMENT, 'individual')) + biosample = models.ForeignKey(Biosample, on_delete=models.SET_NULL, blank=True, null=True, + help_text=rec_help(d.EXPERIMENT, 'biosample')) + individual = models.ForeignKey(Individual, on_delete=models.SET_NULL, blank=True, null=True, + help_text=rec_help(d.EXPERIMENT, 'individual')) def clean(self): if not (self.biosample or self.individual): - raise ValidationError('Either Biosamples or Individual must be specified') + raise ValidationError('Either Biosample or Individual must be specified') def __str__(self): return str(self.id) diff --git a/chord_metadata_service/mcode/schemas.py b/chord_metadata_service/mcode/schemas.py index 2e01f76af..dee1d4dda 100644 --- a/chord_metadata_service/mcode/schemas.py +++ b/chord_metadata_service/mcode/schemas.py @@ -181,6 +181,7 @@ "required": ["id"] }, GENETIC_VARIANT_TESTED) + MCODE_GENETIC_VARIANT_FOUND_SCHEMA = describe_schema({ "type": "object", "properties": { @@ -204,6 +205,7 @@ "required": ["id"] }, GENETIC_VARIANT_FOUND) + MCODE_GENOMICS_REPORT_SCHEMA = describe_schema({ "type": "object", "properties": { @@ -232,6 +234,7 @@ "required": ["id", "test_name"] }, GENOMICS_REPORT) + MCODE_LABS_VITAL_SCHEMA = describe_schema({ "type": "object", "properties": { From 607a27b3d92acfc6895908789a47f0e4119dcbbc Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Wed, 13 May 2020 14:49:11 -0400 Subject: [PATCH 012/190] add migrations --- .../migrations/0005_auto_20200513_1401.py | 36 +++++++++ .../migrations/0003_auto_20200513_1401.py | 75 +++++++++++++++++++ .../migrations/0008_auto_20200513_1401.py | 20 +++++ .../migrations/0007_auto_20200513_1401.py | 41 ++++++++++ 4 files changed, 172 insertions(+) create mode 100644 chord_metadata_service/experiments/migrations/0005_auto_20200513_1401.py create mode 100644 chord_metadata_service/mcode/migrations/0003_auto_20200513_1401.py create mode 100644 chord_metadata_service/patients/migrations/0008_auto_20200513_1401.py create mode 100644 chord_metadata_service/phenopackets/migrations/0007_auto_20200513_1401.py diff --git a/chord_metadata_service/experiments/migrations/0005_auto_20200513_1401.py b/chord_metadata_service/experiments/migrations/0005_auto_20200513_1401.py new file mode 100644 index 000000000..21f774d30 --- /dev/null +++ b/chord_metadata_service/experiments/migrations/0005_auto_20200513_1401.py @@ -0,0 +1,36 @@ +# Generated by Django 2.2.12 on 2020-05-13 18:01 + +import chord_metadata_service.restapi.validators +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('experiments', '0004_auto_20200401_1445'), + ] + + operations = [ + migrations.AlterField( + model_name='experiment', + name='biosample', + field=models.ForeignKey(blank=True, help_text='Biosample on which this experiment was done', null=True, on_delete=django.db.models.deletion.SET_NULL, to='phenopackets.Biosample'), + ), + migrations.AlterField( + model_name='experiment', + name='experiment_ontology', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='(Ontology: OBI) links to experiment ontology information.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)]), + ), + migrations.AlterField( + model_name='experiment', + name='molecule_ontology', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='(Ontology: SO) links to molecule ontology information.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)]), + ), + migrations.AlterField( + model_name='experiment', + name='other_fields', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The other fields for the experiment', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:key_value_object_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'The schema represents a key-value object.', 'patternProperties': {'^.*$': {'type': 'string'}}, 'title': 'Key-value object', 'type': 'object'}, formats=None)]), + ), + ] diff --git a/chord_metadata_service/mcode/migrations/0003_auto_20200513_1401.py b/chord_metadata_service/mcode/migrations/0003_auto_20200513_1401.py new file mode 100644 index 000000000..74eb69de3 --- /dev/null +++ b/chord_metadata_service/mcode/migrations/0003_auto_20200513_1401.py @@ -0,0 +1,75 @@ +# Generated by Django 2.2.12 on 2020-05-13 18:01 + +import chord_metadata_service.restapi.validators +import django.contrib.postgres.fields.jsonb +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('mcode', '0002_auto_20200401_1008'), + ] + + operations = [ + migrations.AlterField( + model_name='cancercondition', + name='body_location_code', + field=django.contrib.postgres.fields.jsonb.JSONField(help_text='Code for the body location, optionally pre-coordinating laterality or direction. Accepted ontologies: SNOMED CT, ICD-O-3 and others.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)]), + ), + migrations.AlterField( + model_name='cancerrelatedprocedure', + name='occurence_time_or_period', + field=django.contrib.postgres.fields.jsonb.JSONField(help_text='The date/time that a procedure was performed.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:time_or_period', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Time of Period schema.', 'properties': {'value': {'anyOf': [{'format': 'date-time', 'type': 'string'}, {'$id': 'chord_metadata_service:period_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Period schema.', 'properties': {'end': {'format': 'date-time', 'type': 'string'}, 'start': {'format': 'date-time', 'type': 'string'}}, 'title': 'Period', 'type': 'object'}]}}, 'title': 'Time of Period', 'type': 'object'}, formats=['date-time'])]), + ), + migrations.AlterField( + model_name='cancerrelatedprocedure', + name='target_body_site', + field=django.contrib.postgres.fields.jsonb.JSONField(help_text='The body location(s) where the procedure was performed.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)]), + ), + migrations.AlterField( + model_name='labsvital', + name='blood_pressure_diastolic', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The blood pressure after the contraction of the heart while the chambers of the heart refill with blood, when the pressure is lowest.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:quantity_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema for the datatype Quantity.', 'properties': {'code': {'type': 'string'}, 'comparator': {'enum': ['<', '>', '<=', '>=', '=']}, 'system': {'format': 'uri', 'type': 'string'}, 'unit': {'type': 'string'}, 'value': {'type': 'number'}}, 'title': 'Quantity schema', 'type': 'object'}, formats=['uri'])]), + ), + migrations.AlterField( + model_name='labsvital', + name='blood_pressure_systolic', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The blood pressure during the contraction of the left ventricle of the heart, when blood pressure is at its highest.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:quantity_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema for the datatype Quantity.', 'properties': {'code': {'type': 'string'}, 'comparator': {'enum': ['<', '>', '<=', '>=', '=']}, 'system': {'format': 'uri', 'type': 'string'}, 'unit': {'type': 'string'}, 'value': {'type': 'number'}}, 'title': 'Quantity schema', 'type': 'object'}, formats=['uri'])]), + ), + migrations.AlterField( + model_name='labsvital', + name='body_height', + field=django.contrib.postgres.fields.jsonb.JSONField(help_text="The patient's height.", validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:quantity_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema for the datatype Quantity.', 'properties': {'code': {'type': 'string'}, 'comparator': {'enum': ['<', '>', '<=', '>=', '=']}, 'system': {'format': 'uri', 'type': 'string'}, 'unit': {'type': 'string'}, 'value': {'type': 'number'}}, 'title': 'Quantity schema', 'type': 'object'}, formats=['uri'])]), + ), + migrations.AlterField( + model_name='labsvital', + name='body_weight', + field=django.contrib.postgres.fields.jsonb.JSONField(help_text="The patient's weight.", validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:quantity_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema for the datatype Quantity.', 'properties': {'code': {'type': 'string'}, 'comparator': {'enum': ['<', '>', '<=', '>=', '=']}, 'system': {'format': 'uri', 'type': 'string'}, 'unit': {'type': 'string'}, 'value': {'type': 'number'}}, 'title': 'Quantity schema', 'type': 'object'}, formats=['uri'])]), + ), + migrations.AlterField( + model_name='medicationstatement', + name='termination_reason', + field=django.contrib.postgres.fields.jsonb.JSONField(help_text='A code explaining unplanned or premature termination of a course of medication. Accepted ontologies: SNOMED CT.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)]), + ), + migrations.AlterField( + model_name='tnmstaging', + name='distant_metastases_category', + field=django.contrib.postgres.fields.jsonb.JSONField(help_text='Category describing the presence or absence of metastases in remote anatomical locations. Accepted ontologies: SNOMED CT, AJCC and others.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:complex_ontology_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Complex object to combine data value and staging system.', 'properties': {'data_value': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'staging_system': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}}, 'required': ['data_value'], 'title': 'Complex ontology', 'type': 'object'}, formats=['uri'])]), + ), + migrations.AlterField( + model_name='tnmstaging', + name='primary_tumor_category', + field=django.contrib.postgres.fields.jsonb.JSONField(help_text='Category of the primary tumor, based on its size and extent. Accepted ontologies: SNOMED CT, AJCC and others.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:complex_ontology_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Complex object to combine data value and staging system.', 'properties': {'data_value': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'staging_system': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}}, 'required': ['data_value'], 'title': 'Complex ontology', 'type': 'object'}, formats=['uri'])]), + ), + migrations.AlterField( + model_name='tnmstaging', + name='regional_nodes_category', + field=django.contrib.postgres.fields.jsonb.JSONField(help_text='Category of the presence or absence of metastases in regional lymph nodes. Accepted ontologies: SNOMED CT, AJCC and others.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:complex_ontology_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Complex object to combine data value and staging system.', 'properties': {'data_value': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'staging_system': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}}, 'required': ['data_value'], 'title': 'Complex ontology', 'type': 'object'}, formats=['uri'])]), + ), + migrations.AlterField( + model_name='tnmstaging', + name='stage_group', + field=django.contrib.postgres.fields.jsonb.JSONField(help_text='The extent of the cancer in the body, according to the TNM classification system.Accepted ontologies: SNOMED CT, AJCC and others.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:complex_ontology_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Complex object to combine data value and staging system.', 'properties': {'data_value': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'staging_system': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}}, 'required': ['data_value'], 'title': 'Complex ontology', 'type': 'object'}, formats=['uri'])]), + ), + ] diff --git a/chord_metadata_service/patients/migrations/0008_auto_20200513_1401.py b/chord_metadata_service/patients/migrations/0008_auto_20200513_1401.py new file mode 100644 index 000000000..5b9855122 --- /dev/null +++ b/chord_metadata_service/patients/migrations/0008_auto_20200513_1401.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.12 on 2020-05-13 18:01 + +import chord_metadata_service.restapi.validators +import django.contrib.postgres.fields.jsonb +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('patients', '0007_auto_20200430_1444'), + ] + + operations = [ + migrations.AlterField( + model_name='individual', + name='age', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The age or age range of the individual.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:age_or_age_range_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'An age object describing the age of the individual at the time of collection of biospecimens or phenotypic observations.', 'oneOf': [{'$id': 'chord_metadata_service:age_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ISO8601 duration string (e.g. P40Y10M05D for 40 years, 10 months, 5 days) representing an age of a subject.', 'help': 'An ISO8601 duration string (e.g. P40Y10M05D for 40 years, 10 months, 5 days) representing an age of a subject.', 'properties': {'age': {'description': 'An ISO8601 duration string (e.g. P40Y10M05D for 40 years, 10 months, 5 days) representing an age of a subject.', 'help': 'Age of a subject.', 'type': 'string'}}, 'required': ['age'], 'title': 'Age schema', 'type': 'object'}, {'$id': 'chord_metadata_service:age_range_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': "Age range of a subject (e.g. when a subject's age falls into a bin.)", 'help': "Age range of a subject (e.g. when a subject's age falls into a bin.)", 'properties': {'end': {'$id': 'chord_metadata_service:age_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ISO8601 duration string representing the end of the age range bin.', 'help': 'An ISO8601 duration string representing the end of the age range bin.', 'properties': {'age': {'description': 'An ISO8601 duration string (e.g. P40Y10M05D for 40 years, 10 months, 5 days) representing an age of a subject.', 'help': 'Age of a subject.', 'type': 'string'}}, 'required': ['age'], 'title': 'Age schema', 'type': 'object'}, 'start': {'$id': 'chord_metadata_service:age_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ISO8601 duration string representing the start of the age range bin.', 'help': 'An ISO8601 duration string representing the start of the age range bin.', 'properties': {'age': {'description': 'An ISO8601 duration string (e.g. P40Y10M05D for 40 years, 10 months, 5 days) representing an age of a subject.', 'help': 'Age of a subject.', 'type': 'string'}}, 'required': ['age'], 'title': 'Age schema', 'type': 'object'}}, 'required': ['start', 'end'], 'title': 'Age range schema', 'type': 'object'}], 'title': 'Age schema', 'type': 'object'}, formats=None)]), + ), + ] diff --git a/chord_metadata_service/phenopackets/migrations/0007_auto_20200513_1401.py b/chord_metadata_service/phenopackets/migrations/0007_auto_20200513_1401.py new file mode 100644 index 000000000..4d794cd40 --- /dev/null +++ b/chord_metadata_service/phenopackets/migrations/0007_auto_20200513_1401.py @@ -0,0 +1,41 @@ +# Generated by Django 2.2.12 on 2020-05-13 18:01 + +import chord_metadata_service.restapi.validators +import django.contrib.postgres.fields +import django.contrib.postgres.fields.jsonb +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('phenopackets', '0006_auto_20200430_1444'), + ] + + operations = [ + migrations.AlterField( + model_name='biosample', + name='individual_age_at_collection', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='individual_age_at_collection', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:age_or_age_range_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'An age object describing the age of the individual at the time of collection of biospecimens or phenotypic observations.', 'oneOf': [{'$id': 'chord_metadata_service:age_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ISO8601 duration string (e.g. P40Y10M05D for 40 years, 10 months, 5 days) representing an age of a subject.', 'help': 'An ISO8601 duration string (e.g. P40Y10M05D for 40 years, 10 months, 5 days) representing an age of a subject.', 'properties': {'age': {'description': 'An ISO8601 duration string (e.g. P40Y10M05D for 40 years, 10 months, 5 days) representing an age of a subject.', 'help': 'Age of a subject.', 'type': 'string'}}, 'required': ['age'], 'title': 'Age schema', 'type': 'object'}, {'$id': 'chord_metadata_service:age_range_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': "Age range of a subject (e.g. when a subject's age falls into a bin.)", 'help': "Age range of a subject (e.g. when a subject's age falls into a bin.)", 'properties': {'end': {'$id': 'chord_metadata_service:age_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ISO8601 duration string representing the end of the age range bin.', 'help': 'An ISO8601 duration string representing the end of the age range bin.', 'properties': {'age': {'description': 'An ISO8601 duration string (e.g. P40Y10M05D for 40 years, 10 months, 5 days) representing an age of a subject.', 'help': 'Age of a subject.', 'type': 'string'}}, 'required': ['age'], 'title': 'Age schema', 'type': 'object'}, 'start': {'$id': 'chord_metadata_service:age_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ISO8601 duration string representing the start of the age range bin.', 'help': 'An ISO8601 duration string representing the start of the age range bin.', 'properties': {'age': {'description': 'An ISO8601 duration string (e.g. P40Y10M05D for 40 years, 10 months, 5 days) representing an age of a subject.', 'help': 'Age of a subject.', 'type': 'string'}}, 'required': ['age'], 'title': 'Age schema', 'type': 'object'}}, 'required': ['start', 'end'], 'title': 'Age range schema', 'type': 'object'}], 'title': 'Age schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='disease', + name='onset', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='A representation of the age of onset of the disease', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:disease_onset_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'anyOf': [{'$id': 'chord_metadata_service:age_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ISO8601 duration string (e.g. P40Y10M05D for 40 years, 10 months, 5 days) representing an age of a subject.', 'help': 'An ISO8601 duration string (e.g. P40Y10M05D for 40 years, 10 months, 5 days) representing an age of a subject.', 'properties': {'age': {'description': 'An ISO8601 duration string (e.g. P40Y10M05D for 40 years, 10 months, 5 days) representing an age of a subject.', 'help': 'Age of a subject.', 'type': 'string'}}, 'required': ['age'], 'title': 'Age schema', 'type': 'object'}, {'$id': 'chord_metadata_service:age_range_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': "Age range of a subject (e.g. when a subject's age falls into a bin.)", 'help': "Age range of a subject (e.g. when a subject's age falls into a bin.)", 'properties': {'end': {'$id': 'chord_metadata_service:age_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ISO8601 duration string representing the end of the age range bin.', 'help': 'An ISO8601 duration string representing the end of the age range bin.', 'properties': {'age': {'description': 'An ISO8601 duration string (e.g. P40Y10M05D for 40 years, 10 months, 5 days) representing an age of a subject.', 'help': 'Age of a subject.', 'type': 'string'}}, 'required': ['age'], 'title': 'Age schema', 'type': 'object'}, 'start': {'$id': 'chord_metadata_service:age_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ISO8601 duration string representing the start of the age range bin.', 'help': 'An ISO8601 duration string representing the start of the age range bin.', 'properties': {'age': {'description': 'An ISO8601 duration string (e.g. P40Y10M05D for 40 years, 10 months, 5 days) representing an age of a subject.', 'help': 'Age of a subject.', 'type': 'string'}}, 'required': ['age'], 'title': 'Age schema', 'type': 'object'}}, 'required': ['start', 'end'], 'title': 'Age range schema', 'type': 'object'}, {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}], 'description': 'Schema for the age of the onset of the disease.', 'title': 'Onset age', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='metadata', + name='external_references', + field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:external_reference_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'An encoding of information about a reference to an external resource.', 'help': 'An encoding of information about a reference to an external resource.', 'properties': {'description': {'description': 'An application-specific free-text description.', 'help': 'An application-specific free-text description.', 'type': 'string'}, 'id': {'description': 'An application-specific identifier. It is RECOMMENDED that this is a CURIE that uniquely identifies the evidence source when combined with a resource; e.g. PMID:123456 with a resource `pmid`. It could also be a URI or other relevant identifier.', 'help': 'An application-specific identifier. It is RECOMMENDED that this is a CURIE that uniquely identifies the evidence source when combined with a resource; e.g. PMID:123456 with a resource `pmid`. It could also be a URI or other relevant identifier.', 'type': 'string'}}, 'required': ['id'], 'title': 'External reference schema', 'type': 'object'}, formats=None)]), blank=True, help_text='A list of external (non-resource) references.', null=True, size=None), + ), + migrations.AlterField( + model_name='metadata', + name='updates', + field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:update_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An update event for a record (e.g. a phenopacket.)', 'help': 'An update event for a record (e.g. a phenopacket.)', 'properties': {'comment': {'description': 'Free-text comment about the changes made and/or the reason for the update.', 'help': 'Free-text comment about the changes made and/or the reason for the update.', 'type': 'string'}, 'timestamp': {'description': 'ISO8601 UTC timestamp specifying when when this update occurred.', 'format': 'date-time', 'help': 'Timestamp specifying when when this update occurred.', 'type': 'string'}, 'updated_by': {'description': 'Information about the person/organization/network that performed the update.', 'help': 'Information about the person/organization/network that performed the update.', 'type': 'string'}}, 'required': ['timestamp', 'comment'], 'title': 'Updates schema', 'type': 'object'}, formats=['date-time'])]), blank=True, help_text='A list of updates to the phenopacket.', null=True, size=None), + ), + migrations.AlterField( + model_name='phenotypicfeature', + name='evidence', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='One or more pieces of evidence that specify how the phenotype was determined.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:evidence_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'A representation of the evidence for an assertion, such as an observation of a phenotypic feature.', 'help': 'A representation of the evidence for an assertion, such as an observation of a phenotypic feature.', 'properties': {'evidence_code': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term that represents the evidence type.', 'help': 'An ontology term that represents the evidence type.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term that represents the evidence type.', 'help': 'A CURIE-style identifier for an ontology term that represents the evidence type.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term that represents the evidence type.', 'help': 'A human readable class name for an ontology term that represents the evidence type.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'reference': {'$id': 'chord_metadata_service:external_reference_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'An encoding of information about a reference to an external resource.', 'help': 'An encoding of information about a reference to an external resource.', 'properties': {'description': {'description': 'An application-specific free-text description.', 'help': 'An application-specific free-text description.', 'type': 'string'}, 'id': {'description': 'An application-specific identifier. It is RECOMMENDED that this is a CURIE that uniquely identifies the evidence source when combined with a resource; e.g. PMID:123456 with a resource `pmid`. It could also be a URI or other relevant identifier.', 'help': 'An application-specific identifier. It is RECOMMENDED that this is a CURIE that uniquely identifies the evidence source when combined with a resource; e.g. PMID:123456 with a resource `pmid`. It could also be a URI or other relevant identifier.', 'type': 'string'}}, 'required': ['id'], 'title': 'External reference schema', 'type': 'object'}}, 'required': ['evidence_code'], 'title': 'Evidence schema', 'type': 'object'}, formats=None)]), + ), + ] From 2ee24e72d05f8980c201252277b7d8b80df647a8 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Wed, 13 May 2020 19:00:00 -0400 Subject: [PATCH 013/190] add allele descriptions --- .../phenopackets/descriptions.py | 18 ++++++++++++++++++ chord_metadata_service/phenopackets/schemas.py | 9 +++++---- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/chord_metadata_service/phenopackets/descriptions.py b/chord_metadata_service/phenopackets/descriptions.py index 2ab3485c6..7f0928838 100644 --- a/chord_metadata_service/phenopackets/descriptions.py +++ b/chord_metadata_service/phenopackets/descriptions.py @@ -191,6 +191,24 @@ def phenotypic_feature(subject="a subject or biosample"): } } +ALLELE = { + "properties": { + "id": "An arbitrary identifier.", + "hgvs": "", + "genome_assembly": "The reference genome identifier e.g. GRCh38.", + "chr": "A chromosome identifier e.g. chr2 or 2.", + "pos": "The 1-based genomic position e.g. 134327882.", + "ref": "The reference base(s).", + "alt": "The alternate base(s).", + "info": "Relevant parts of the INFO field.", + "seq_id": "Sequence ID, e.g. Seq1.", + "position": "Position , a 0-based coordinate for where the Deleted Sequence starts, e.g. 4.", + "deleted_sequence": "Deleted sequence , sequence for the deletion, can be empty, e.g. A", + "inserted_sequence": "Inserted sequence , sequence for the insertion, can be empty, e.g. G", + "iscn": "E.g. t(8;9;11)(q12;p24;p12)." + } +} + VARIANT = { "description": "A representation used to describe candidate or diagnosed causative variants.", # TODO: GA4GH VR "properties": { diff --git a/chord_metadata_service/phenopackets/schemas.py b/chord_metadata_service/phenopackets/schemas.py index ff76c1f87..c3c8a1cf8 100644 --- a/chord_metadata_service/phenopackets/schemas.py +++ b/chord_metadata_service/phenopackets/schemas.py @@ -27,7 +27,7 @@ ] -ALLELE_SCHEMA = { +ALLELE_SCHEMA = describe_schema({ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "chord_metadata_service:allele_schema", "title": "Allele schema", @@ -41,7 +41,7 @@ "genome_assembly": {"type": "string"}, "chr": {"type": "string"}, "pos": {"type": "integer"}, - "re": {"type": "string"}, + "ref": {"type": "string"}, "alt": {"type": "string"}, "info": {"type": "string"}, @@ -60,10 +60,10 @@ {"required": ["iscn"]} ], "dependencies": { - "genome_assembly": ["chr", "pos", "re", "alt", "info"], + "genome_assembly": ["chr", "pos", "ref", "alt", "info"], "seq_id": ["position", "deleted_sequence", "inserted_sequence"] } -} # TODO: Descriptions +}, descriptions.ALLELE) PHENOPACKET_ONTOLOGY_SCHEMA = describe_schema(ONTOLOGY_CLASS, ONTOLOGY_CLASS_DESC) @@ -195,6 +195,7 @@ "severity": PHENOPACKET_ONTOLOGY_SCHEMA, "modifier": { # TODO: Plural? "type": "array", + "items": PHENOPACKET_ONTOLOGY_SCHEMA }, "onset": PHENOPACKET_ONTOLOGY_SCHEMA, "evidence": PHENOPACKET_EVIDENCE_SCHEMA, From 8595a9ee45dfa2d704dfbe6f2a5dfe7376d8b9f3 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Wed, 13 May 2020 20:36:32 -0400 Subject: [PATCH 014/190] some adjustments in descriptions --- .../experiments/descriptions.py | 23 ++++++++++++------- .../phenopackets/descriptions.py | 4 ++-- chord_metadata_service/restapi/schemas.py | 2 +- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/chord_metadata_service/experiments/descriptions.py b/chord_metadata_service/experiments/descriptions.py index 96d22280c..50963793c 100644 --- a/chord_metadata_service/experiments/descriptions.py +++ b/chord_metadata_service/experiments/descriptions.py @@ -1,22 +1,29 @@ from chord_metadata_service.restapi.description_utils import EXTRA_PROPERTIES + EXPERIMENT = { - "description": "A subject of a phenopacket, representing either a human (typically) or another organism.", + "description": "Experiment related metadata.", "properties": { "id": "An arbitrary identifier for the experiment.", - "reference_registry_id": "The IHEC EpiRR ID for this dataset, only for IHEC Reference Epigenome datasets. Otherwise leave empty.", + "reference_registry_id": "The IHEC EpiRR ID for this dataset, only for IHEC Reference Epigenome datasets. " + "Otherwise leave empty.", "qc_flags": "Any quanlity control observations can be noted here. This field can be omitted if empty", - "experiment_type": "(Controlled Vocabulary) The assay target (e.g. ‘DNA Methylation’, ‘mRNA-Seq’, ‘smRNA-Seq’, 'Histone H3K4me1').", + "experiment_type": "(Controlled Vocabulary) The assay target (e.g. ‘DNA Methylation’, ‘mRNA-Seq’, ‘smRNA-Seq’, " + "'Histone H3K4me1').", "experiment_ontology": "(Ontology: OBI) links to experiment ontology information.", "molecule_ontology": "(Ontology: SO) links to molecule ontology information.", - "molecule": "(Controlled Vocabulary) The type of molecule that was extracted from the biological material. Include one of the following: total RNA, polyA RNA, cytoplasmic RNA, nuclear RNA, small RNA, genomic DNA, protein, or other.", - "library_strategy": "(Controlled Vocabulary) The assay used. These are defined within the SRA metadata specifications with a controlled vocabulary (e.g. ‘Bisulfite-Seq’, ‘RNA-Seq’, ‘ChIP-Seq’). For a complete list, see https://www.ebi.ac.uk/ena/submit/reads-library-strategy.", + "molecule": "(Controlled Vocabulary) The type of molecule that was extracted from the biological material." + "Include one of the following: total RNA, polyA RNA, cytoplasmic RNA, nuclear RNA, small RNA, " + "genomic DNA, protein, or other.", + "library_strategy": "(Controlled Vocabulary) The assay used. These are defined within the SRA metadata " + "specifications with a controlled vocabulary (e.g. ‘Bisulfite-Seq’, ‘RNA-Seq’, ‘ChIP-Seq’)." + " For a complete list, see https://www.ebi.ac.uk/ena/submit/reads-library-strategy.", - "other_fields": "The other fields for the experiment", + "other_fields": "The other fields for the experiment.", - "biosample": "Biosample on which this experiment was done", - "individual": "Donor on which this experiment was done", + "biosample": "Biosample on which this experiment was done.", + "individual": "Donor on which this experiment was done.", **EXTRA_PROPERTIES } diff --git a/chord_metadata_service/phenopackets/descriptions.py b/chord_metadata_service/phenopackets/descriptions.py index 7f0928838..036885ce2 100644 --- a/chord_metadata_service/phenopackets/descriptions.py +++ b/chord_metadata_service/phenopackets/descriptions.py @@ -267,7 +267,7 @@ def phenotypic_feature(subject="a subject or biosample"): "tumor_progression": ontology_class("representing if the specimen is from a primary tumour, a metastasis, or a " "recurrence. There are multiple ways of representing this using ontology " "terms, and the terms chosen will have a specific meaning that is " - "application specific."), + "application specific"), "tumor_grade": ontology_class("representing the tumour grade. This should be a child term of NCIT:C28076 " "(Disease Grade Qualifier) or equivalent"), "diagnostic_markers": { @@ -275,7 +275,7 @@ def phenotypic_feature(subject="a subject or biosample"): "items": ontology_class("representing a clinically-relevant bio-marker. Most of the assays, such as " "immunohistochemistry (IHC), are covered by the NCIT ontology under the " "sub-hierarchy NCIT:C25294 (Laboratory Procedure), e.g. NCIT:C68748 " - "(HER2/Neu Positive), or NCIT:C131711 Human Papillomavirus-18 Positive).") + "(HER2/Neu Positive), or NCIT:C131711 Human Papillomavirus-18 Positive)") }, "procedure": PROCEDURE, "hts_files": { diff --git a/chord_metadata_service/restapi/schemas.py b/chord_metadata_service/restapi/schemas.py index d8160edf1..ee1e6f529 100644 --- a/chord_metadata_service/restapi/schemas.py +++ b/chord_metadata_service/restapi/schemas.py @@ -23,7 +23,7 @@ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "chord_metadata_service:ontology_class_schema", "title": "Ontology class schema", - "description": "todo", + "description": "Schema to describe terms from ontologies.", "type": "object", "properties": { "id": {"type": "string", "description": "CURIE style identifier."}, From 87f8119d82619a77b509dbc94812d0b68f7ecf2c Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Thu, 14 May 2020 15:41:55 -0400 Subject: [PATCH 015/190] add migrations --- .../migrations/0006_auto_20200514_1541.py | 46 +++++++ .../migrations/0004_auto_20200514_1541.py | 130 ++++++++++++++++++ .../migrations/0009_auto_20200514_1541.py | 35 +++++ .../migrations/0008_auto_20200514_1541.py | 101 ++++++++++++++ 4 files changed, 312 insertions(+) create mode 100644 chord_metadata_service/experiments/migrations/0006_auto_20200514_1541.py create mode 100644 chord_metadata_service/mcode/migrations/0004_auto_20200514_1541.py create mode 100644 chord_metadata_service/patients/migrations/0009_auto_20200514_1541.py create mode 100644 chord_metadata_service/phenopackets/migrations/0008_auto_20200514_1541.py diff --git a/chord_metadata_service/experiments/migrations/0006_auto_20200514_1541.py b/chord_metadata_service/experiments/migrations/0006_auto_20200514_1541.py new file mode 100644 index 000000000..9e9b6f967 --- /dev/null +++ b/chord_metadata_service/experiments/migrations/0006_auto_20200514_1541.py @@ -0,0 +1,46 @@ +# Generated by Django 2.2.12 on 2020-05-14 19:41 + +import chord_metadata_service.restapi.validators +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('experiments', '0005_auto_20200513_1401'), + ] + + operations = [ + migrations.AlterField( + model_name='experiment', + name='biosample', + field=models.ForeignKey(blank=True, help_text='Biosample on which this experiment was done.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='phenopackets.Biosample'), + ), + migrations.AlterField( + model_name='experiment', + name='experiment_ontology', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='(Ontology: OBI) links to experiment ontology information.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)]), + ), + migrations.AlterField( + model_name='experiment', + name='individual', + field=models.ForeignKey(blank=True, help_text='Donor on which this experiment was done.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='patients.Individual'), + ), + migrations.AlterField( + model_name='experiment', + name='molecule', + field=models.CharField(blank=True, choices=[('total RNA', 'total RNA'), ('polyA RNA', 'polyA RNA'), ('cytoplasmic RNA', 'cytoplasmic RNA'), ('nuclear RNA', 'nuclear RNA'), ('small RNA', 'small RNA'), ('genomic DNA', 'genomic DNA'), ('protein', 'protein'), ('other', 'other')], help_text='(Controlled Vocabulary) The type of molecule that was extracted from the biological material.Include one of the following: total RNA, polyA RNA, cytoplasmic RNA, nuclear RNA, small RNA, genomic DNA, protein, or other.', max_length=20, null=True), + ), + migrations.AlterField( + model_name='experiment', + name='molecule_ontology', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='(Ontology: SO) links to molecule ontology information.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)]), + ), + migrations.AlterField( + model_name='experiment', + name='other_fields', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The other fields for the experiment.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:key_value_object_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'The schema represents a key-value object.', 'patternProperties': {'^.*$': {'type': 'string'}}, 'title': 'Key-value object', 'type': 'object'}, formats=None)]), + ), + ] diff --git a/chord_metadata_service/mcode/migrations/0004_auto_20200514_1541.py b/chord_metadata_service/mcode/migrations/0004_auto_20200514_1541.py new file mode 100644 index 000000000..8d880bb51 --- /dev/null +++ b/chord_metadata_service/mcode/migrations/0004_auto_20200514_1541.py @@ -0,0 +1,130 @@ +# Generated by Django 2.2.12 on 2020-05-14 19:41 + +import chord_metadata_service.restapi.validators +import django.contrib.postgres.fields.jsonb +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('mcode', '0003_auto_20200513_1401'), + ] + + operations = [ + migrations.AlterField( + model_name='cancercondition', + name='body_location_code', + field=django.contrib.postgres.fields.jsonb.JSONField(help_text='Code for the body location, optionally pre-coordinating laterality or direction. Accepted ontologies: SNOMED CT, ICD-O-3 and others.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)]), + ), + migrations.AlterField( + model_name='cancercondition', + name='clinical_status', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='A flag indicating whether the condition is active or inactive, recurring, in remission, or resolved (as of the last update of the Condition). Accepted code system: http://terminology.hl7.org/CodeSystem/condition-clinical', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='cancercondition', + name='condition_code', + field=django.contrib.postgres.fields.jsonb.JSONField(help_text='A code describing the type of primary or secondary malignant neoplastic disease.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='cancercondition', + name='histology_morphology_behavior', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='A description of the morphologic and behavioral characteristics of the cancer. Accepted ontologies: SNOMED CT, ICD-O-3 and others.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='cancerrelatedprocedure', + name='code', + field=django.contrib.postgres.fields.jsonb.JSONField(help_text='Code for the procedure performed.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='cancerrelatedprocedure', + name='target_body_site', + field=django.contrib.postgres.fields.jsonb.JSONField(help_text='The body location(s) where the procedure was performed.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)]), + ), + migrations.AlterField( + model_name='cancerrelatedprocedure', + name='treatment_intent', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The purpose of a treatment.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='geneticvariantfound', + name='genomic_source_class', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology or controlled vocabulary term to identify the genomic class of the specimen being analyzed.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='geneticvariantfound', + name='method', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology or controlled vocabulary term to identify the method used to perform the genetic test. Accepted value set: NCIT.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='geneticvariantfound', + name='variant_found_identifier', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The variation ID assigned by HGVS, for example, 360448 is the identifier for NM_005228.4(EGFR):c.-237A>G (single nucleotide variant in EGFR). Accepted value set: ClinVar.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='geneticvarianttested', + name='data_value', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology or controlled vocabulary term to identify positive or negative value forthe mutation. Accepted value set: SNOMED CT.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='geneticvarianttested', + name='method', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology or controlled vocabulary term to identify the method used to perform the genetic test. Accepted value set: NCIT.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='geneticvarianttested', + name='variant_tested_identifier', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The variation ID assigned by HGVS, for example, 360448 is the identifier for NM_005228.4(EGFR):c.-237A>G (single nucleotide variant in EGFR).', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='genomicsreport', + name='specimen_type', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology or controlled vocabulary term to identify the type of material the specimen contains or consists of. Accepted value set: HL7 Version 2 and Specimen Type.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='genomicsreport', + name='test_name', + field=django.contrib.postgres.fields.jsonb.JSONField(help_text='An ontology or controlled vocabulary term to identify the laboratory test. Accepted value sets: LOINC, GTR.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='labsvital', + name='tumor_marker_test', + field=django.contrib.postgres.fields.jsonb.JSONField(help_text='An ontology or controlled vocabulary term to identify tumor marker test.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:tumor_marker_test', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Tumor marker test schema.', 'properties': {'code': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'data_value': {'anyOf': [{'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, {'$id': 'chord_metadata_service:quantity_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema for the datatype Quantity.', 'properties': {'code': {'type': 'string'}, 'comparator': {'enum': ['<', '>', '<=', '>=', '=']}, 'system': {'format': 'uri', 'type': 'string'}, 'unit': {'type': 'string'}, 'value': {'type': 'number'}}, 'title': 'Quantity schema', 'type': 'object'}, {'$id': 'chord_metadata_service:ratio', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Ratio schema.', 'properties': {'denominator': {'$id': 'chord_metadata_service:quantity_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema for the datatype Quantity.', 'properties': {'code': {'type': 'string'}, 'comparator': {'enum': ['<', '>', '<=', '>=', '=']}, 'system': {'format': 'uri', 'type': 'string'}, 'unit': {'type': 'string'}, 'value': {'type': 'number'}}, 'title': 'Quantity schema', 'type': 'object'}, 'numerator': {'$id': 'chord_metadata_service:quantity_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema for the datatype Quantity.', 'properties': {'code': {'type': 'string'}, 'comparator': {'enum': ['<', '>', '<=', '>=', '=']}, 'system': {'format': 'uri', 'type': 'string'}, 'unit': {'type': 'string'}, 'value': {'type': 'number'}}, 'title': 'Quantity schema', 'type': 'object'}}, 'title': 'Ratio', 'type': 'object'}]}}, 'required': ['code'], 'title': 'Tumor marker test', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='medicationstatement', + name='medication_code', + field=django.contrib.postgres.fields.jsonb.JSONField(help_text='A code for medication. Accepted code systems: Medication Clinical Drug (RxNorm) and other.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='medicationstatement', + name='termination_reason', + field=django.contrib.postgres.fields.jsonb.JSONField(help_text='A code explaining unplanned or premature termination of a course of medication. Accepted ontologies: SNOMED CT.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)]), + ), + migrations.AlterField( + model_name='medicationstatement', + name='treatment_intent', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The purpose of a treatment. Accepted ontologies: SNOMED CT.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='tnmstaging', + name='distant_metastases_category', + field=django.contrib.postgres.fields.jsonb.JSONField(help_text='Category describing the presence or absence of metastases in remote anatomical locations. Accepted ontologies: SNOMED CT, AJCC and others.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:complex_ontology_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Complex object to combine data value and staging system.', 'properties': {'data_value': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'staging_system': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}}, 'required': ['data_value'], 'title': 'Complex ontology', 'type': 'object'}, formats=['uri'])]), + ), + migrations.AlterField( + model_name='tnmstaging', + name='primary_tumor_category', + field=django.contrib.postgres.fields.jsonb.JSONField(help_text='Category of the primary tumor, based on its size and extent. Accepted ontologies: SNOMED CT, AJCC and others.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:complex_ontology_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Complex object to combine data value and staging system.', 'properties': {'data_value': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'staging_system': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}}, 'required': ['data_value'], 'title': 'Complex ontology', 'type': 'object'}, formats=['uri'])]), + ), + migrations.AlterField( + model_name='tnmstaging', + name='regional_nodes_category', + field=django.contrib.postgres.fields.jsonb.JSONField(help_text='Category of the presence or absence of metastases in regional lymph nodes. Accepted ontologies: SNOMED CT, AJCC and others.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:complex_ontology_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Complex object to combine data value and staging system.', 'properties': {'data_value': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'staging_system': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}}, 'required': ['data_value'], 'title': 'Complex ontology', 'type': 'object'}, formats=['uri'])]), + ), + migrations.AlterField( + model_name='tnmstaging', + name='stage_group', + field=django.contrib.postgres.fields.jsonb.JSONField(help_text='The extent of the cancer in the body, according to the TNM classification system.Accepted ontologies: SNOMED CT, AJCC and others.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:complex_ontology_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Complex object to combine data value and staging system.', 'properties': {'data_value': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'staging_system': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}}, 'required': ['data_value'], 'title': 'Complex ontology', 'type': 'object'}, formats=['uri'])]), + ), + ] diff --git a/chord_metadata_service/patients/migrations/0009_auto_20200514_1541.py b/chord_metadata_service/patients/migrations/0009_auto_20200514_1541.py new file mode 100644 index 000000000..592fa61ff --- /dev/null +++ b/chord_metadata_service/patients/migrations/0009_auto_20200514_1541.py @@ -0,0 +1,35 @@ +# Generated by Django 2.2.12 on 2020-05-14 19:41 + +import chord_metadata_service.restapi.validators +import django.contrib.postgres.fields.jsonb +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('patients', '0008_auto_20200513_1401'), + ] + + operations = [ + migrations.AlterField( + model_name='individual', + name='comorbid_condition', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='One or more conditions that occur with primary condition.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:comorbid_condition_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Comorbid condition schema.', 'properties': {'clinical_status': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'code': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}}, 'required': [], 'title': 'Comorbid Condition schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='individual', + name='ecog_performance_status', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Value representing the Eastern Cooperative Oncology Group performance status.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='individual', + name='karnofsky', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Value representing the Karnofsky Performance status.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='individual', + name='taxonomy', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Ontology resource representing the species (e.g., NCBITaxon:9615).', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + ] diff --git a/chord_metadata_service/phenopackets/migrations/0008_auto_20200514_1541.py b/chord_metadata_service/phenopackets/migrations/0008_auto_20200514_1541.py new file mode 100644 index 000000000..e5b44022c --- /dev/null +++ b/chord_metadata_service/phenopackets/migrations/0008_auto_20200514_1541.py @@ -0,0 +1,101 @@ +# Generated by Django 2.2.12 on 2020-05-14 19:41 + +import chord_metadata_service.restapi.validators +import django.contrib.postgres.fields +import django.contrib.postgres.fields.jsonb +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('phenopackets', '0007_auto_20200513_1401'), + ] + + operations = [ + migrations.AlterField( + model_name='biosample', + name='diagnostic_markers', + field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), blank=True, help_text='A list of ontology terms representing clinically-relevant bio-markers.', null=True, size=None), + ), + migrations.AlterField( + model_name='biosample', + name='histological_diagnosis', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term representing a refinement of the clinical diagnosis. Normal samples could be tagged with NCIT:C38757, representing a negative finding.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='biosample', + name='sampled_tissue', + field=django.contrib.postgres.fields.jsonb.JSONField(help_text='An ontology term describing the tissue from which the specimen was collected. The use of UBERON is recommended.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='biosample', + name='taxonomy', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term specified when more than one organism may be studied. It is advised that codesfrom the NCBI Taxonomy resource are used, e.g. NCBITaxon:9606 for humans.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='biosample', + name='tumor_grade', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term representing the tumour grade. This should be a child term of NCIT:C28076 (Disease Grade Qualifier) or equivalent.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='biosample', + name='tumor_progression', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term representing if the specimen is from a primary tumour, a metastasis, or a recurrence. There are multiple ways of representing this using ontology terms, and the terms chosen will have a specific meaning that is application specific.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='disease', + name='disease_stage', + field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), blank=True, help_text='A list of terms representing the disease stage. Elements should be derived from child terms of NCIT:C28108 (Disease Stage Qualifier) or equivalent hierarchy from another ontology.', null=True, size=None), + ), + migrations.AlterField( + model_name='disease', + name='term', + field=django.contrib.postgres.fields.jsonb.JSONField(help_text="An ontology term that represents the disease. It's recommended that one of the OMIM, Orphanet, or MONDO ontologies is used for rare human diseases.", validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='disease', + name='tnm_finding', + field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), blank=True, help_text='A list of terms representing the tumour TNM score. Elements should be derived from child terms of NCIT:C48232 (Cancer TNM Finding) or equivalent hierarchy from another ontology.', null=True, size=None), + ), + migrations.AlterField( + model_name='phenotypicfeature', + name='modifier', + field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), blank=True, help_text='A list of ontology terms that provide more expressive / precise descriptions of a phenotypic feature, including e.g. positionality or external factors.', null=True, size=None), + ), + migrations.AlterField( + model_name='phenotypicfeature', + name='onset', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term that describes the age at which the phenotypic feature was first noticed or diagnosed, e.g. HP:0003674.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='phenotypicfeature', + name='pftype', + field=django.contrib.postgres.fields.jsonb.JSONField(help_text='An ontology term which describes the phenotype.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)], verbose_name='type'), + ), + migrations.AlterField( + model_name='phenotypicfeature', + name='severity', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term that describes the severity of the condition.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='procedure', + name='body_site', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term that is specified when it is not possible to represent the procedure with a single ontology class.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='procedure', + name='code', + field=django.contrib.postgres.fields.jsonb.JSONField(help_text='An ontology term that represents a clinical procedure performed on a subject.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='variant', + name='allele', + field=django.contrib.postgres.fields.jsonb.JSONField(help_text="The variant's corresponding allele", validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:allele_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'dependencies': {'genome_assembly': ['chr', 'pos', 'ref', 'alt', 'info'], 'seq_id': ['position', 'deleted_sequence', 'inserted_sequence']}, 'description': 'Variant allele types', 'oneOf': [{'required': ['hgvs']}, {'required': ['genome_assembly']}, {'required': ['seq_id']}, {'required': ['iscn']}], 'properties': {'alt': {'description': 'The alternate base(s).', 'help': 'The alternate base(s).', 'type': 'string'}, 'chr': {'description': 'A chromosome identifier e.g. chr2 or 2.', 'help': 'A chromosome identifier e.g. chr2 or 2.', 'type': 'string'}, 'deleted_sequence': {'description': 'Deleted sequence , sequence for the deletion, can be empty, e.g. A', 'help': 'Deleted sequence , sequence for the deletion, can be empty, e.g. A', 'type': 'string'}, 'genome_assembly': {'description': 'The reference genome identifier e.g. GRCh38.', 'help': 'The reference genome identifier e.g. GRCh38.', 'type': 'string'}, 'hgvs': {'description': '', 'help': '', 'type': 'string'}, 'id': {'description': 'An arbitrary identifier.', 'help': 'An arbitrary identifier.', 'type': 'string'}, 'info': {'description': 'Relevant parts of the INFO field.', 'help': 'Relevant parts of the INFO field.', 'type': 'string'}, 'inserted_sequence': {'description': 'Inserted sequence , sequence for the insertion, can be empty, e.g. G', 'help': 'Inserted sequence , sequence for the insertion, can be empty, e.g. G', 'type': 'string'}, 'iscn': {'description': 'E.g. t(8;9;11)(q12;p24;p12).', 'help': 'E.g. t(8;9;11)(q12;p24;p12).', 'type': 'string'}, 'pos': {'description': 'The 1-based genomic position e.g. 134327882.', 'help': 'The 1-based genomic position e.g. 134327882.', 'type': 'integer'}, 'position': {'description': 'Position , a 0-based coordinate for where the Deleted Sequence starts, e.g. 4.', 'help': 'Position , a 0-based coordinate for where the Deleted Sequence starts, e.g. 4.', 'type': 'integer'}, 'ref': {'description': 'The reference base(s).', 'help': 'The reference base(s).', 'type': 'string'}, 'seq_id': {'description': 'Sequence ID, e.g. Seq1.', 'help': 'Sequence ID, e.g. Seq1.', 'type': 'string'}}, 'title': 'Allele schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='variant', + name='zygosity', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term taken from the Genotype Ontology (GENO) representing the zygosity of the variant.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + ] From 2c98674cc9ff66c52a138d09d91dd042d2aa44de Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Thu, 14 May 2020 15:42:20 -0400 Subject: [PATCH 016/190] Fix issues with "true" queries which should return all results --- chord_metadata_service/chord/tests/test_api_search.py | 10 ++++++++++ requirements.txt | 4 ++-- setup.py | 4 ++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/chord_metadata_service/chord/tests/test_api_search.py b/chord_metadata_service/chord/tests/test_api_search.py index 47bb44db1..ba2ccee2d 100644 --- a/chord_metadata_service/chord/tests/test_api_search.py +++ b/chord_metadata_service/chord/tests/test_api_search.py @@ -243,6 +243,16 @@ def test_private_table_search_4(self): self.assertEqual(len(c["results"]), 1) self.assertEqual(self.phenopacket.id, c["results"][0]["id"]) + def test_private_table_search_5(self): + # Valid query: literal "true" + r = self.client.post(reverse("private-table-search", args=[str(self.dataset.identifier)]), data=json.dumps({ + "query": True + }), content_type="application/json") + self.assertEqual(r.status_code, status.HTTP_200_OK) + c = r.json() + self.assertEqual(len(c["results"]), 1) + self.assertEqual(self.phenopacket.id, c["results"][0]["id"]) + @patch('chord_metadata_service.chord.views_search.es') def test_fhir_search(self, mocked_es): mocked_es.search.return_value = SEARCH_SUCCESS diff --git a/requirements.txt b/requirements.txt index 626a3cc4f..0a59ac116 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ attrs==19.3.0 Babel==2.8.0 certifi==2020.4.5.1 chardet==3.0.4 -chord-lib==0.8.0 +chord_lib==0.9.0 codecov==2.0.22 colorama==0.4.3 coreapi==2.3.3 @@ -40,7 +40,7 @@ pytz==2020.1 PyYAML==5.3.1 rdflib==4.2.2 rdflib-jsonld==0.4.0 -redis==3.4.1 +redis==3.5.1 requests==2.23.0 rfc3987==1.3.8 simplejson==3.17.0 diff --git a/setup.py b/setup.py index cc4dc0cb8..d52c7765a 100644 --- a/setup.py +++ b/setup.py @@ -16,8 +16,8 @@ python_requires=">=3.6", install_requires=[ - "chord_lib[django]==0.8.0", - "Django>=2.2,<3.0", + "chord_lib[django]==0.9.0", + "Django>=2.2.12,<3.0", "django-filter>=2.2,<3.0", "django-nose>=1.4,<2.0", "djangorestframework>=3.11,<3.12", From 96857481f40ed22c8dd5b4bb2ce996e9ab636bbf Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Thu, 14 May 2020 19:52:57 -0400 Subject: [PATCH 017/190] Travis debug --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 5df749e32..f15eda29a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,6 +10,7 @@ addons: - postgresql-11 - postgresql-contrib-11 before_install: + - sudo systemctl status postgresql - sudo -u postgres psql -U postgres -p 5432 -d postgres -c "alter user postgres with password 'hj38f3Ntr';" install: - pip install -r requirements.txt From a035e810c5f328d13d292863724357b24d963271 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Thu, 14 May 2020 20:01:01 -0400 Subject: [PATCH 018/190] Travis debug II --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f15eda29a..05981f1b1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,7 @@ addons: - postgresql-11 - postgresql-contrib-11 before_install: - - sudo systemctl status postgresql + - cat /var/log/postgresql/postgresql-11-main.log - sudo -u postgres psql -U postgres -p 5432 -d postgres -c "alter user postgres with password 'hj38f3Ntr';" install: - pip install -r requirements.txt From d794c7a012d11d50aa691d59bd74af3cdf055bf6 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Thu, 14 May 2020 20:08:27 -0400 Subject: [PATCH 019/190] Travis debug III --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 05981f1b1..ae1a4dcf2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,7 @@ addons: - postgresql-11 - postgresql-contrib-11 before_install: - - cat /var/log/postgresql/postgresql-11-main.log + - sudo cat /var/log/postgresql/postgresql-11-main.log - sudo -u postgres psql -U postgres -p 5432 -d postgres -c "alter user postgres with password 'hj38f3Ntr';" install: - pip install -r requirements.txt From 60e9356343e6adfd1b3be36e6d188ec359040fab Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Thu, 14 May 2020 20:40:31 -0400 Subject: [PATCH 020/190] Travis debug IV --- .travis.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index ae1a4dcf2..24fcef9be 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,13 +4,12 @@ python: - "3.6" - "3.8" addons: - postgresql: "11" + postgresql: "12" apt: packages: - - postgresql-11 - - postgresql-contrib-11 + - postgresql-12 + - postgresql-contrib-12 before_install: - - sudo cat /var/log/postgresql/postgresql-11-main.log - sudo -u postgres psql -U postgres -p 5432 -d postgres -c "alter user postgres with password 'hj38f3Ntr';" install: - pip install -r requirements.txt From 97a88f591951c7ed92fb6400eaa87850b1dddbfd Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Thu, 14 May 2020 20:48:50 -0400 Subject: [PATCH 021/190] Travis debug V --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 24fcef9be..21e42c177 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,7 @@ addons: - postgresql-12 - postgresql-contrib-12 before_install: - - sudo -u postgres psql -U postgres -p 5432 -d postgres -c "alter user postgres with password 'hj38f3Ntr';" + - sudo -u postgres psql -U postgres -p 5433 -d postgres -c "alter user postgres with password 'hj38f3Ntr';" install: - pip install -r requirements.txt - pip install . From 7d2a5ba658d34060d9d1ef381200ecacfba1ca9d Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Thu, 14 May 2020 20:51:29 -0400 Subject: [PATCH 022/190] Travis debug VI --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 21e42c177..d4382e253 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,7 @@ install: - pip install -r requirements.txt - pip install . script: - - export POSTGRES_USER="postgres" && export POSTGRES_PASSWORD="hj38f3Ntr" && export POSTGRES_PORT=5432 + - export POSTGRES_USER="postgres" && export POSTGRES_PASSWORD="hj38f3Ntr" && export POSTGRES_PORT=5433 - python3 -m coverage run ./manage.py test - codecov - rm -rf chord_metadata_service From 8b735618b7b9f86fe04682ad3e5bb6cd6927fa70 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Fri, 15 May 2020 10:51:44 -0400 Subject: [PATCH 023/190] clean --- chord_metadata_service/chord/views_ingest.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/chord_metadata_service/chord/views_ingest.py b/chord_metadata_service/chord/views_ingest.py index 139f410e2..027a4217c 100644 --- a/chord_metadata_service/chord/views_ingest.py +++ b/chord_metadata_service/chord/views_ingest.py @@ -159,7 +159,6 @@ def _query_and_check_nulls(obj: dict, key: str, transform: Callable = lambda x: def ingest_phenopacket(phenopacket_data, table_id): """ Ingests one phenopacket. """ - #new_phenopacket_id = str(uuid.uuid4()) # TODO: Is this provided? new_phenopacket_id = phenopacket_data.get("id", str(uuid.uuid4())) subject = phenopacket_data.get("subject", None) @@ -223,7 +222,6 @@ def ingest_phenopacket(phenopacket_data, table_id): term=disease["term"], disease_stage=disease.get("disease_stage", []), tnm_finding=disease.get("tnm_finding", []), - #**_query_and_check_nulls(disease, "onset") onset=disease.get("onset", None) ) diseases_db.append(d_obj.id) From b141d78602eb9e136276d7809612987840a736c4 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Fri, 15 May 2020 16:17:53 -0400 Subject: [PATCH 024/190] Refactor some schema related stuff Factor out some helpers Move ontology search and description schema to restapi app Misc other cleanup --- .../experiments/api_views.py | 1 - .../phenopackets/schemas.py | 47 ++++---- .../phenopackets/search_schemas.py | 106 ++++++------------ .../restapi/schema_utils.py | 35 ++++-- chord_metadata_service/restapi/schemas.py | 11 +- .../restapi/search_schemas.py | 20 ++++ 6 files changed, 110 insertions(+), 110 deletions(-) create mode 100644 chord_metadata_service/restapi/search_schemas.py diff --git a/chord_metadata_service/experiments/api_views.py b/chord_metadata_service/experiments/api_views.py index bf71d7e28..1a9c0eb85 100644 --- a/chord_metadata_service/experiments/api_views.py +++ b/chord_metadata_service/experiments/api_views.py @@ -33,4 +33,3 @@ def get_experiment_schema(_request): Experiment schema """ return Response(EXPERIMENT_SCHEMA) - diff --git a/chord_metadata_service/phenopackets/schemas.py b/chord_metadata_service/phenopackets/schemas.py index c3c8a1cf8..a373ecc4e 100644 --- a/chord_metadata_service/phenopackets/schemas.py +++ b/chord_metadata_service/phenopackets/schemas.py @@ -2,15 +2,18 @@ import chord_metadata_service.phenopackets.descriptions as descriptions from chord_metadata_service.patients.schemas import INDIVIDUAL_SCHEMA -from chord_metadata_service.restapi.description_utils import describe_schema, ONTOLOGY_CLASS as ONTOLOGY_CLASS_DESC +from chord_metadata_service.restapi.description_utils import describe_schema from chord_metadata_service.restapi.schemas import ( - AGE, AGE_RANGE, AGE_OR_AGE_RANGE, ONTOLOGY_CLASS, EXTRA_PROPERTIES_SCHEMA + AGE, + AGE_RANGE, + AGE_OR_AGE_RANGE, + EXTRA_PROPERTIES_SCHEMA, + ONTOLOGY_CLASS, ) __all__ = [ "ALLELE_SCHEMA", - "PHENOPACKET_ONTOLOGY_SCHEMA", "PHENOPACKET_EXTERNAL_REFERENCE_SCHEMA", "PHENOPACKET_RESOURCE_SCHEMA", "PHENOPACKET_UPDATE_SCHEMA", @@ -66,8 +69,6 @@ }, descriptions.ALLELE) -PHENOPACKET_ONTOLOGY_SCHEMA = describe_schema(ONTOLOGY_CLASS, ONTOLOGY_CLASS_DESC) - PHENOPACKET_EXTERNAL_REFERENCE_SCHEMA = describe_schema({ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "chord_metadata_service:external_reference_schema", @@ -175,7 +176,7 @@ "title": "Evidence schema", "type": "object", "properties": { - "evidence_code": PHENOPACKET_ONTOLOGY_SCHEMA, + "evidence_code": ONTOLOGY_CLASS, "reference": PHENOPACKET_EXTERNAL_REFERENCE_SCHEMA }, "additionalProperties": False, @@ -188,16 +189,16 @@ "description": { "type": "string", }, - "type": PHENOPACKET_ONTOLOGY_SCHEMA, + "type": ONTOLOGY_CLASS, "negated": { "type": "boolean", }, - "severity": PHENOPACKET_ONTOLOGY_SCHEMA, + "severity": ONTOLOGY_CLASS, "modifier": { # TODO: Plural? "type": "array", - "items": PHENOPACKET_ONTOLOGY_SCHEMA + "items": ONTOLOGY_CLASS }, - "onset": PHENOPACKET_ONTOLOGY_SCHEMA, + "onset": ONTOLOGY_CLASS, "evidence": PHENOPACKET_EVIDENCE_SCHEMA, "extra_properties": EXTRA_PROPERTIES_SCHEMA }, @@ -255,7 +256,7 @@ "type": "object", # TODO "properties": { "allele": ALLELE_SCHEMA, # TODO - "zygosity": PHENOPACKET_ONTOLOGY_SCHEMA, + "zygosity": ONTOLOGY_CLASS, "extra_properties": EXTRA_PROPERTIES_SCHEMA } }, descriptions.VARIANT) @@ -273,25 +274,25 @@ "description": { "type": "string", }, - "sampled_tissue": PHENOPACKET_ONTOLOGY_SCHEMA, + "sampled_tissue": ONTOLOGY_CLASS, "phenotypic_features": { "type": "array", "items": PHENOPACKET_PHENOTYPIC_FEATURE_SCHEMA, }, - "taxonomy": PHENOPACKET_ONTOLOGY_SCHEMA, + "taxonomy": ONTOLOGY_CLASS, "individual_age_at_collection": AGE_OR_AGE_RANGE, - "histological_diagnosis": PHENOPACKET_ONTOLOGY_SCHEMA, - "tumor_progression": PHENOPACKET_ONTOLOGY_SCHEMA, - "tumor_grade": PHENOPACKET_ONTOLOGY_SCHEMA, # TODO: Is this a list? + "histological_diagnosis": ONTOLOGY_CLASS, + "tumor_progression": ONTOLOGY_CLASS, + "tumor_grade": ONTOLOGY_CLASS, # TODO: Is this a list? "diagnostic_markers": { "type": "array", - "items": PHENOPACKET_ONTOLOGY_SCHEMA, + "items": ONTOLOGY_CLASS, }, "procedure": { "type": "object", "properties": { - "code": PHENOPACKET_ONTOLOGY_SCHEMA, - "body_site": PHENOPACKET_ONTOLOGY_SCHEMA + "code": ONTOLOGY_CLASS, + "body_site": ONTOLOGY_CLASS }, "required": ["code"], }, @@ -321,7 +322,7 @@ "anyOf": [ AGE, AGE_RANGE, - PHENOPACKET_ONTOLOGY_SCHEMA + ONTOLOGY_CLASS ] } @@ -331,15 +332,15 @@ "title": "Disease schema", "type": "object", "properties": { - "term": PHENOPACKET_ONTOLOGY_SCHEMA, + "term": ONTOLOGY_CLASS, "onset": PHENOPACKET_DISEASE_ONSET_SCHEMA, "disease_stage": { "type": "array", - "items": PHENOPACKET_ONTOLOGY_SCHEMA, + "items": ONTOLOGY_CLASS, }, "tnm_finding": { "type": "array", - "items": PHENOPACKET_ONTOLOGY_SCHEMA, + "items": ONTOLOGY_CLASS, }, "extra_properties": EXTRA_PROPERTIES_SCHEMA }, diff --git a/chord_metadata_service/phenopackets/search_schemas.py b/chord_metadata_service/phenopackets/search_schemas.py index 71127bfb5..f2163c43e 100644 --- a/chord_metadata_service/phenopackets/search_schemas.py +++ b/chord_metadata_service/phenopackets/search_schemas.py @@ -1,44 +1,19 @@ from . import models, schemas from chord_metadata_service.patients.schemas import INDIVIDUAL_SCHEMA -from chord_metadata_service.restapi.schema_utils import tag_schema_with_search_properties +from chord_metadata_service.restapi.schema_utils import ( + search_optional_eq, + search_optional_str, + tag_schema_with_search_properties, +) +from chord_metadata_service.restapi.search_schemas import ONTOLOGY_SEARCH_SCHEMA __all__ = [ - "ONTOLOGY_SEARCH_SCHEMA", "EXTERNAL_REFERENCE_SEARCH_SCHEMA", "PHENOPACKET_SEARCH_SCHEMA", ] -def _single_optional_eq_search(order, queryable: str = "all"): - return { - "operations": ["eq"], - "queryable": queryable, - "canNegate": True, - "required": False, - "type": "single", - "order": order - } - - -def _optional_str_search(order, queryable: str = "all"): - return { - "operations": ["eq", "co"], - "queryable": queryable, - "canNegate": True, - "required": False, - "order": order - } - - -def _single_optional_str_search(order, queryable: str = "all"): - return {**_optional_str_search(order, queryable), "type": "single"} - - -def _multiple_optional_str_search(order, queryable: str = "all"): - return {**_optional_str_search(order, queryable), "type": "multiple"} - - # TODO: Rewrite and use def _tag_with_database_attrs(schema: dict, db_attrs: dict): return { @@ -53,29 +28,13 @@ def _tag_with_database_attrs(schema: dict, db_attrs: dict): } -ONTOLOGY_SEARCH_SCHEMA = tag_schema_with_search_properties(schemas.PHENOPACKET_ONTOLOGY_SCHEMA, { - "properties": { - "id": { - "search": _multiple_optional_str_search(0) - }, - "label": { - "search": _multiple_optional_str_search(1) - } - }, - "search": { - "database": { - "type": "jsonb" # TODO: parameterize? - } - } -}) - EXTERNAL_REFERENCE_SEARCH_SCHEMA = tag_schema_with_search_properties(schemas.PHENOPACKET_EXTERNAL_REFERENCE_SCHEMA, { "properties": { "id": { - "search": _single_optional_str_search(0) + "search": search_optional_str(0) }, "description": { - "search": _multiple_optional_str_search(1) # TODO: Searchable? may leak + "search": search_optional_str(1, multiple=True) # TODO: Searchable? may leak } }, "search": { @@ -89,7 +48,7 @@ def _tag_with_database_attrs(schema: dict, db_attrs: dict): "properties": { "id": { "search": { - **_single_optional_eq_search(0, queryable="internal"), + **search_optional_eq(0, queryable="internal"), "database": { "field": models.Individual._meta.pk.column } @@ -97,7 +56,7 @@ def _tag_with_database_attrs(schema: dict, db_attrs: dict): }, "alternate_ids": { "items": { - "search": _multiple_optional_str_search(0, queryable="internal") + "search": search_optional_str(0, queryable="internal", multiple=True) }, "search": { "database": { @@ -107,14 +66,15 @@ def _tag_with_database_attrs(schema: dict, db_attrs: dict): }, "date_of_birth": { # TODO: Internal? - "search": _single_optional_eq_search(1, queryable="internal") + # TODO: Allow lt / gt + "search": search_optional_eq(1, queryable="internal") }, # TODO: Age "sex": { - "search": _single_optional_eq_search(2) + "search": search_optional_eq(2) }, "karyotypic_sex": { - "search": _single_optional_eq_search(3) + "search": search_optional_eq(3) }, "taxonomy": ONTOLOGY_SEARCH_SCHEMA, }, @@ -129,22 +89,22 @@ def _tag_with_database_attrs(schema: dict, db_attrs: dict): RESOURCE_SEARCH_SCHEMA = tag_schema_with_search_properties(schemas.PHENOPACKET_RESOURCE_SCHEMA, { "properties": { "id": { - "search": _single_optional_str_search(0) + "search": search_optional_str(0) }, "name": { - "search": _multiple_optional_str_search(1) + "search": search_optional_str(1, multiple=True) }, "namespace_prefix": { - "search": _multiple_optional_str_search(2) + "search": search_optional_str(2, multiple=True) }, "url": { - "search": _multiple_optional_str_search(3) + "search": search_optional_str(3, multiple=True) }, "version": { - "search": _multiple_optional_str_search(4) + "search": search_optional_str(4, multiple=True) }, "iri_prefix": { - "search": _multiple_optional_str_search(5) + "search": search_optional_str(5, multiple=True) } }, "search": { @@ -161,10 +121,10 @@ def _tag_with_database_attrs(schema: dict, db_attrs: dict): "properties": { # TODO: timestamp "updated_by": { - "search": _multiple_optional_str_search(0), + "search": search_optional_str(0, multiple=True), }, "comment": { - "search": _multiple_optional_str_search(1) + "search": search_optional_str(1, multiple=True), } }, "search": { @@ -179,10 +139,10 @@ def _tag_with_database_attrs(schema: dict, db_attrs: dict): "properties": { # TODO: created "created_by": { - "search": _multiple_optional_str_search(0) + "search": search_optional_str(0, multiple=True), }, "submitted_by": { - "search": _multiple_optional_str_search(1) + "search": search_optional_str(1, multiple=True), }, "resources": { "items": RESOURCE_SEARCH_SCHEMA, @@ -233,11 +193,11 @@ def _tag_with_database_attrs(schema: dict, db_attrs: dict): PHENOTYPIC_FEATURE_SEARCH_SCHEMA = tag_schema_with_search_properties(schemas.PHENOPACKET_PHENOTYPIC_FEATURE_SCHEMA, { "properties": { "description": { - "search": _multiple_optional_str_search(0), # TODO: Searchable? may leak + "search": search_optional_str(0, multiple=True), # TODO: Searchable? may leak }, "type": ONTOLOGY_SEARCH_SCHEMA, "negated": { - "search": _single_optional_eq_search(1) + "search": search_optional_eq(1), }, "severity": ONTOLOGY_SEARCH_SCHEMA, "modifier": { # TODO: Plural? @@ -258,15 +218,15 @@ def _tag_with_database_attrs(schema: dict, db_attrs: dict): GENE_SEARCH_SCHEMA = tag_schema_with_search_properties(schemas.PHENOPACKET_GENE_SCHEMA, { "properties": { "id": { - "search": _single_optional_str_search(0) + "search": search_optional_str(0), }, "alternate_ids": { "items": { - "search": _single_optional_str_search(1) + "search": search_optional_str(1), } }, "symbol": { - "search": _single_optional_str_search(2) + "search": search_optional_str(2), } }, }) @@ -286,15 +246,15 @@ def _tag_with_database_attrs(schema: dict, db_attrs: dict): "properties": { "id": { "search": { - **_single_optional_eq_search(0, queryable="internal"), + **search_optional_eq(0, queryable="internal"), "database": {"field": models.Biosample._meta.pk.column} } }, "individual_id": { # TODO: Does this work? - "search": _single_optional_eq_search(1, queryable="internal"), + "search": search_optional_eq(1, queryable="internal"), }, "description": { - "search": _multiple_optional_str_search(2), # TODO: Searchable? may leak + "search": search_optional_str(2, multiple=True), # TODO: Searchable? may leak }, "sampled_tissue": ONTOLOGY_SEARCH_SCHEMA, "phenotypic_features": { @@ -343,7 +303,7 @@ def _tag_with_database_attrs(schema: dict, db_attrs: dict): "items": VARIANT_SEARCH_SCHEMA, # TODO: search? }, "is_control_sample": { - "search": _single_optional_eq_search(1), # TODO: Boolean search + "search": search_optional_eq(1), # TODO: Boolean search }, }, "search": { diff --git a/chord_metadata_service/restapi/schema_utils.py b/chord_metadata_service/restapi/schema_utils.py index 3bf8158f4..88d0070fc 100644 --- a/chord_metadata_service/restapi/schema_utils.py +++ b/chord_metadata_service/restapi/schema_utils.py @@ -1,6 +1,29 @@ -from typing import Optional +from typing import List, Optional -__all__ = ["tag_schema_with_search_properties"] +__all__ = [ + "search_optional_eq", + "search_optional_str", + "tag_schema_with_search_properties", +] + + +def _searchable_field(operations: List[str], order: int, queryable: str = "all", multiple: bool = False): + return { + "operations": operations, + "queryable": queryable, + "canNegate": True, + "required": False, + "order": order, + "type": "multiple" if multiple else "single" + } + + +def search_optional_eq(order: int, queryable: str = "all"): + return _searchable_field(["eq"], order, queryable, multiple=False) + + +def search_optional_str(order: int, queryable: str = "all", multiple: bool = False): + return _searchable_field(["eq", "co"], order, queryable, multiple) def tag_schema_with_search_properties(schema, search_descriptions: Optional[dict]): @@ -38,10 +61,8 @@ def tag_schema_with_search_properties(schema, search_descriptions: Optional[dict def customize_schema(first_typeof: dict, second_typeof: dict, first_property: str, second_property: str, - schema_id: str = None, title: str = None, description: str = None, additional_properties: bool = False, - required=None) -> dict: - if required is None: - required = [] + schema_id: str = None, title: str = None, description: str = None, + additional_properties: bool = False, required=None) -> dict: return { "$schema": "http://json-schema.org/draft-07/schema#", "$id": schema_id, @@ -52,6 +73,6 @@ def customize_schema(first_typeof: dict, second_typeof: dict, first_property: st first_property: first_typeof, second_property: second_typeof }, - "required": required, + "required": required or [], "additionalProperties": additional_properties } diff --git a/chord_metadata_service/restapi/schemas.py b/chord_metadata_service/restapi/schemas.py index ee1e6f529..e53eb393a 100644 --- a/chord_metadata_service/restapi/schemas.py +++ b/chord_metadata_service/restapi/schemas.py @@ -1,5 +1,5 @@ from . import descriptions -from .description_utils import describe_schema, EXTRA_PROPERTIES +from .description_utils import describe_schema, EXTRA_PROPERTIES, ONTOLOGY_CLASS as ONTOLOGY_CLASS_DESC # Individual schemas for validation of JSONField values @@ -19,19 +19,18 @@ ################################ Phenopackets based schemas ################################ -ONTOLOGY_CLASS = { +ONTOLOGY_CLASS = describe_schema({ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "chord_metadata_service:ontology_class_schema", "title": "Ontology class schema", - "description": "Schema to describe terms from ontologies.", "type": "object", "properties": { - "id": {"type": "string", "description": "CURIE style identifier."}, - "label": {"type": "string", "description": "Human-readable class name."} + "id": {"type": "string"}, + "label": {"type": "string"} }, "additionalProperties": False, "required": ["id", "label"] -} +}, ONTOLOGY_CLASS_DESC) ONTOLOGY_CLASS_LIST = { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/chord_metadata_service/restapi/search_schemas.py b/chord_metadata_service/restapi/search_schemas.py new file mode 100644 index 000000000..c7ec5b8e1 --- /dev/null +++ b/chord_metadata_service/restapi/search_schemas.py @@ -0,0 +1,20 @@ +from .schemas import ONTOLOGY_CLASS +from .schema_utils import search_optional_str, tag_schema_with_search_properties + +__all__ = ["ONTOLOGY_SEARCH_SCHEMA"] + +ONTOLOGY_SEARCH_SCHEMA = tag_schema_with_search_properties(ONTOLOGY_CLASS, { + "properties": { + "id": { + "search": search_optional_str(0, multiple=True) + }, + "label": { + "search": search_optional_str(1, multiple=True) + } + }, + "search": { + "database": { + "type": "jsonb" # TODO: parameterize? + } + } +}) From a116543fdbf0493dc5396224b89dec728cc01d78 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Fri, 15 May 2020 16:18:15 -0400 Subject: [PATCH 025/190] Add missing enum for experiment schema --- chord_metadata_service/experiments/schemas.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/chord_metadata_service/experiments/schemas.py b/chord_metadata_service/experiments/schemas.py index ce3f8c27c..7748fd086 100644 --- a/chord_metadata_service/experiments/schemas.py +++ b/chord_metadata_service/experiments/schemas.py @@ -3,6 +3,9 @@ from chord_metadata_service.restapi.schemas import ONTOLOGY_CLASS_LIST, KEY_VALUE_OBJECT +__all__ = ["EXPERIMENT_SCHEMA"] + + EXPERIMENT_SCHEMA = describe_schema({ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "chord_metadata_service:experiment_schema", @@ -27,7 +30,17 @@ }, "experiment_ontology": ONTOLOGY_CLASS_LIST, "molecule": { - "type": "string" + "type": "string", + "enum": [ + "total RNA", + "polyA RNA", + "cytoplasmic RNA", + "nuclear RNA", + "small RNA", + "genomic DNA", + "protein", + "other", + ] }, "molecule_ontology": ONTOLOGY_CLASS_LIST, "library_strategy": { From c747b4314585333e7965238097a833394f2092da Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Fri, 15 May 2020 16:18:43 -0400 Subject: [PATCH 026/190] Add note about experiment model's purpose --- chord_metadata_service/experiments/models.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/chord_metadata_service/experiments/models.py b/chord_metadata_service/experiments/models.py index d557c582c..86cf76f5e 100644 --- a/chord_metadata_service/experiments/models.py +++ b/chord_metadata_service/experiments/models.py @@ -9,6 +9,15 @@ from chord_metadata_service.phenopackets.models import Biosample import chord_metadata_service.experiments.descriptions as d + +__all__ = ["Experiment"] + + +# The experiment class here is primarily designed for *genomic* experiments - thus the need for a biosample ID. If, in +# the future, medical imaging or something which isn't sample-based is desired, it may be best to create a separate +# model for the desired purposes. + + class Experiment(models.Model, IndexableMixin): """ Class to store Experiment information """ From e636a8423c6145c198169399f2c42f72a1001c20 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Fri, 15 May 2020 16:25:46 -0400 Subject: [PATCH 027/190] Add shared file defining phenopacket and experiment data types --- chord_metadata_service/chord/data_types.py | 26 ++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 chord_metadata_service/chord/data_types.py diff --git a/chord_metadata_service/chord/data_types.py b/chord_metadata_service/chord/data_types.py new file mode 100644 index 000000000..3c639f0a1 --- /dev/null +++ b/chord_metadata_service/chord/data_types.py @@ -0,0 +1,26 @@ +from chord_metadata_service.experiments.search_schemas import EXPERIMENT_SEARCH_SCHEMA +from chord_metadata_service.phenopackets.search_schemas import PHENOPACKET_SEARCH_SCHEMA + +__all__ = [ + "DATA_TYPE_EXPERIMENT", + "DATA_TYPE_PHENOPACKET", + "DATA_TYPES", +] + +DATA_TYPE_EXPERIMENT = "experiment" +DATA_TYPE_PHENOPACKET = "phenopacket" + +DATA_TYPES = { + DATA_TYPE_EXPERIMENT: { + "schema": EXPERIMENT_SEARCH_SCHEMA, + "metadata_schema": { + "type": "object", # TODO + }, + }, + DATA_TYPE_PHENOPACKET: { + "schema": PHENOPACKET_SEARCH_SCHEMA, + "metadata_schema": { + "type": "object", # TODO + } + } +} From 28c5a106cbbebedade3ded9b4d7649cd93955752 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Fri, 15 May 2020 16:27:27 -0400 Subject: [PATCH 028/190] Fix some start imports in views_ingest --- chord_metadata_service/chord/views_ingest.py | 37 ++++++++++---------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/chord_metadata_service/chord/views_ingest.py b/chord_metadata_service/chord/views_ingest.py index 041741309..f79c1a08e 100644 --- a/chord_metadata_service/chord/views_ingest.py +++ b/chord_metadata_service/chord/views_ingest.py @@ -12,13 +12,14 @@ from rest_framework.renderers import BaseRenderer from rest_framework.response import Response -from chord_lib.responses.errors import * +from chord_lib.responses import errors from chord_lib.workflows import get_workflow, get_workflow_resource, workflow_exists from typing import Callable from chord_metadata_service.chord.models import * -from chord_metadata_service.phenopackets.models import * +from chord_metadata_service.chord.data_types import DATA_TYPE_PHENOPACKET +from chord_metadata_service.phenopackets import models as pm METADATA_WORKFLOWS = { @@ -67,7 +68,7 @@ def workflow_list(_request): @permission_classes([AllowAny]) def workflow_item(_request, workflow_id): if not workflow_exists(workflow_id, METADATA_WORKFLOWS): - return Response(not_found_error(f"No workflow with ID {workflow_id}"), status=404) + return Response(errors.not_found_error(f"No workflow with ID {workflow_id}"), status=404) return Response(get_workflow(workflow_id, METADATA_WORKFLOWS)) @@ -87,7 +88,7 @@ def workflow_file(_request, workflow_id): def create_phenotypic_feature(pf): - pf_obj = PhenotypicFeature( + pf_obj = pm.PhenotypicFeature( description=pf.get("description", ""), pftype=pf["type"], negated=pf.get("negated", False), @@ -115,12 +116,12 @@ def ingest(request): try: jsonschema.validate(request.data, chord_lib.schemas.chord.CHORD_INGEST_SCHEMA) except jsonschema.exceptions.ValidationError: - return Response(bad_request_error("Invalid ingest request body"), status=400) # TODO: Validation errors + return Response(errors.bad_request_error("Invalid ingest request body"), status=400) # TODO: Validation errors table_id = request.data["table_id"] if not Dataset.objects.filter(identifier=table_id).exists(): - return Response(bad_request_error(f"Table with ID {table_id} does not exist"), status=400) + return Response(errors.bad_request_error(f"Table with ID {table_id} does not exist"), status=400) table_id = str(uuid.UUID(table_id)) # Normalize dataset ID to UUID's str format. @@ -128,10 +129,10 @@ def ingest(request): workflow_outputs = request.data["workflow_outputs"] if not chord_lib.workflows.workflow_exists(workflow_id, METADATA_WORKFLOWS): # Check that the workflow exists - return Response(bad_request_error(f"Workflow with ID {workflow_id} does not exist"), status=400) + return Response(errors.bad_request_error(f"Workflow with ID {workflow_id} does not exist"), status=400) if "json_document" not in workflow_outputs: - return Response(bad_request_error("Missing workflow output 'json_document'"), status=400) + return Response(errors.bad_request_error("Missing workflow output 'json_document'"), status=400) with open(workflow_outputs["json_document"], "r") as jf: try: @@ -143,7 +144,7 @@ def ingest(request): ingest_phenopacket(phenopacket_data, table_id) except json.decoder.JSONDecodeError as e: - return Response(bad_request_error(f"Invalid JSON provided for phenopacket document (message: {e})"), + return Response(errors.bad_request_error(f"Invalid JSON provided for phenopacket document (message: {e})"), status=400) # TODO: Schema validation @@ -172,21 +173,21 @@ def ingest_phenopacket(phenopacket_data, table_id): subject_query = _query_and_check_nulls(subject, "date_of_birth", transform=isoparse) for k in ("alternate_ids", "age", "sex", "karyotypic_sex", "taxonomy"): subject_query.update(_query_and_check_nulls(subject, k)) - subject, _ = Individual.objects.get_or_create(id=subject["id"], **subject_query) + subject, _ = pm.Individual.objects.get_or_create(id=subject["id"], **subject_query) phenotypic_features_db = [create_phenotypic_feature(pf) for pf in phenotypic_features] biosamples_db = [] for bs in biosamples: # TODO: This should probably be a JSON field, or compound key with code/body_site - procedure, _ = Procedure.objects.get_or_create(**bs["procedure"]) + procedure, _ = pm.Procedure.objects.get_or_create(**bs["procedure"]) - bs_query = _query_and_check_nulls(bs, "individual_id", lambda i: Individual.objects.get(id=i)) + bs_query = _query_and_check_nulls(bs, "individual_id", lambda i: pm.Individual.objects.get(id=i)) for k in ("sampled_tissue", "taxonomy", "individual_age_at_collection", "histological_diagnosis", "tumor_progression", "tumor_grade"): bs_query.update(_query_and_check_nulls(bs, k)) - bs_obj, bs_created = Biosample.objects.get_or_create( + bs_obj, bs_created = pm.Biosample.objects.get_or_create( id=bs["id"], description=bs.get("description", ""), procedure=procedure, @@ -208,7 +209,7 @@ def ingest_phenopacket(phenopacket_data, table_id): for g in genes: # TODO: Validate CURIE # TODO: Rename alternate_id - g_obj, _ = Gene.objects.get_or_create( + g_obj, _ = pm.Gene.objects.get_or_create( id=g["id"], alternate_ids=g.get("alternate_ids", []), symbol=g["symbol"] @@ -218,7 +219,7 @@ def ingest_phenopacket(phenopacket_data, table_id): diseases_db = [] for disease in diseases: # TODO: Primary key, should this be a model? - d_obj, _ = Disease.objects.get_or_create( + d_obj, _ = pm.Disease.objects.get_or_create( term=disease["term"], disease_stage=disease.get("disease_stage", []), tnm_finding=disease.get("tnm_finding", []), @@ -228,7 +229,7 @@ def ingest_phenopacket(phenopacket_data, table_id): resources_db = [] for rs in meta_data.get("resources", []): - rs_obj, _ = Resource.objects.get_or_create( + rs_obj, _ = pm.Resource.objects.get_or_create( id=rs["id"], # TODO: This ID is a bit iffy, because they're researcher-provided name=rs["name"], namespace_prefix=rs["namespace_prefix"], @@ -238,7 +239,7 @@ def ingest_phenopacket(phenopacket_data, table_id): ) resources_db.append(rs_obj) - meta_data_obj = MetaData( + meta_data_obj = pm.MetaData( created_by=meta_data["created_by"], submitted_by=meta_data.get("submitted_by", None), phenopacket_schema_version="1.0.0-RC3", @@ -248,7 +249,7 @@ def ingest_phenopacket(phenopacket_data, table_id): meta_data_obj.resources.set(resources_db) # TODO: primary key ??? - new_phenopacket = Phenopacket( + new_phenopacket = pm.Phenopacket( id=new_phenopacket_id, subject=subject, meta_data=meta_data_obj, From c4613f670d1542d79c5cc918efa927508d49f3ec Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Fri, 15 May 2020 16:49:33 -0400 Subject: [PATCH 029/190] change array fields to json fields in phenopackets add remove and add migrations --- .../migrations/0009_auto_20200515_1613.py | 37 +++++++++++++++ .../migrations/0010_auto_20200515_1645.py | 45 +++++++++++++++++++ chord_metadata_service/phenopackets/models.py | 37 ++++++++------- .../restapi/schema_utils.py | 15 ++++++- 4 files changed, 114 insertions(+), 20 deletions(-) create mode 100644 chord_metadata_service/phenopackets/migrations/0009_auto_20200515_1613.py create mode 100644 chord_metadata_service/phenopackets/migrations/0010_auto_20200515_1645.py diff --git a/chord_metadata_service/phenopackets/migrations/0009_auto_20200515_1613.py b/chord_metadata_service/phenopackets/migrations/0009_auto_20200515_1613.py new file mode 100644 index 000000000..d6c2acf42 --- /dev/null +++ b/chord_metadata_service/phenopackets/migrations/0009_auto_20200515_1613.py @@ -0,0 +1,37 @@ +# Generated by Django 2.2.12 on 2020-05-15 20:13 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('phenopackets', '0008_auto_20200514_1541'), + ] + + operations = [ + migrations.RemoveField( + model_name='biosample', + name='diagnostic_markers', + ), + migrations.RemoveField( + model_name='disease', + name='disease_stage', + ), + migrations.RemoveField( + model_name='disease', + name='tnm_finding', + ), + migrations.RemoveField( + model_name='metadata', + name='external_references', + ), + migrations.RemoveField( + model_name='metadata', + name='updates', + ), + migrations.RemoveField( + model_name='phenotypicfeature', + name='modifier', + ), + ] diff --git a/chord_metadata_service/phenopackets/migrations/0010_auto_20200515_1645.py b/chord_metadata_service/phenopackets/migrations/0010_auto_20200515_1645.py new file mode 100644 index 000000000..418ca30d0 --- /dev/null +++ b/chord_metadata_service/phenopackets/migrations/0010_auto_20200515_1645.py @@ -0,0 +1,45 @@ +# Generated by Django 2.2.12 on 2020-05-15 20:45 + +import chord_metadata_service.restapi.validators +import django.contrib.postgres.fields.jsonb +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('phenopackets', '0009_auto_20200515_1613'), + ] + + operations = [ + migrations.AddField( + model_name='biosample', + name='diagnostic_markers', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='A list of ontology terms representing clinically-relevant bio-markers.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)]), + ), + migrations.AddField( + model_name='disease', + name='disease_stage', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='A list of terms representing the disease stage. Elements should be derived from child terms of NCIT:C28108 (Disease Stage Qualifier) or equivalent hierarchy from another ontology.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)]), + ), + migrations.AddField( + model_name='disease', + name='tnm_finding', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='A list of terms representing the tumour TNM score. Elements should be derived from child terms of NCIT:C48232 (Cancer TNM Finding) or equivalent hierarchy from another ontology.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)]), + ), + migrations.AddField( + model_name='metadata', + name='external_references', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='A list of external (non-resource) references.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:schema_list', '$schema': 'http://json-schema.org/draft-07/schema#', 'items': {'$id': 'chord_metadata_service:external_reference_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'An encoding of information about a reference to an external resource.', 'help': 'An encoding of information about a reference to an external resource.', 'properties': {'description': {'description': 'An application-specific free-text description.', 'help': 'An application-specific free-text description.', 'type': 'string'}, 'id': {'description': 'An application-specific identifier. It is RECOMMENDED that this is a CURIE that uniquely identifies the evidence source when combined with a resource; e.g. PMID:123456 with a resource `pmid`. It could also be a URI or other relevant identifier.', 'help': 'An application-specific identifier. It is RECOMMENDED that this is a CURIE that uniquely identifies the evidence source when combined with a resource; e.g. PMID:123456 with a resource `pmid`. It could also be a URI or other relevant identifier.', 'type': 'string'}}, 'required': ['id'], 'title': 'External reference schema', 'type': 'object'}, 'title': 'Schema list', 'type': 'array'}, formats=None)]), + ), + migrations.AddField( + model_name='metadata', + name='updates', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='A list of updates to the phenopacket.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:schema_list', '$schema': 'http://json-schema.org/draft-07/schema#', 'items': {'$id': 'chord_metadata_service:update_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An update event for a record (e.g. a phenopacket.)', 'help': 'An update event for a record (e.g. a phenopacket.)', 'properties': {'comment': {'description': 'Free-text comment about the changes made and/or the reason for the update.', 'help': 'Free-text comment about the changes made and/or the reason for the update.', 'type': 'string'}, 'timestamp': {'description': 'ISO8601 UTC timestamp specifying when when this update occurred.', 'format': 'date-time', 'help': 'Timestamp specifying when when this update occurred.', 'type': 'string'}, 'updated_by': {'description': 'Information about the person/organization/network that performed the update.', 'help': 'Information about the person/organization/network that performed the update.', 'type': 'string'}}, 'required': ['timestamp', 'comment'], 'title': 'Updates schema', 'type': 'object'}, 'title': 'Schema list', 'type': 'array'}, formats=['date-time'])]), + ), + migrations.AddField( + model_name='phenotypicfeature', + name='modifier', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='A list of ontology terms that provide more expressive / precise descriptions of a phenotypic feature, including e.g. positionality or external factors.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)]), + ), + ] diff --git a/chord_metadata_service/phenopackets/models.py b/chord_metadata_service/phenopackets/models.py index 25521bf72..75ab66ded 100644 --- a/chord_metadata_service/phenopackets/models.py +++ b/chord_metadata_service/phenopackets/models.py @@ -3,12 +3,14 @@ from django.core.exceptions import ValidationError from django.contrib.postgres.fields import JSONField, ArrayField from chord_metadata_service.patients.models import Individual +from chord_metadata_service.restapi.schema_utils import schema_list from chord_metadata_service.restapi.description_utils import rec_help from chord_metadata_service.restapi.models import IndexableMixin from chord_metadata_service.restapi.validators import ( JsonSchemaValidator, age_or_age_range_validator, ontology_validator, + ontology_list_validator ) from . import descriptions as d from .schemas import ( @@ -62,15 +64,14 @@ class MetaData(models.Model): created_by = models.CharField(max_length=200, help_text=rec_help(d.META_DATA, "created_by")) submitted_by = models.CharField(max_length=200, blank=True, help_text=rec_help(d.META_DATA, "submitted_by")) resources = models.ManyToManyField(Resource, help_text=rec_help(d.META_DATA, "resources")) - updates = ArrayField( - JSONField(null=True, blank=True, - validators=[JsonSchemaValidator(schema=PHENOPACKET_UPDATE_SCHEMA, formats=['date-time'])]), - blank=True, null=True, help_text=rec_help(d.META_DATA, "updates")) + updates = JSONField(blank=True, null=True, validators=[JsonSchemaValidator( + schema=schema_list(PHENOPACKET_UPDATE_SCHEMA), formats=['date-time'])], + help_text=rec_help(d.META_DATA, "updates")) phenopacket_schema_version = models.CharField(max_length=200, blank=True, help_text='Schema version of the current phenopacket.') - external_references = ArrayField( - JSONField(null=True, blank=True, validators=[JsonSchemaValidator(PHENOPACKET_EXTERNAL_REFERENCE_SCHEMA)]), - blank=True, null=True, help_text=rec_help(d.META_DATA, "external_references")) + external_references = JSONField(blank=True, null=True, validators=[JsonSchemaValidator( + schema=schema_list(PHENOPACKET_EXTERNAL_REFERENCE_SCHEMA))], + help_text=rec_help(d.META_DATA, "external_references")) extra_properties = JSONField(blank=True, null=True, help_text=rec_help(d.META_DATA, "extra_properties")) updated = models.DateTimeField(auto_now_add=True) @@ -101,9 +102,8 @@ class PhenotypicFeature(models.Model, IndexableMixin): negated = 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 = ArrayField( - JSONField(null=True, blank=True, validators=[ontology_validator]), blank=True, null=True, - help_text=rec_help(d.PHENOTYPIC_FEATURE, "modifier")) + modifier = JSONField(blank=True, null=True, validators=[ontology_list_validator], + help_text=rec_help(d.PHENOTYPIC_FEATURE, "modifier")) onset = JSONField(blank=True, null=True, validators=[ontology_validator], help_text=rec_help(d.PHENOTYPIC_FEATURE, "onset")) # evidence can stay here because evidence is given for an observation of PF @@ -188,9 +188,8 @@ class Gene(models.Model): # Gene id is unique id = models.CharField(primary_key=True, max_length=200, help_text=rec_help(d.GENE, "id")) # CURIE style? Yes! - alternate_ids = ArrayField( - models.CharField(max_length=200, blank=True), blank=True, null=True, - help_text=rec_help(d.GENE, "alternate_ids")) + alternate_ids = ArrayField(models.CharField(max_length=200, blank=True), blank=True, null=True, + help_text=rec_help(d.GENE, "alternate_ids")) symbol = models.CharField(max_length=200, help_text=rec_help(d.GENE, "symbol")) extra_properties = JSONField(blank=True, null=True, help_text=rec_help(d.GENE, "extra_properties")) created = models.DateTimeField(auto_now=True) @@ -246,10 +245,10 @@ class Disease(models.Model, IndexableMixin): # } onset = JSONField(blank=True, null=True, validators=[JsonSchemaValidator(schema=PHENOPACKET_DISEASE_ONSET_SCHEMA)], help_text=rec_help(d.DISEASE, "onset")) - disease_stage = ArrayField(JSONField(null=True, blank=True, validators=[ontology_validator]), - blank=True, null=True, help_text=rec_help(d.DISEASE, "disease_stage")) - tnm_finding = ArrayField(JSONField(null=True, blank=True, validators=[ontology_validator]), - blank=True, null=True, help_text=rec_help(d.DISEASE, "tnm_finding")) + disease_stage = JSONField(blank=True, null=True, validators=[ontology_list_validator], + help_text=rec_help(d.DISEASE, "disease_stage")) + tnm_finding = JSONField(blank=True, null=True, validators=[ontology_list_validator], + help_text=rec_help(d.DISEASE, "tnm_finding")) extra_properties = JSONField(blank=True, null=True, help_text=rec_help(d.DISEASE, "extra_properties")) created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) @@ -287,8 +286,8 @@ class Biosample(models.Model, IndexableMixin): help_text=rec_help(d.BIOSAMPLE, "tumor_progression")) tumor_grade = JSONField(blank=True, null=True, validators=[ontology_validator], help_text=rec_help(d.BIOSAMPLE, "tumor_grade")) - diagnostic_markers = ArrayField(JSONField(null=True, blank=True, validators=[ontology_validator]), - blank=True, null=True, help_text=rec_help(d.BIOSAMPLE, "diagnostic_markers")) + diagnostic_markers = JSONField(blank=True, null=True, validators=[ontology_list_validator], + help_text=rec_help(d.BIOSAMPLE, "diagnostic_markers")) # CHECK! if Procedure instance is deleted Biosample instance is deleted too procedure = models.ForeignKey(Procedure, on_delete=models.CASCADE, help_text=rec_help(d.BIOSAMPLE, "procedure")) hts_files = models.ManyToManyField( diff --git a/chord_metadata_service/restapi/schema_utils.py b/chord_metadata_service/restapi/schema_utils.py index 3bf8158f4..f2e7606a3 100644 --- a/chord_metadata_service/restapi/schema_utils.py +++ b/chord_metadata_service/restapi/schema_utils.py @@ -38,7 +38,8 @@ def tag_schema_with_search_properties(schema, search_descriptions: Optional[dict def customize_schema(first_typeof: dict, second_typeof: dict, first_property: str, second_property: str, - schema_id: str = None, title: str = None, description: str = None, additional_properties: bool = False, + schema_id: str = None, title: str = None, description: str = None, + additional_properties: bool = False, required=None) -> dict: if required is None: required = [] @@ -55,3 +56,15 @@ def customize_schema(first_typeof: dict, second_typeof: dict, first_property: st "required": required, "additionalProperties": additional_properties } + + +def schema_list(schema): + """ Schema to validate JSON array values. """ + + return { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "chord_metadata_service:schema_list", + "title": "Schema list", + "type": "array", + "items": schema + } From fa7acf3dd74f42b4ecd4401f92c6610e88756831 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Fri, 15 May 2020 17:21:49 -0400 Subject: [PATCH 030/190] change array fields to json in chord Dataset add remove and add migrations --- .../migrations/0011_auto_20200515_1657.py | 69 +++++++++++++++ .../migrations/0012_auto_20200515_1714.py | 84 +++++++++++++++++++ chord_metadata_service/chord/models.py | 75 ++++++++--------- 3 files changed, 186 insertions(+), 42 deletions(-) create mode 100644 chord_metadata_service/chord/migrations/0011_auto_20200515_1657.py create mode 100644 chord_metadata_service/chord/migrations/0012_auto_20200515_1714.py diff --git a/chord_metadata_service/chord/migrations/0011_auto_20200515_1657.py b/chord_metadata_service/chord/migrations/0011_auto_20200515_1657.py new file mode 100644 index 000000000..c806c1665 --- /dev/null +++ b/chord_metadata_service/chord/migrations/0011_auto_20200515_1657.py @@ -0,0 +1,69 @@ +# Generated by Django 2.2.12 on 2020-05-15 20:57 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('chord', '0010_auto_20200309_1945'), + ] + + operations = [ + migrations.RemoveField( + model_name='dataset', + name='acknowledges', + ), + migrations.RemoveField( + model_name='dataset', + name='alternate_identifiers', + ), + migrations.RemoveField( + model_name='dataset', + name='citations', + ), + migrations.RemoveField( + model_name='dataset', + name='creators', + ), + migrations.RemoveField( + model_name='dataset', + name='dates', + ), + migrations.RemoveField( + model_name='dataset', + name='dimensions', + ), + migrations.RemoveField( + model_name='dataset', + name='distributions', + ), + migrations.RemoveField( + model_name='dataset', + name='keywords', + ), + migrations.RemoveField( + model_name='dataset', + name='licenses', + ), + migrations.RemoveField( + model_name='dataset', + name='linked_field_sets', + ), + migrations.RemoveField( + model_name='dataset', + name='primary_publications', + ), + migrations.RemoveField( + model_name='dataset', + name='related_identifiers', + ), + migrations.RemoveField( + model_name='dataset', + name='spatial_coverage', + ), + migrations.RemoveField( + model_name='dataset', + name='types', + ), + ] diff --git a/chord_metadata_service/chord/migrations/0012_auto_20200515_1714.py b/chord_metadata_service/chord/migrations/0012_auto_20200515_1714.py new file mode 100644 index 000000000..a6b657b29 --- /dev/null +++ b/chord_metadata_service/chord/migrations/0012_auto_20200515_1714.py @@ -0,0 +1,84 @@ +# Generated by Django 2.2.12 on 2020-05-15 21:14 + +import django.contrib.postgres.fields.jsonb +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('chord', '0011_auto_20200515_1657'), + ] + + operations = [ + migrations.AddField( + model_name='dataset', + name='acknowledges', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, help_text='The grant(s) which funded and supported the work reported by the dataset.'), + ), + migrations.AddField( + model_name='dataset', + name='alternate_identifiers', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, help_text='Alternate identifiers for the dataset.'), + ), + migrations.AddField( + model_name='dataset', + name='citations', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, help_text='The publication(s) that cite this dataset.'), + ), + migrations.AddField( + model_name='dataset', + name='creators', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, help_text='The person(s) or organization(s) which contributed to the creation of the dataset.'), + ), + migrations.AddField( + model_name='dataset', + name='dates', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, help_text='Relevant dates for the datasets, a date must be added, e.g. creation date or last modification date should be added.'), + ), + migrations.AddField( + model_name='dataset', + name='dimensions', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, help_text='The different dimensions (granular components) making up a dataset.'), + ), + migrations.AddField( + model_name='dataset', + name='distributions', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, help_text='The distribution(s) by which datasets are made available (for example: mySQL dump).'), + ), + migrations.AddField( + model_name='dataset', + name='keywords', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, help_text='Tags associated with the dataset, which will help in its discovery.'), + ), + migrations.AddField( + model_name='dataset', + name='licenses', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, help_text='The terms of use of the dataset.'), + ), + migrations.AddField( + model_name='dataset', + name='linked_field_sets', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, help_text='Data type fields which are linked together.'), + ), + migrations.AddField( + model_name='dataset', + name='primary_publications', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, help_text='The primary publication(s) associated with the dataset, usually describing how the dataset was produced.'), + ), + migrations.AddField( + model_name='dataset', + name='related_identifiers', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, help_text='Related identifiers for the dataset.'), + ), + migrations.AddField( + model_name='dataset', + name='spatial_coverage', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, help_text='The geographical extension and span covered by the dataset and its measured dimensions/variables.'), + ), + migrations.AddField( + model_name='dataset', + name='types', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, help_text='A term, ideally from a controlled terminology, identifying the dataset type or nature of the data, placing it in a typology.'), + ), + ] diff --git a/chord_metadata_service/chord/models.py b/chord_metadata_service/chord/models.py index 59110e052..7c6f3fa1e 100644 --- a/chord_metadata_service/chord/models.py +++ b/chord_metadata_service/chord/models.py @@ -1,6 +1,6 @@ import uuid -from django.contrib.postgres.fields import JSONField, ArrayField +from django.contrib.postgres.fields import JSONField from django.db import models from django.utils import timezone @@ -51,9 +51,7 @@ class Dataset(models.Model): ) data_use = JSONField() - - linked_field_sets = ArrayField(JSONField(), blank=True, default=list, - help_text="Data type fields which are linked together.") + linked_field_sets = JSONField(blank=True, default=list, help_text="Data type fields which are linked together.") @property def n_of_tables(self): @@ -62,21 +60,17 @@ def n_of_tables(self): # --------------------------- DATS model fields --------------------------- - alternate_identifiers = ArrayField(JSONField(null=True, blank=True), blank=True, null=True, - help_text="Alternate identifiers for the dataset.") - related_identifiers = ArrayField(JSONField(null=True, blank=True), blank=True, null=True, - help_text="Related identifiers for the dataset.") - dates = ArrayField(JSONField(null=True, blank=True), blank=True, null=True, - help_text="Relevant dates for the datasets, a date must be added, e.g. creation date or last " - "modification date should be added.") + alternate_identifiers = JSONField(blank=True, default=list, help_text="Alternate identifiers for the dataset.") + related_identifiers = JSONField(blank=True, default=list, help_text="Related identifiers for the dataset.") + dates = JSONField(blank=True, default=list, help_text="Relevant dates for the datasets, a date must be added, e.g. " + "creation date or last modification date should be added.") # TODO: Can this be auto-synthesized? (Specified in settings) stored_in = JSONField(blank=True, null=True, help_text="The data repository hosting the dataset.") - spatial_coverage = ArrayField(JSONField(null=True, blank=True), blank=True, null=True, - help_text="The geographical extension and span covered by the dataset and its " - "measured dimensions/variables.") - types = ArrayField(JSONField(null=True, blank=True), blank=True, null=True, - help_text="A term, ideally from a controlled terminology, identifying the dataset type or " - "nature of the data, placing it in a typology.") + spatial_coverage = JSONField(blank=True, default=list, help_text="The geographical extension and span covered " + "by the dataset and its measured dimensions/variables.") + types = JSONField(blank=True, default=list, help_text="A term, ideally from a controlled terminology, identifying " + "the dataset type or nature of the data, placing it in a " + "typology.") # TODO: Can this be derived from / combined with DUO stuff? availability = models.CharField(max_length=200, blank=True, help_text="A qualifier indicating the different types of availability for a " @@ -91,40 +85,37 @@ def n_of_tables(self): privacy = models.CharField(max_length=200, blank=True, help_text="A qualifier to describe the data protection applied to the dataset. This is " "relevant for clinical data.") - distributions = ArrayField(JSONField(null=True, blank=True), blank=True, null=True, - help_text="The distribution(s) by which datasets are made available (for example: " - "mySQL dump).") - dimensions = ArrayField(JSONField(null=True, blank=True), blank=True, null=True, - help_text="The different dimensions (granular components) making up a dataset.") - primary_publications = ArrayField(JSONField(null=True, blank=True), blank=True, null=True, - help_text="The primary publication(s) associated with the dataset, usually " - "describing how the dataset was produced.") - citations = ArrayField(JSONField(null=True, blank=True), blank=True, null=True, - help_text="The publication(s) that cite this dataset.") + distributions = JSONField(blank=True, default=list, help_text="The distribution(s) by which datasets are made " + "available (for example: mySQL dump).") + dimensions = JSONField(blank=True, default=list, help_text="The different dimensions (granular components) " + "making up a dataset.") + primary_publications = JSONField(blank=True, default=list, help_text="The primary publication(s) associated with " + "the dataset, usually describing how the " + "dataset was produced.") + citations = JSONField(blank=True, default=list, help_text="The publication(s) that cite this dataset.") citation_count = models.IntegerField(blank=True, null=True, help_text="The number of publications that cite this dataset (enumerated in " "the citations property).") produced_by = JSONField(blank=True, null=True, help_text="A study process which generated a given dataset, if any.") - creators = ArrayField(JSONField(null=True, blank=True), blank=True, null=True, - help_text="The person(s) or organization(s) which contributed to the creation of the " - "dataset.") + creators = JSONField(blank=True, default=list, help_text="The person(s) or organization(s) which contributed to " + "the creation of the dataset.") # TODO: How to reconcile this and data_use? - licenses = ArrayField(JSONField(null=True, blank=True), blank=True, null=True, - help_text="The terms of use of the dataset.") + licenses = JSONField(blank=True, default=list, help_text="The terms of use of the dataset.") # is_about this field will be calculated based on sample field # in tableOwnership - has_part = models.ManyToManyField("self", blank=True, - help_text="A Dataset that is a subset of this Dataset; Datasets declaring the " - "'hasPart' relationship are considered a collection of Datasets, the " - "aggregation criteria could be included in the 'description' field.") - acknowledges = ArrayField(JSONField(null=True, blank=True), blank=True, null=True, - help_text="The grant(s) which funded and supported the work reported by the dataset.") - keywords = ArrayField(JSONField(null=True, blank=True), blank=True, null=True, - help_text="Tags associated with the dataset, which will help in its discovery.") + has_part = models.ManyToManyField("self", blank=True, help_text="A Dataset that is a subset of this Dataset; " + "Datasets declaring the 'hasPart' relationship are " + "considered a collection of Datasets, the " + "aggregation criteria could be included in " + "the 'description' field.") + acknowledges = JSONField(blank=True, default=list, help_text="The grant(s) which funded and supported the work " + "reported by the dataset.") + keywords = JSONField(blank=True, default=list, help_text="Tags associated with the dataset, which will help in " + "its discovery.") version = models.CharField(max_length=200, blank=True, default=version_default, help_text="A release point for the dataset when applicable.") - extra_properties = JSONField(blank=True, null=True, - help_text="Extra properties that do not fit in the previous specified attributes.") + extra_properties = JSONField(blank=True, null=True, help_text="Extra properties that do not fit in the previous " + "specified attributes.") # ------------------------------------------------------------------------- From 07e194ad6c6c401aa56aac4633094f3b0c0eab5f Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Tue, 19 May 2020 11:25:18 -0400 Subject: [PATCH 031/190] Missing merge bits --- chord_metadata_service/chord/models.py | 75 ++++++++++++-------------- 1 file changed, 33 insertions(+), 42 deletions(-) diff --git a/chord_metadata_service/chord/models.py b/chord_metadata_service/chord/models.py index 59110e052..7c6f3fa1e 100644 --- a/chord_metadata_service/chord/models.py +++ b/chord_metadata_service/chord/models.py @@ -1,6 +1,6 @@ import uuid -from django.contrib.postgres.fields import JSONField, ArrayField +from django.contrib.postgres.fields import JSONField from django.db import models from django.utils import timezone @@ -51,9 +51,7 @@ class Dataset(models.Model): ) data_use = JSONField() - - linked_field_sets = ArrayField(JSONField(), blank=True, default=list, - help_text="Data type fields which are linked together.") + linked_field_sets = JSONField(blank=True, default=list, help_text="Data type fields which are linked together.") @property def n_of_tables(self): @@ -62,21 +60,17 @@ def n_of_tables(self): # --------------------------- DATS model fields --------------------------- - alternate_identifiers = ArrayField(JSONField(null=True, blank=True), blank=True, null=True, - help_text="Alternate identifiers for the dataset.") - related_identifiers = ArrayField(JSONField(null=True, blank=True), blank=True, null=True, - help_text="Related identifiers for the dataset.") - dates = ArrayField(JSONField(null=True, blank=True), blank=True, null=True, - help_text="Relevant dates for the datasets, a date must be added, e.g. creation date or last " - "modification date should be added.") + alternate_identifiers = JSONField(blank=True, default=list, help_text="Alternate identifiers for the dataset.") + related_identifiers = JSONField(blank=True, default=list, help_text="Related identifiers for the dataset.") + dates = JSONField(blank=True, default=list, help_text="Relevant dates for the datasets, a date must be added, e.g. " + "creation date or last modification date should be added.") # TODO: Can this be auto-synthesized? (Specified in settings) stored_in = JSONField(blank=True, null=True, help_text="The data repository hosting the dataset.") - spatial_coverage = ArrayField(JSONField(null=True, blank=True), blank=True, null=True, - help_text="The geographical extension and span covered by the dataset and its " - "measured dimensions/variables.") - types = ArrayField(JSONField(null=True, blank=True), blank=True, null=True, - help_text="A term, ideally from a controlled terminology, identifying the dataset type or " - "nature of the data, placing it in a typology.") + spatial_coverage = JSONField(blank=True, default=list, help_text="The geographical extension and span covered " + "by the dataset and its measured dimensions/variables.") + types = JSONField(blank=True, default=list, help_text="A term, ideally from a controlled terminology, identifying " + "the dataset type or nature of the data, placing it in a " + "typology.") # TODO: Can this be derived from / combined with DUO stuff? availability = models.CharField(max_length=200, blank=True, help_text="A qualifier indicating the different types of availability for a " @@ -91,40 +85,37 @@ def n_of_tables(self): privacy = models.CharField(max_length=200, blank=True, help_text="A qualifier to describe the data protection applied to the dataset. This is " "relevant for clinical data.") - distributions = ArrayField(JSONField(null=True, blank=True), blank=True, null=True, - help_text="The distribution(s) by which datasets are made available (for example: " - "mySQL dump).") - dimensions = ArrayField(JSONField(null=True, blank=True), blank=True, null=True, - help_text="The different dimensions (granular components) making up a dataset.") - primary_publications = ArrayField(JSONField(null=True, blank=True), blank=True, null=True, - help_text="The primary publication(s) associated with the dataset, usually " - "describing how the dataset was produced.") - citations = ArrayField(JSONField(null=True, blank=True), blank=True, null=True, - help_text="The publication(s) that cite this dataset.") + distributions = JSONField(blank=True, default=list, help_text="The distribution(s) by which datasets are made " + "available (for example: mySQL dump).") + dimensions = JSONField(blank=True, default=list, help_text="The different dimensions (granular components) " + "making up a dataset.") + primary_publications = JSONField(blank=True, default=list, help_text="The primary publication(s) associated with " + "the dataset, usually describing how the " + "dataset was produced.") + citations = JSONField(blank=True, default=list, help_text="The publication(s) that cite this dataset.") citation_count = models.IntegerField(blank=True, null=True, help_text="The number of publications that cite this dataset (enumerated in " "the citations property).") produced_by = JSONField(blank=True, null=True, help_text="A study process which generated a given dataset, if any.") - creators = ArrayField(JSONField(null=True, blank=True), blank=True, null=True, - help_text="The person(s) or organization(s) which contributed to the creation of the " - "dataset.") + creators = JSONField(blank=True, default=list, help_text="The person(s) or organization(s) which contributed to " + "the creation of the dataset.") # TODO: How to reconcile this and data_use? - licenses = ArrayField(JSONField(null=True, blank=True), blank=True, null=True, - help_text="The terms of use of the dataset.") + licenses = JSONField(blank=True, default=list, help_text="The terms of use of the dataset.") # is_about this field will be calculated based on sample field # in tableOwnership - has_part = models.ManyToManyField("self", blank=True, - help_text="A Dataset that is a subset of this Dataset; Datasets declaring the " - "'hasPart' relationship are considered a collection of Datasets, the " - "aggregation criteria could be included in the 'description' field.") - acknowledges = ArrayField(JSONField(null=True, blank=True), blank=True, null=True, - help_text="The grant(s) which funded and supported the work reported by the dataset.") - keywords = ArrayField(JSONField(null=True, blank=True), blank=True, null=True, - help_text="Tags associated with the dataset, which will help in its discovery.") + has_part = models.ManyToManyField("self", blank=True, help_text="A Dataset that is a subset of this Dataset; " + "Datasets declaring the 'hasPart' relationship are " + "considered a collection of Datasets, the " + "aggregation criteria could be included in " + "the 'description' field.") + acknowledges = JSONField(blank=True, default=list, help_text="The grant(s) which funded and supported the work " + "reported by the dataset.") + keywords = JSONField(blank=True, default=list, help_text="Tags associated with the dataset, which will help in " + "its discovery.") version = models.CharField(max_length=200, blank=True, default=version_default, help_text="A release point for the dataset when applicable.") - extra_properties = JSONField(blank=True, null=True, - help_text="Extra properties that do not fit in the previous specified attributes.") + extra_properties = JSONField(blank=True, null=True, help_text="Extra properties that do not fit in the previous " + "specified attributes.") # ------------------------------------------------------------------------- From 2fc454439d0baedf845079dd699d59bb7241cfb8 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Tue, 19 May 2020 12:10:44 -0400 Subject: [PATCH 032/190] Update experiments model * Remove individual * Default JSON lists to empty list instead of null --- .../experiments/descriptions.py | 1 - .../migrations/0007_auto_20200519_1538.py | 52 +++++++++++++++++++ chord_metadata_service/experiments/models.py | 19 +++---- chord_metadata_service/experiments/schemas.py | 5 +- .../experiments/serializers.py | 3 ++ 5 files changed, 62 insertions(+), 18 deletions(-) create mode 100644 chord_metadata_service/experiments/migrations/0007_auto_20200519_1538.py diff --git a/chord_metadata_service/experiments/descriptions.py b/chord_metadata_service/experiments/descriptions.py index 50963793c..3af447185 100644 --- a/chord_metadata_service/experiments/descriptions.py +++ b/chord_metadata_service/experiments/descriptions.py @@ -23,7 +23,6 @@ "other_fields": "The other fields for the experiment.", "biosample": "Biosample on which this experiment was done.", - "individual": "Donor on which this experiment was done.", **EXTRA_PROPERTIES } diff --git a/chord_metadata_service/experiments/migrations/0007_auto_20200519_1538.py b/chord_metadata_service/experiments/migrations/0007_auto_20200519_1538.py new file mode 100644 index 000000000..b41e643fc --- /dev/null +++ b/chord_metadata_service/experiments/migrations/0007_auto_20200519_1538.py @@ -0,0 +1,52 @@ +# Generated by Django 2.2.12 on 2020-05-19 15:38 + +import chord_metadata_service.restapi.validators +import django.contrib.postgres.fields +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('chord', '0013_table'), + ('experiments', '0006_auto_20200514_1541'), + ] + + operations = [ + migrations.RemoveField( + model_name='experiment', + name='individual', + ), + migrations.AddField( + model_name='experiment', + name='table', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='chord.Table'), + ), + migrations.AlterField( + model_name='experiment', + name='biosample', + field=models.ForeignKey(help_text='Biosample on which this experiment was done.', on_delete=django.db.models.deletion.CASCADE, to='phenopackets.Biosample'), + ), + migrations.AlterField( + model_name='experiment', + name='experiment_ontology', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, help_text='(Ontology: OBI) links to experiment ontology information.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)]), + ), + migrations.AlterField( + model_name='experiment', + name='molecule_ontology', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, help_text='(Ontology: SO) links to molecule ontology information.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)]), + ), + migrations.AlterField( + model_name='experiment', + name='other_fields', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict, help_text='The other fields for the experiment.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:key_value_object_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'The schema represents a key-value object.', 'patternProperties': {'^.*$': {'type': 'string'}}, 'title': 'Key-value object', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='experiment', + name='qc_flags', + field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(help_text='Any quanlity control observations can be noted here. This field can be omitted if empty', max_length=100), blank=True, default=list, size=None), + ), + ] diff --git a/chord_metadata_service/experiments/models.py b/chord_metadata_service/experiments/models.py index 86cf76f5e..dd7caa0e0 100644 --- a/chord_metadata_service/experiments/models.py +++ b/chord_metadata_service/experiments/models.py @@ -1,6 +1,5 @@ from django.db import models from django.db.models import CharField -from django.core.exceptions import ValidationError from django.contrib.postgres.fields import JSONField, ArrayField from chord_metadata_service.restapi.models import IndexableMixin from chord_metadata_service.restapi.description_utils import rec_help @@ -50,28 +49,22 @@ class Experiment(models.Model, IndexableMixin): reference_registry_id = CharField(max_length=30, blank=True, null=True, help_text=rec_help(d.EXPERIMENT, 'reference_registry_id')) qc_flags = ArrayField(CharField(max_length=100, help_text=rec_help(d.EXPERIMENT, 'qc_flags')), - null=True, blank=True, default=list) + blank=True, default=list) experiment_type = CharField(max_length=30, help_text=rec_help(d.EXPERIMENT, 'experiment_type')) - experiment_ontology = JSONField(blank=True, null=True, validators=[ontology_list_validator], + experiment_ontology = JSONField(blank=True, default=list, validators=[ontology_list_validator], help_text=rec_help(d.EXPERIMENT, 'experiment_ontology')) - molecule_ontology = JSONField(blank=True, null=True, validators=[ontology_list_validator], + molecule_ontology = JSONField(blank=True, default=list, validators=[ontology_list_validator], help_text=rec_help(d.EXPERIMENT, 'molecule_ontology')) molecule = CharField(choices=MOLECULE, max_length=20, blank=True, null=True, help_text=rec_help(d.EXPERIMENT, 'molecule')) library_strategy = CharField(choices=LIBRARY_STRATEGY, max_length=25, help_text=rec_help(d.EXPERIMENT, 'library_strategy')) - other_fields = JSONField(blank=True, null=True, validators=[key_value_validator], + other_fields = JSONField(blank=True, default=dict, validators=[key_value_validator], help_text=rec_help(d.EXPERIMENT, 'other_fields')) - biosample = models.ForeignKey(Biosample, on_delete=models.SET_NULL, blank=True, null=True, - help_text=rec_help(d.EXPERIMENT, 'biosample')) - individual = models.ForeignKey(Individual, on_delete=models.SET_NULL, blank=True, null=True, - help_text=rec_help(d.EXPERIMENT, 'individual')) - - def clean(self): - if not (self.biosample or self.individual): - raise ValidationError('Either Biosample or Individual must be specified') + biosample = models.ForeignKey(Biosample, on_delete=models.CASCADE, help_text=rec_help(d.EXPERIMENT, 'biosample')) + table = models.ForeignKey("chord.Table", on_delete=models.CASCADE, blank=True, null=True) # TODO: Help text def __str__(self): return str(self.id) diff --git a/chord_metadata_service/experiments/schemas.py b/chord_metadata_service/experiments/schemas.py index 7748fd086..dc39041c1 100644 --- a/chord_metadata_service/experiments/schemas.py +++ b/chord_metadata_service/experiments/schemas.py @@ -55,16 +55,13 @@ "ChIP-Seq", "RNA-Seq", "miRNA-Seq", - "WGS" + "WGS", ] }, "other_fields": KEY_VALUE_OBJECT, "biosample": { "type": "string" }, - "individual": { - "type": "string" - } }, "required": ["id", "experiment_type", "library_strategy"] }, EXPERIMENT) diff --git a/chord_metadata_service/experiments/serializers.py b/chord_metadata_service/experiments/serializers.py index 22a0d5014..23ac230b6 100644 --- a/chord_metadata_service/experiments/serializers.py +++ b/chord_metadata_service/experiments/serializers.py @@ -2,6 +2,9 @@ from .models import Experiment +__all__ = ["ExperimentSerializer"] + + class ExperimentSerializer(GenericSerializer): class Meta: model = Experiment From 3b54a74df0df8e503f2e329cfa23b5e640c0c881 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Tue, 19 May 2020 12:11:01 -0400 Subject: [PATCH 033/190] Add experiments search schemas --- .../experiments/search_schemas.py | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 chord_metadata_service/experiments/search_schemas.py diff --git a/chord_metadata_service/experiments/search_schemas.py b/chord_metadata_service/experiments/search_schemas.py new file mode 100644 index 000000000..ebc4e2676 --- /dev/null +++ b/chord_metadata_service/experiments/search_schemas.py @@ -0,0 +1,55 @@ +from . import models, schemas +from chord_metadata_service.restapi.schema_utils import ( + search_optional_eq, + search_optional_str, + tag_schema_with_search_properties, +) +from chord_metadata_service.restapi.search_schemas import ONTOLOGY_SEARCH_SCHEMA + + +__all__ = ["EXPERIMENT_SEARCH_SCHEMA"] + + +EXPERIMENT_SEARCH_SCHEMA = tag_schema_with_search_properties(schemas.EXPERIMENT_SCHEMA, { + "properties": { + "id": { + "search": {"database": {"field": models.Experiment._meta.pk.column}} + }, + "reference_registry_id": { + "search": search_optional_str(0, queryable="internal"), + }, + "qc_flags": { + "items": { + "search": search_optional_str(0), + }, + "search": {"database": {"type": "array"}} + }, + "experiment_type": { + "search": search_optional_str(1, queryable="internal"), + }, + "experiment_ontology": { + "items": ONTOLOGY_SEARCH_SCHEMA, # TODO: Specific ontology? + "search": {"database": {"type": "jsonb"}} + }, + "molecule": { + "search": search_optional_eq(2), + }, + "molecule_ontology": { + "items": ONTOLOGY_SEARCH_SCHEMA, # TODO: Specific ontology? + "search": {"database": {"type": "jsonb"}} + }, + "library_strategy": { + "search": search_optional_eq(3), + }, + # TODO: other_fields: ? + "biosample": { + "search": search_optional_eq(4, queryable="internal"), + }, + }, + "search": { + "database": { + "relation": models.Experiment._meta.db_table, + "primary_key": models.Experiment._meta.pk.column, + } + } +}) From 68e93090afd011618abcfbb7d1a44200b7462287 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Tue, 19 May 2020 12:11:21 -0400 Subject: [PATCH 034/190] Add a missing migration file for mcode from merge --- .../migrations/0005_auto_20200519_1538.py | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 chord_metadata_service/mcode/migrations/0005_auto_20200519_1538.py diff --git a/chord_metadata_service/mcode/migrations/0005_auto_20200519_1538.py b/chord_metadata_service/mcode/migrations/0005_auto_20200519_1538.py new file mode 100644 index 000000000..bee3527e6 --- /dev/null +++ b/chord_metadata_service/mcode/migrations/0005_auto_20200519_1538.py @@ -0,0 +1,130 @@ +# Generated by Django 2.2.12 on 2020-05-19 15:38 + +import chord_metadata_service.restapi.validators +import django.contrib.postgres.fields.jsonb +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('mcode', '0004_auto_20200514_1541'), + ] + + operations = [ + migrations.AlterField( + model_name='cancercondition', + name='body_location_code', + field=django.contrib.postgres.fields.jsonb.JSONField(help_text='Code for the body location, optionally pre-coordinating laterality or direction. Accepted ontologies: SNOMED CT, ICD-O-3 and others.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)]), + ), + migrations.AlterField( + model_name='cancercondition', + name='clinical_status', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='A flag indicating whether the condition is active or inactive, recurring, in remission, or resolved (as of the last update of the Condition). Accepted code system: http://terminology.hl7.org/CodeSystem/condition-clinical', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='cancercondition', + name='condition_code', + field=django.contrib.postgres.fields.jsonb.JSONField(help_text='A code describing the type of primary or secondary malignant neoplastic disease.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='cancercondition', + name='histology_morphology_behavior', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='A description of the morphologic and behavioral characteristics of the cancer. Accepted ontologies: SNOMED CT, ICD-O-3 and others.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='cancerrelatedprocedure', + name='code', + field=django.contrib.postgres.fields.jsonb.JSONField(help_text='Code for the procedure performed.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='cancerrelatedprocedure', + name='target_body_site', + field=django.contrib.postgres.fields.jsonb.JSONField(help_text='The body location(s) where the procedure was performed.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)]), + ), + migrations.AlterField( + model_name='cancerrelatedprocedure', + name='treatment_intent', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The purpose of a treatment.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='geneticvariantfound', + name='genomic_source_class', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology or controlled vocabulary term to identify the genomic class of the specimen being analyzed.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='geneticvariantfound', + name='method', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology or controlled vocabulary term to identify the method used to perform the genetic test. Accepted value set: NCIT.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='geneticvariantfound', + name='variant_found_identifier', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The variation ID assigned by HGVS, for example, 360448 is the identifier for NM_005228.4(EGFR):c.-237A>G (single nucleotide variant in EGFR). Accepted value set: ClinVar.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='geneticvarianttested', + name='data_value', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology or controlled vocabulary term to identify positive or negative value forthe mutation. Accepted value set: SNOMED CT.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='geneticvarianttested', + name='method', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology or controlled vocabulary term to identify the method used to perform the genetic test. Accepted value set: NCIT.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='geneticvarianttested', + name='variant_tested_identifier', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The variation ID assigned by HGVS, for example, 360448 is the identifier for NM_005228.4(EGFR):c.-237A>G (single nucleotide variant in EGFR).', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='genomicsreport', + name='specimen_type', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology or controlled vocabulary term to identify the type of material the specimen contains or consists of. Accepted value set: HL7 Version 2 and Specimen Type.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='genomicsreport', + name='test_name', + field=django.contrib.postgres.fields.jsonb.JSONField(help_text='An ontology or controlled vocabulary term to identify the laboratory test. Accepted value sets: LOINC, GTR.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='labsvital', + name='tumor_marker_test', + field=django.contrib.postgres.fields.jsonb.JSONField(help_text='An ontology or controlled vocabulary term to identify tumor marker test.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:tumor_marker_test', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Tumor marker test schema.', 'properties': {'code': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'data_value': {'anyOf': [{'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, {'$id': 'chord_metadata_service:quantity_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema for the datatype Quantity.', 'properties': {'code': {'type': 'string'}, 'comparator': {'enum': ['<', '>', '<=', '>=', '=']}, 'system': {'format': 'uri', 'type': 'string'}, 'unit': {'type': 'string'}, 'value': {'type': 'number'}}, 'title': 'Quantity schema', 'type': 'object'}, {'$id': 'chord_metadata_service:ratio', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Ratio schema.', 'properties': {'denominator': {'$id': 'chord_metadata_service:quantity_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema for the datatype Quantity.', 'properties': {'code': {'type': 'string'}, 'comparator': {'enum': ['<', '>', '<=', '>=', '=']}, 'system': {'format': 'uri', 'type': 'string'}, 'unit': {'type': 'string'}, 'value': {'type': 'number'}}, 'title': 'Quantity schema', 'type': 'object'}, 'numerator': {'$id': 'chord_metadata_service:quantity_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema for the datatype Quantity.', 'properties': {'code': {'type': 'string'}, 'comparator': {'enum': ['<', '>', '<=', '>=', '=']}, 'system': {'format': 'uri', 'type': 'string'}, 'unit': {'type': 'string'}, 'value': {'type': 'number'}}, 'title': 'Quantity schema', 'type': 'object'}}, 'title': 'Ratio', 'type': 'object'}]}}, 'required': ['code'], 'title': 'Tumor marker test', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='medicationstatement', + name='medication_code', + field=django.contrib.postgres.fields.jsonb.JSONField(help_text='A code for medication. Accepted code systems: Medication Clinical Drug (RxNorm) and other.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='medicationstatement', + name='termination_reason', + field=django.contrib.postgres.fields.jsonb.JSONField(help_text='A code explaining unplanned or premature termination of a course of medication. Accepted ontologies: SNOMED CT.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)]), + ), + migrations.AlterField( + model_name='medicationstatement', + name='treatment_intent', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The purpose of a treatment. Accepted ontologies: SNOMED CT.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='tnmstaging', + name='distant_metastases_category', + field=django.contrib.postgres.fields.jsonb.JSONField(help_text='Category describing the presence or absence of metastases in remote anatomical locations. Accepted ontologies: SNOMED CT, AJCC and others.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:complex_ontology_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Complex object to combine data value and staging system.', 'properties': {'data_value': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'staging_system': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}}, 'required': ['data_value'], 'title': 'Complex ontology', 'type': 'object'}, formats=['uri'])]), + ), + migrations.AlterField( + model_name='tnmstaging', + name='primary_tumor_category', + field=django.contrib.postgres.fields.jsonb.JSONField(help_text='Category of the primary tumor, based on its size and extent. Accepted ontologies: SNOMED CT, AJCC and others.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:complex_ontology_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Complex object to combine data value and staging system.', 'properties': {'data_value': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'staging_system': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}}, 'required': ['data_value'], 'title': 'Complex ontology', 'type': 'object'}, formats=['uri'])]), + ), + migrations.AlterField( + model_name='tnmstaging', + name='regional_nodes_category', + field=django.contrib.postgres.fields.jsonb.JSONField(help_text='Category of the presence or absence of metastases in regional lymph nodes. Accepted ontologies: SNOMED CT, AJCC and others.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:complex_ontology_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Complex object to combine data value and staging system.', 'properties': {'data_value': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'staging_system': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}}, 'required': ['data_value'], 'title': 'Complex ontology', 'type': 'object'}, formats=['uri'])]), + ), + migrations.AlterField( + model_name='tnmstaging', + name='stage_group', + field=django.contrib.postgres.fields.jsonb.JSONField(help_text='The extent of the cancer in the body, according to the TNM classification system.Accepted ontologies: SNOMED CT, AJCC and others.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:complex_ontology_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Complex object to combine data value and staging system.', 'properties': {'data_value': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'staging_system': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}}, 'required': ['data_value'], 'title': 'Complex ontology', 'type': 'object'}, formats=['uri'])]), + ), + ] From ec9d4252cc21e7b0de9dc8a2b4c7bb876eb66f4e Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Tue, 19 May 2020 12:11:50 -0400 Subject: [PATCH 035/190] Add missing entries to restapi schema_utils __all__ --- chord_metadata_service/restapi/schema_utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/chord_metadata_service/restapi/schema_utils.py b/chord_metadata_service/restapi/schema_utils.py index e5fe590c1..edc9e1337 100644 --- a/chord_metadata_service/restapi/schema_utils.py +++ b/chord_metadata_service/restapi/schema_utils.py @@ -4,6 +4,8 @@ "search_optional_eq", "search_optional_str", "tag_schema_with_search_properties", + "customize_schema", + "schema_list", ] From d2bb26bfadd24b3e87a378390063859108afa87d Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Tue, 19 May 2020 12:13:07 -0400 Subject: [PATCH 036/190] Split apart table concept from dataset in CHORD service --- chord_metadata_service/chord/api_views.py | 11 +++++--- .../chord/migrations/0013_table.py | 24 +++++++++++++++++ .../migrations/0014_remove_table_data_type.py | 17 ++++++++++++ .../chord/migrations/0015_table_data_type.py | 19 +++++++++++++ chord_metadata_service/chord/models.py | 27 ++++++++++++++++--- chord_metadata_service/chord/serializers.py | 10 ++++++- chord_metadata_service/phenopackets/models.py | 2 +- 7 files changed, 102 insertions(+), 8 deletions(-) create mode 100644 chord_metadata_service/chord/migrations/0013_table.py create mode 100644 chord_metadata_service/chord/migrations/0014_remove_table_data_type.py create mode 100644 chord_metadata_service/chord/migrations/0015_table_data_type.py diff --git a/chord_metadata_service/chord/api_views.py b/chord_metadata_service/chord/api_views.py index d8e914efd..9fff4d5ee 100644 --- a/chord_metadata_service/chord/api_views.py +++ b/chord_metadata_service/chord/api_views.py @@ -4,12 +4,12 @@ from chord_metadata_service.restapi.api_renderers import PhenopacketsRenderer, JSONLDDatasetRenderer, RDFDatasetRenderer from chord_metadata_service.restapi.pagination import LargeResultsSetPagination -from .models import * +from .models import Project, Dataset, TableOwnership, Table from .permissions import OverrideOrSuperUserOnly -from .serializers import * +from .serializers import ProjectSerializer, DatasetSerializer, TableOwnershipSerializer, TableSerializer -__all__ = ["ProjectViewSet", "DatasetViewSet", "TableOwnershipViewSet"] +__all__ = ["ProjectViewSet", "DatasetViewSet", "TableOwnershipViewSet", "TableViewSet"] class ReadOnly(BasePermission): @@ -66,3 +66,8 @@ class TableOwnershipViewSet(CHORDPublicModelViewSet): queryset = TableOwnership.objects.all().order_by("table_id") serializer_class = TableOwnershipSerializer + + +class TableViewSet(CHORDPublicModelViewSet): + queryset = Table.objects.all().order_by("ownership_record_id") + serializer_class = TableSerializer diff --git a/chord_metadata_service/chord/migrations/0013_table.py b/chord_metadata_service/chord/migrations/0013_table.py new file mode 100644 index 000000000..90e4c95ac --- /dev/null +++ b/chord_metadata_service/chord/migrations/0013_table.py @@ -0,0 +1,24 @@ +# Generated by Django 2.2.12 on 2020-05-19 15:38 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('chord', '0012_auto_20200515_1714'), + ] + + operations = [ + migrations.CreateModel( + name='Table', + fields=[ + ('ownership_record', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='chord.TableOwnership')), + ('name', models.CharField(max_length=200, unique=True)), + ('data_type', models.CharField(choices=[('experiment', 'experiment'), ('phenopacket', 'phenopacket')], max_length=30)), + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ], + ), + ] diff --git a/chord_metadata_service/chord/migrations/0014_remove_table_data_type.py b/chord_metadata_service/chord/migrations/0014_remove_table_data_type.py new file mode 100644 index 000000000..eac87843f --- /dev/null +++ b/chord_metadata_service/chord/migrations/0014_remove_table_data_type.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.12 on 2020-05-19 15:54 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('chord', '0013_table'), + ] + + operations = [ + migrations.RemoveField( + model_name='table', + name='data_type', + ), + ] diff --git a/chord_metadata_service/chord/migrations/0015_table_data_type.py b/chord_metadata_service/chord/migrations/0015_table_data_type.py new file mode 100644 index 000000000..b532892f4 --- /dev/null +++ b/chord_metadata_service/chord/migrations/0015_table_data_type.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.12 on 2020-05-19 15:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('chord', '0014_remove_table_data_type'), + ] + + operations = [ + migrations.AddField( + model_name='table', + name='data_type', + field=models.CharField(choices=[('experiment', 'experiment'), ('phenopacket', 'phenopacket')], default='phenopacket', max_length=30), + preserve_default=False, + ), + ] diff --git a/chord_metadata_service/chord/models.py b/chord_metadata_service/chord/models.py index 7c6f3fa1e..5b9dddaee 100644 --- a/chord_metadata_service/chord/models.py +++ b/chord_metadata_service/chord/models.py @@ -4,8 +4,10 @@ from django.db import models from django.utils import timezone +from .data_types import DATA_TYPE_EXPERIMENT, DATA_TYPE_PHENOPACKET -__all__ = ["Project", "Dataset", "TableOwnership"] + +__all__ = ["Project", "Dataset", "TableOwnership", "Table"] def version_default(): @@ -67,7 +69,8 @@ def n_of_tables(self): # TODO: Can this be auto-synthesized? (Specified in settings) stored_in = JSONField(blank=True, null=True, help_text="The data repository hosting the dataset.") spatial_coverage = JSONField(blank=True, default=list, help_text="The geographical extension and span covered " - "by the dataset and its measured dimensions/variables.") + "by the dataset and its measured " + "dimensions/variables.") types = JSONField(blank=True, default=list, help_text="A term, ideally from a controlled terminology, identifying " "the dataset type or nature of the data, placing it in a " "typology.") @@ -135,7 +138,7 @@ class TableOwnership(models.Model): table_id = models.CharField(primary_key=True, max_length=200) service_id = models.UUIDField(max_length=200) service_artifact = models.CharField(max_length=200, default="") - data_type = models.CharField(max_length=200) # TODO: Is this needed? + data_type = models.CharField(max_length=200) # TODO: Is this needed? TODO: Remove # Delete table ownership upon project/dataset deletion dataset = models.ForeignKey(Dataset, on_delete=models.CASCADE, related_name='table_ownership') @@ -144,3 +147,21 @@ class TableOwnership(models.Model): def __str__(self): return f"{self.dataset if not self.sample else self.sample} -> {self.table_id}" + + +class Table(models.Model): + TABLE_DATA_TYPE_CHOICES = ( + (DATA_TYPE_EXPERIMENT, DATA_TYPE_EXPERIMENT), + (DATA_TYPE_PHENOPACKET, DATA_TYPE_PHENOPACKET), + ) + + ownership_record = models.OneToOneField(TableOwnership, on_delete=models.CASCADE, primary_key=True) + name = models.CharField(max_length=200, unique=True) + data_type = models.CharField(max_length=30, choices=TABLE_DATA_TYPE_CHOICES) + + created = models.DateTimeField(auto_now_add=True) + updated = models.DateTimeField(auto_now=True) + + @property + def identifier(self): + return self.ownership_record_id diff --git a/chord_metadata_service/chord/serializers.py b/chord_metadata_service/chord/serializers.py index 492583a79..9f2aff7d1 100644 --- a/chord_metadata_service/chord/serializers.py +++ b/chord_metadata_service/chord/serializers.py @@ -5,7 +5,7 @@ from chord_metadata_service.restapi.dats_schemas import get_dats_schema, CREATORS from chord_metadata_service.restapi.utils import transform_keys -from .models import * +from .models import Project, Dataset, TableOwnership, Table from .schemas import LINKED_FIELD_SETS_SCHEMA @@ -25,6 +25,14 @@ class Meta: fields = '__all__' +class TableSerializer(GenericSerializer): + identifier = serializers.CharField(read_only=True) + + class Meta: + model = Table + fields = "__all__" + + class DatasetSerializer(GenericSerializer): always_include = ( "description", diff --git a/chord_metadata_service/phenopackets/models.py b/chord_metadata_service/phenopackets/models.py index 75ab66ded..091bacfa1 100644 --- a/chord_metadata_service/phenopackets/models.py +++ b/chord_metadata_service/phenopackets/models.py @@ -333,7 +333,7 @@ class Phenopacket(models.Model, IndexableMixin): hts_files = models.ManyToManyField(HtsFile, blank=True, help_text=rec_help(d.PHENOPACKET, "hts_files")) # TODO OneToOneField meta_data = models.ForeignKey(MetaData, on_delete=models.CASCADE, help_text=rec_help(d.PHENOPACKET, "meta_data")) - dataset = models.ForeignKey("chord.Dataset", on_delete=models.CASCADE, blank=True, null=True) + table = models.ForeignKey("chord.Table", on_delete=models.CASCADE, blank=True, null=True) # TODO: Help text extra_properties = JSONField(blank=True, null=True, help_text=rec_help(d.PHENOPACKET, "extra_properties")) created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) From dc786f23ce6261ff4744f73046ec33801d134b66 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Tue, 19 May 2020 12:13:54 -0400 Subject: [PATCH 037/190] Add missing patient, phenopacket migrations Default alternate_ids to list --- .../migrations/0010_auto_20200519_1538.py | 35 ++++++ .../migrations/0011_auto_20200519_1538.py | 112 ++++++++++++++++++ chord_metadata_service/phenopackets/models.py | 4 +- 3 files changed, 149 insertions(+), 2 deletions(-) create mode 100644 chord_metadata_service/patients/migrations/0010_auto_20200519_1538.py create mode 100644 chord_metadata_service/phenopackets/migrations/0011_auto_20200519_1538.py diff --git a/chord_metadata_service/patients/migrations/0010_auto_20200519_1538.py b/chord_metadata_service/patients/migrations/0010_auto_20200519_1538.py new file mode 100644 index 000000000..79a8fa34f --- /dev/null +++ b/chord_metadata_service/patients/migrations/0010_auto_20200519_1538.py @@ -0,0 +1,35 @@ +# Generated by Django 2.2.12 on 2020-05-19 15:38 + +import chord_metadata_service.restapi.validators +import django.contrib.postgres.fields.jsonb +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('patients', '0009_auto_20200514_1541'), + ] + + operations = [ + migrations.AlterField( + model_name='individual', + name='comorbid_condition', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='One or more conditions that occur with primary condition.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:comorbid_condition_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Comorbid condition schema.', 'properties': {'clinical_status': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'code': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}}, 'required': [], 'title': 'Comorbid Condition schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='individual', + name='ecog_performance_status', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Value representing the Eastern Cooperative Oncology Group performance status.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='individual', + name='karnofsky', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Value representing the Karnofsky Performance status.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='individual', + name='taxonomy', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Ontology resource representing the species (e.g., NCBITaxon:9615).', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + ] diff --git a/chord_metadata_service/phenopackets/migrations/0011_auto_20200519_1538.py b/chord_metadata_service/phenopackets/migrations/0011_auto_20200519_1538.py new file mode 100644 index 000000000..29216d0b8 --- /dev/null +++ b/chord_metadata_service/phenopackets/migrations/0011_auto_20200519_1538.py @@ -0,0 +1,112 @@ +# Generated by Django 2.2.12 on 2020-05-19 15:38 + +import chord_metadata_service.restapi.validators +import django.contrib.postgres.fields +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('chord', '0013_table'), + ('phenopackets', '0010_auto_20200515_1645'), + ] + + operations = [ + migrations.RemoveField( + model_name='phenopacket', + name='dataset', + ), + migrations.AddField( + model_name='phenopacket', + name='table', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='chord.Table'), + ), + migrations.AlterField( + model_name='biosample', + name='diagnostic_markers', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='A list of ontology terms representing clinically-relevant bio-markers.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)]), + ), + migrations.AlterField( + model_name='biosample', + name='histological_diagnosis', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term representing a refinement of the clinical diagnosis. Normal samples could be tagged with NCIT:C38757, representing a negative finding.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='biosample', + name='sampled_tissue', + field=django.contrib.postgres.fields.jsonb.JSONField(help_text='An ontology term describing the tissue from which the specimen was collected. The use of UBERON is recommended.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='biosample', + name='taxonomy', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term specified when more than one organism may be studied. It is advised that codesfrom the NCBI Taxonomy resource are used, e.g. NCBITaxon:9606 for humans.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='biosample', + name='tumor_grade', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term representing the tumour grade. This should be a child term of NCIT:C28076 (Disease Grade Qualifier) or equivalent.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='biosample', + name='tumor_progression', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term representing if the specimen is from a primary tumour, a metastasis, or a recurrence. There are multiple ways of representing this using ontology terms, and the terms chosen will have a specific meaning that is application specific.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='disease', + name='disease_stage', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='A list of terms representing the disease stage. Elements should be derived from child terms of NCIT:C28108 (Disease Stage Qualifier) or equivalent hierarchy from another ontology.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)]), + ), + migrations.AlterField( + model_name='disease', + name='term', + field=django.contrib.postgres.fields.jsonb.JSONField(help_text="An ontology term that represents the disease. It's recommended that one of the OMIM, Orphanet, or MONDO ontologies is used for rare human diseases.", validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='disease', + name='tnm_finding', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='A list of terms representing the tumour TNM score. Elements should be derived from child terms of NCIT:C48232 (Cancer TNM Finding) or equivalent hierarchy from another ontology.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)]), + ), + migrations.AlterField( + model_name='gene', + name='alternate_ids', + field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=200), blank=True, default=list, help_text='A list of identifiers for alternative resources where the gene is used or catalogued.', size=None), + ), + migrations.AlterField( + model_name='phenotypicfeature', + name='modifier', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='A list of ontology terms that provide more expressive / precise descriptions of a phenotypic feature, including e.g. positionality or external factors.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)]), + ), + migrations.AlterField( + model_name='phenotypicfeature', + name='onset', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term that describes the age at which the phenotypic feature was first noticed or diagnosed, e.g. HP:0003674.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='phenotypicfeature', + name='pftype', + field=django.contrib.postgres.fields.jsonb.JSONField(help_text='An ontology term which describes the phenotype.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)], verbose_name='type'), + ), + migrations.AlterField( + model_name='phenotypicfeature', + name='severity', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term that describes the severity of the condition.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='procedure', + name='body_site', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term that is specified when it is not possible to represent the procedure with a single ontology class.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='procedure', + name='code', + field=django.contrib.postgres.fields.jsonb.JSONField(help_text='An ontology term that represents a clinical procedure performed on a subject.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AlterField( + model_name='variant', + name='zygosity', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term taken from the Genotype Ontology (GENO) representing the zygosity of the variant.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + ] diff --git a/chord_metadata_service/phenopackets/models.py b/chord_metadata_service/phenopackets/models.py index 091bacfa1..072e7957e 100644 --- a/chord_metadata_service/phenopackets/models.py +++ b/chord_metadata_service/phenopackets/models.py @@ -3,9 +3,9 @@ from django.core.exceptions import ValidationError from django.contrib.postgres.fields import JSONField, ArrayField from chord_metadata_service.patients.models import Individual -from chord_metadata_service.restapi.schema_utils import schema_list from chord_metadata_service.restapi.description_utils import rec_help from chord_metadata_service.restapi.models import IndexableMixin +from chord_metadata_service.restapi.schema_utils import schema_list from chord_metadata_service.restapi.validators import ( JsonSchemaValidator, age_or_age_range_validator, @@ -188,7 +188,7 @@ class Gene(models.Model): # Gene id is unique id = models.CharField(primary_key=True, max_length=200, help_text=rec_help(d.GENE, "id")) # CURIE style? Yes! - alternate_ids = ArrayField(models.CharField(max_length=200, blank=True), blank=True, null=True, + alternate_ids = ArrayField(models.CharField(max_length=200, blank=True), blank=True, default=list, help_text=rec_help(d.GENE, "alternate_ids")) symbol = models.CharField(max_length=200, help_text=rec_help(d.GENE, "symbol")) extra_properties = JSONField(blank=True, null=True, help_text=rec_help(d.GENE, "extra_properties")) From 7c403ac0adc6ee0b10304277aa0f8e7779812735 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Tue, 19 May 2020 12:14:27 -0400 Subject: [PATCH 038/190] Make table APIs more generic Add search, ingest for experiments Split apart ingest and views_ingest Misc other refactoring --- chord_metadata_service/chord/ingest.py | 240 +++++++++++++++ chord_metadata_service/chord/urls.py | 27 ++ chord_metadata_service/chord/views_ingest.py | 193 +----------- chord_metadata_service/chord/views_search.py | 276 ++++++++++-------- .../chord/workflows/experiments_json.wdl | 17 ++ chord_metadata_service/metadata/urls.py | 42 +-- chord_metadata_service/restapi/urls.py | 9 +- 7 files changed, 474 insertions(+), 330 deletions(-) create mode 100644 chord_metadata_service/chord/ingest.py create mode 100644 chord_metadata_service/chord/urls.py create mode 100644 chord_metadata_service/chord/workflows/experiments_json.wdl diff --git a/chord_metadata_service/chord/ingest.py b/chord_metadata_service/chord/ingest.py new file mode 100644 index 000000000..285108a2a --- /dev/null +++ b/chord_metadata_service/chord/ingest.py @@ -0,0 +1,240 @@ +import os +import uuid + +from dateutil.parser import isoparse +from typing import Callable + +from chord_metadata_service.chord.data_types import DATA_TYPE_EXPERIMENT, DATA_TYPE_PHENOPACKET +from chord_metadata_service.chord.models import Table +from chord_metadata_service.experiments import models as em +from chord_metadata_service.phenopackets import models as pm + + +__all__ = [ + "METADATA_WORKFLOWS", + "WORKFLOWS_PATH", + "DATA_TYPE_INGEST_FUNCTION_MAP", +] + + +METADATA_WORKFLOWS = { + "ingestion": { + "phenopackets_json": { + "name": "Bento Phenopackets-Compatible JSON", + "description": "This ingestion workflow will validate and import a Phenopackets schema-compatible " + "JSON document.", + "data_type": "phenopacket", + "file": "phenopackets_json.wdl", + "inputs": [ + { + "id": "json_document", + "type": "file", + "extensions": [".json"] + } + ], + "outputs": [ + { + "id": "json_document", + "type": "file", + "value": "{json_document}" + } + ] + }, + "experiments_json": { + "name": "Bento Experiments JSON", + "description": "This ingestion workflow will validate and import a Bento Experiments schema-compatible " + "JSON document.", + "data_type": "experiment", + "file": "experiments_json.wdl", + "inputs": [ + { + "id": "json_document", + "type": "file", + "extensions": [".json"] + } + ], + "outputs": [ + { + "id": "json_document", + "type": "file", + "value": "{json_document}" + } + ] + } + }, + "analysis": {} +} + +WORKFLOWS_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), "workflows") + + +def create_phenotypic_feature(pf): + pf_obj = pm.PhenotypicFeature( + description=pf.get("description", ""), + pftype=pf["type"], + negated=pf.get("negated", False), + severity=pf.get("severity", None), + modifier=pf.get("modifier", []), # TODO: Validate ontology term in schema... + onset=pf.get("onset", None), + evidence=pf.get("evidence", None) # TODO: Separate class? + ) + + pf_obj.save() + return pf_obj + + +def _query_and_check_nulls(obj: dict, key: str, transform: Callable = lambda x: x): + value = obj.get(key, None) + return {f"{key}__isnull": True} if value is None else {key: transform(value)} + + +def ingest_experiment(experiment_data, table_id) -> em.Experiment: + """Ingests a single experiment.""" + + new_experiment_id = experiment_data["id"] # TODO: Is this provided? + + reference_registry_id = experiment_data.get("reference_registry_id") + qc_flags = experiment_data.get("qc_flags", []) + experiment_type = experiment_data["experiment_type"] + experiment_ontology = experiment_data.get("experiment_ontology", []) + molecule_ontology = experiment_data.get("molecule_ontology", []) + molecule = experiment_data.get("molecule") + library_strategy = experiment_data["library_strategy"] + other_fields = experiment_data.get("other_fields", {}) + biosample = experiment_data.get("biosample") + + if biosample is not None: + biosample = pm.Biosample.objects.get(id=biosample) # TODO: Handle error nicer + + new_experiment = em.Experiment.objects.create( + id=new_experiment_id, + reference_registry_id=reference_registry_id, + qc_flags=qc_flags, + experiment_type=experiment_type, + experiment_ontology=experiment_ontology, + molecule_ontology=molecule_ontology, + molecule=molecule, + library_strategy=library_strategy, + other_fields=other_fields, + biosample=biosample, + table=Table.objects.get(ownership_record_id=table_id, data_type=DATA_TYPE_EXPERIMENT) + ) + + return new_experiment + + +def ingest_phenopacket(phenopacket_data, table_id) -> pm.Phenopacket: + """Ingests a single phenopacket.""" + + new_phenopacket_id = phenopacket_data.get("id", str(uuid.uuid4())) + + subject = phenopacket_data.get("subject", None) + phenotypic_features = phenopacket_data.get("phenotypic_features", []) + biosamples = phenopacket_data.get("biosamples", []) + genes = phenopacket_data.get("genes", []) + diseases = phenopacket_data.get("diseases", []) + meta_data = phenopacket_data["meta_data"] + + if subject: + # Be a bit flexible with the subject date_of_birth field for Signature; convert blank strings to None. + subject["date_of_birth"] = subject.get("date_of_birth") or None + subject_query = _query_and_check_nulls(subject, "date_of_birth", transform=isoparse) + for k in ("alternate_ids", "age", "sex", "karyotypic_sex", "taxonomy"): + subject_query.update(_query_and_check_nulls(subject, k)) + subject, _ = pm.Individual.objects.get_or_create(id=subject["id"], **subject_query) + + phenotypic_features_db = [create_phenotypic_feature(pf) for pf in phenotypic_features] + + biosamples_db = [] + for bs in biosamples: + # TODO: This should probably be a JSON field, or compound key with code/body_site + procedure, _ = pm.Procedure.objects.get_or_create(**bs["procedure"]) + + bs_query = _query_and_check_nulls(bs, "individual_id", lambda i: pm.Individual.objects.get(id=i)) + for k in ("sampled_tissue", "taxonomy", "individual_age_at_collection", "histological_diagnosis", + "tumor_progression", "tumor_grade"): + bs_query.update(_query_and_check_nulls(bs, k)) + + bs_obj, bs_created = pm.Biosample.objects.get_or_create( + id=bs["id"], + description=bs.get("description", ""), + procedure=procedure, + is_control_sample=bs.get("is_control_sample", False), + diagnostic_markers=bs.get("diagnostic_markers", []), + **bs_query + ) + + if bs_created: + bs_pfs = [create_phenotypic_feature(pf) for pf in bs.get("phenotypic_features", [])] + bs_obj.phenotypic_features.set(bs_pfs) + + # TODO: Update phenotypic features otherwise? + + biosamples_db.append(bs_obj) + + # TODO: May want to augment alternate_ids + genes_db = [] + for g in genes: + # TODO: Validate CURIE + # TODO: Rename alternate_id + g_obj, _ = pm.Gene.objects.get_or_create( + id=g["id"], + alternate_ids=g.get("alternate_ids", []), + symbol=g["symbol"] + ) + genes_db.append(g_obj) + + diseases_db = [] + for disease in diseases: + # TODO: Primary key, should this be a model? + d_obj, _ = pm.Disease.objects.get_or_create( + term=disease["term"], + disease_stage=disease.get("disease_stage", []), + tnm_finding=disease.get("tnm_finding", []), + **_query_and_check_nulls(disease, "onset") + ) + diseases_db.append(d_obj.id) + + resources_db = [] + for rs in meta_data.get("resources", []): + rs_obj, _ = pm.Resource.objects.get_or_create( + id=rs["id"], # TODO: This ID is a bit iffy, because they're researcher-provided + name=rs["name"], + namespace_prefix=rs["namespace_prefix"], + url=rs["url"], + version=rs["version"], + iri_prefix=rs["iri_prefix"] + ) + resources_db.append(rs_obj) + + meta_data_obj = pm.MetaData( + created_by=meta_data["created_by"], + submitted_by=meta_data.get("submitted_by", None), + phenopacket_schema_version="1.0.0-RC3", + external_references=meta_data.get("external_references", []) + ) + meta_data_obj.save() + + meta_data_obj.resources.set(resources_db) # TODO: primary key ??? + + new_phenopacket = pm.Phenopacket( + id=new_phenopacket_id, + subject=subject, + meta_data=meta_data_obj, + table=Table.objects.get(ownership_record_id=table_id, data_type=DATA_TYPE_PHENOPACKET) + ) + + new_phenopacket.save() + + new_phenopacket.phenotypic_features.set(phenotypic_features_db) + new_phenopacket.biosamples.set(biosamples_db) + new_phenopacket.genes.set(genes_db) + new_phenopacket.diseases.set(diseases_db) + + return new_phenopacket + + +DATA_TYPE_INGEST_FUNCTION_MAP = { + DATA_TYPE_EXPERIMENT: ingest_experiment, + DATA_TYPE_PHENOPACKET: ingest_phenopacket, +} diff --git a/chord_metadata_service/chord/urls.py b/chord_metadata_service/chord/urls.py new file mode 100644 index 000000000..64190a594 --- /dev/null +++ b/chord_metadata_service/chord/urls.py @@ -0,0 +1,27 @@ +from django.urls import path + +from . import views_ingest, views_search + +urlpatterns = [ + path('workflows', views_ingest.workflow_list, name="workflows"), + path('workflows/', views_ingest.workflow_item, name="workflow-detail"), + path('workflows/.wdl', views_ingest.workflow_file, name="workflow-file"), + + path('private/ingest', views_ingest.ingest, name="ingest"), + + path('data-types', views_search.data_type_list, name="data-type-list"), + path('data-types/', views_search.data_type_detail, name="data-type-detail"), + path('data-types//schema', views_search.data_type_schema, name="data-type-schema"), + # TODO: Consistent snake or kebab + path('data-types//metadata_schema', views_search.data_type_metadata_schema, + name="data-type-metadata-schema"), + path('tables', views_search.table_list, name="chord-table-list"), + path('tables/', views_search.table_detail, name="chord-table-detail"), + path('tables//summary', views_search.chord_table_summary, name="table-summary"), + path('tables//search', views_search.chord_public_table_search, name="public-table-search"), + path('search', views_search.chord_search, name="search"), + path('fhir-search', views_search.fhir_public_search, name="fhir-search"), + path('private/fhir-search', views_search.fhir_private_search, name="fhir-private-search"), + path('private/search', views_search.chord_private_search, name="private-search"), + path('private/tables//search', views_search.chord_private_table_search, name="private-table-search"), +] diff --git a/chord_metadata_service/chord/views_ingest.py b/chord_metadata_service/chord/views_ingest.py index f79c1a08e..5175c575f 100644 --- a/chord_metadata_service/chord/views_ingest.py +++ b/chord_metadata_service/chord/views_ingest.py @@ -1,53 +1,20 @@ -import chord_lib import json import jsonschema import jsonschema.exceptions import os import uuid -from dateutil.parser import isoparse - from rest_framework.decorators import api_view, permission_classes, renderer_classes from rest_framework.permissions import AllowAny from rest_framework.renderers import BaseRenderer from rest_framework.response import Response +from chord_lib.schemas.chord import CHORD_INGEST_SCHEMA from chord_lib.responses import errors from chord_lib.workflows import get_workflow, get_workflow_resource, workflow_exists -from typing import Callable - -from chord_metadata_service.chord.models import * -from chord_metadata_service.chord.data_types import DATA_TYPE_PHENOPACKET -from chord_metadata_service.phenopackets import models as pm - - -METADATA_WORKFLOWS = { - "ingestion": { - "phenopackets_json": { - "name": "Phenopackets-Compatible JSON", - "description": "This ingestion workflow will validate and import a Phenopackets schema-compatible " - "JSON document.", - "data_type": "phenopacket", - "file": "phenopackets_json.wdl", - "inputs": [ - { - "id": "json_document", - "type": "file", - "extensions": [".json"] - } - ], - "outputs": [ - { - "id": "json_document", - "type": "file", - "value": "{json_document}" - } - ] - }, - }, - "analysis": {} -} +from chord_metadata_service.chord.ingest import METADATA_WORKFLOWS, WORKFLOWS_PATH, DATA_TYPE_INGEST_FUNCTION_MAP +from chord_metadata_service.chord.models import Table class WDLRenderer(BaseRenderer): @@ -80,28 +47,11 @@ def workflow_file(_request, workflow_id): if not workflow_exists(workflow_id, METADATA_WORKFLOWS): return Response(status=404, data="Not found") - wdl_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "workflows", - get_workflow_resource(workflow_id, METADATA_WORKFLOWS)) - + wdl_path = os.path.join(WORKFLOWS_PATH, get_workflow_resource(workflow_id, METADATA_WORKFLOWS)) with open(wdl_path, "r") as wf: return Response(wf.read()) -def create_phenotypic_feature(pf): - pf_obj = pm.PhenotypicFeature( - description=pf.get("description", ""), - pftype=pf["type"], - negated=pf.get("negated", False), - severity=pf.get("severity", None), - modifier=pf.get("modifier", []), # TODO: Validate ontology term in schema... - onset=pf.get("onset", None), - evidence=pf.get("evidence", None) # TODO: Separate class? - ) - - pf_obj.save() - return pf_obj - - # Mounted on /private/, so will get protected anyway; this allows for access from WES # TODO: Ugly and misleading permissions @api_view(["POST"]) @@ -114,13 +64,13 @@ def ingest(request): # not be optimal...) try: - jsonschema.validate(request.data, chord_lib.schemas.chord.CHORD_INGEST_SCHEMA) + jsonschema.validate(request.data, CHORD_INGEST_SCHEMA) except jsonschema.exceptions.ValidationError: return Response(errors.bad_request_error("Invalid ingest request body"), status=400) # TODO: Validation errors table_id = request.data["table_id"] - if not Dataset.objects.filter(identifier=table_id).exists(): + if not Table.objects.filter(ownership_record_id=table_id).exists(): return Response(errors.bad_request_error(f"Table with ID {table_id} does not exist"), status=400) table_id = str(uuid.UUID(table_id)) # Normalize dataset ID to UUID's str format. @@ -128,139 +78,28 @@ def ingest(request): workflow_id = request.data["workflow_id"].strip() workflow_outputs = request.data["workflow_outputs"] - if not chord_lib.workflows.workflow_exists(workflow_id, METADATA_WORKFLOWS): # Check that the workflow exists + if not workflow_exists(workflow_id, METADATA_WORKFLOWS): # Check that the workflow exists return Response(errors.bad_request_error(f"Workflow with ID {workflow_id} does not exist"), status=400) + workflow = get_workflow(workflow_id, METADATA_WORKFLOWS) + if "json_document" not in workflow_outputs: return Response(errors.bad_request_error("Missing workflow output 'json_document'"), status=400) with open(workflow_outputs["json_document"], "r") as jf: try: - phenopacket_data = json.load(jf) - if isinstance(phenopacket_data, list): - for item in phenopacket_data: - ingest_phenopacket(item, table_id) + json_data = json.load(jf) + ingest_fn = DATA_TYPE_INGEST_FUNCTION_MAP[workflow["data_type"]] + if isinstance(json_data, list): + for obj in json_data: + ingest_fn(obj, table_id) else: - ingest_phenopacket(phenopacket_data, table_id) + ingest_fn(json_data, table_id) except json.decoder.JSONDecodeError as e: - return Response(errors.bad_request_error(f"Invalid JSON provided for phenopacket document (message: {e})"), + return Response(errors.bad_request_error(f"Invalid JSON provided for ingest document (message: {e})"), status=400) # TODO: Schema validation # TODO: Rollback in case of failures return Response(status=204) - - -def _query_and_check_nulls(obj: dict, key: str, transform: Callable = lambda x: x): - value = obj.get(key, None) - return {f"{key}__isnull": True} if value is None else {key: transform(value)} - - -def ingest_phenopacket(phenopacket_data, table_id): - """ Ingests one phenopacket. """ - - new_phenopacket_id = str(uuid.uuid4()) # TODO: Is this provided? - - subject = phenopacket_data.get("subject", None) - phenotypic_features = phenopacket_data.get("phenotypic_features", []) - biosamples = phenopacket_data.get("biosamples", []) - genes = phenopacket_data.get("genes", []) - diseases = phenopacket_data.get("diseases", []) - meta_data = phenopacket_data["meta_data"] - - if subject: - subject_query = _query_and_check_nulls(subject, "date_of_birth", transform=isoparse) - for k in ("alternate_ids", "age", "sex", "karyotypic_sex", "taxonomy"): - subject_query.update(_query_and_check_nulls(subject, k)) - subject, _ = pm.Individual.objects.get_or_create(id=subject["id"], **subject_query) - - phenotypic_features_db = [create_phenotypic_feature(pf) for pf in phenotypic_features] - - biosamples_db = [] - for bs in biosamples: - # TODO: This should probably be a JSON field, or compound key with code/body_site - procedure, _ = pm.Procedure.objects.get_or_create(**bs["procedure"]) - - bs_query = _query_and_check_nulls(bs, "individual_id", lambda i: pm.Individual.objects.get(id=i)) - for k in ("sampled_tissue", "taxonomy", "individual_age_at_collection", "histological_diagnosis", - "tumor_progression", "tumor_grade"): - bs_query.update(_query_and_check_nulls(bs, k)) - - bs_obj, bs_created = pm.Biosample.objects.get_or_create( - id=bs["id"], - description=bs.get("description", ""), - procedure=procedure, - is_control_sample=bs.get("is_control_sample", False), - diagnostic_markers=bs.get("diagnostic_markers", []), - **bs_query - ) - - if bs_created: - bs_pfs = [create_phenotypic_feature(pf) for pf in bs.get("phenotypic_features", [])] - bs_obj.phenotypic_features.set(bs_pfs) - - # TODO: Update phenotypic features otherwise? - - biosamples_db.append(bs_obj) - - # TODO: May want to augment alternate_ids - genes_db = [] - for g in genes: - # TODO: Validate CURIE - # TODO: Rename alternate_id - g_obj, _ = pm.Gene.objects.get_or_create( - id=g["id"], - alternate_ids=g.get("alternate_ids", []), - symbol=g["symbol"] - ) - genes_db.append(g_obj) - - diseases_db = [] - for disease in diseases: - # TODO: Primary key, should this be a model? - d_obj, _ = pm.Disease.objects.get_or_create( - term=disease["term"], - disease_stage=disease.get("disease_stage", []), - tnm_finding=disease.get("tnm_finding", []), - **_query_and_check_nulls(disease, "onset") - ) - diseases_db.append(d_obj.id) - - resources_db = [] - for rs in meta_data.get("resources", []): - rs_obj, _ = pm.Resource.objects.get_or_create( - id=rs["id"], # TODO: This ID is a bit iffy, because they're researcher-provided - name=rs["name"], - namespace_prefix=rs["namespace_prefix"], - url=rs["url"], - version=rs["version"], - iri_prefix=rs["iri_prefix"] - ) - resources_db.append(rs_obj) - - meta_data_obj = pm.MetaData( - created_by=meta_data["created_by"], - submitted_by=meta_data.get("submitted_by", None), - phenopacket_schema_version="1.0.0-RC3", - external_references=meta_data.get("external_references", []) - ) - meta_data_obj.save() - - meta_data_obj.resources.set(resources_db) # TODO: primary key ??? - - new_phenopacket = pm.Phenopacket( - id=new_phenopacket_id, - subject=subject, - meta_data=meta_data_obj, - dataset=Dataset.objects.get(identifier=table_id) - ) - - new_phenopacket.save() - - new_phenopacket.phenotypic_features.set(phenotypic_features_db) - new_phenopacket.biosamples.set(biosamples_db) - new_phenopacket.genes.set(genes_db) - new_phenopacket.diseases.set(diseases_db) - - return new_phenopacket diff --git a/chord_metadata_service/chord/views_search.py b/chord_metadata_service/chord/views_search.py index bede263dd..bd3f243c4 100644 --- a/chord_metadata_service/chord/views_search.py +++ b/chord_metadata_service/chord/views_search.py @@ -8,74 +8,79 @@ from rest_framework.decorators import api_view, permission_classes from rest_framework.permissions import AllowAny from rest_framework.response import Response +from typing import Any, Callable, Dict from chord_lib.responses import errors from chord_lib.search import build_search_response, postgres +from chord_metadata_service.experiments.models import Experiment +from chord_metadata_service.experiments.serializers import ExperimentSerializer +from chord_metadata_service.metadata.elastic import es from chord_metadata_service.metadata.settings import DEBUG from chord_metadata_service.patients.models import Individual from chord_metadata_service.phenopackets.api_views import PHENOPACKET_PREFETCH from chord_metadata_service.phenopackets.models import Phenopacket -from chord_metadata_service.phenopackets.search_schemas import PHENOPACKET_SEARCH_SCHEMA from chord_metadata_service.phenopackets.serializers import PhenopacketSerializer -from chord_metadata_service.metadata.elastic import es -from .models import Dataset +from .data_types import DATA_TYPE_EXPERIMENT, DATA_TYPE_PHENOPACKET, DATA_TYPES +from .models import Table from .permissions import OverrideOrSuperUserOnly -PHENOPACKET_DATA_TYPE_ID = "phenopacket" - -PHENOPACKET_METADATA_SCHEMA = { - "type": "object" - # TODO -} - @api_view(["GET"]) @permission_classes([AllowAny]) def data_type_list(_request): - return Response([{"id": PHENOPACKET_DATA_TYPE_ID, "schema": PHENOPACKET_SEARCH_SCHEMA}]) + return Response([ + {"id": DATA_TYPE_EXPERIMENT, "schema": DATA_TYPES[DATA_TYPE_EXPERIMENT]["schema"]}, + {"id": DATA_TYPE_PHENOPACKET, "schema": DATA_TYPES[DATA_TYPE_PHENOPACKET]["schema"]}, + ]) @api_view(["GET"]) @permission_classes([AllowAny]) -def data_type_phenopacket(_request): - return Response({ - "id": PHENOPACKET_DATA_TYPE_ID, - "schema": PHENOPACKET_SEARCH_SCHEMA, - "metadata_schema": PHENOPACKET_METADATA_SCHEMA - }) +def data_type_detail(_request, data_type: str): + if data_type not in DATA_TYPES: + return Response(errors.not_found_error(f"Date type {data_type} not found"), status=404) + + return Response({"id": data_type, **DATA_TYPES[data_type]}) @api_view(["GET"]) @permission_classes([AllowAny]) -def data_type_phenopacket_schema(_request): - return Response(PHENOPACKET_SEARCH_SCHEMA) +def data_type_schema(_request, data_type: str): + if data_type not in DATA_TYPES: + return Response(errors.not_found_error(f"Date type {data_type} not found"), status=404) + + return Response(DATA_TYPES[data_type]["schema"]) @api_view(["GET"]) @permission_classes([AllowAny]) -def data_type_phenopacket_metadata_schema(_request): - return Response(PHENOPACKET_METADATA_SCHEMA) +def data_type_metadata_schema(_request, data_type: str): + if data_type not in DATA_TYPES: + return Response(errors.not_found_error(f"Date type {data_type} not found"), status=404) + + return Response(DATA_TYPES[DATA_TYPE_PHENOPACKET]["metadata_schema"]) @api_view(["GET"]) @permission_classes([AllowAny]) def table_list(request): data_types = request.query_params.getlist("data-type") - if PHENOPACKET_DATA_TYPE_ID not in data_types: - return Response(errors.bad_request_error(f"Missing or invalid data type (Specified: {data_types})"), status=400) + + if len(data_types) == 0 or next((dt for dt in data_types if dt not in DATA_TYPES), None) is not None: + return Response(errors.bad_request_error(f"Missing or invalid data type(s) (Specified: {data_types})"), + status=400) return Response([{ - "id": d.identifier, - "name": d.title, + "id": t.identifier, + "name": t.name, "metadata": { - "description": d.description, - "project_id": d.project_id, - "created": d.created.isoformat(), - "updated": d.updated.isoformat() + "dataset_id": t.ownership_record.dataset_id, + "created": t.created.isoformat(), + "updated": t.updated.isoformat() }, - "schema": PHENOPACKET_SEARCH_SCHEMA - } for d in Dataset.objects.all()]) + "schema": DATA_TYPES[t.data_type]["schema"], + } for t in Table.objects.filter(data_type__in=data_types)]) # TODO: Remove pragma: no cover when GET/POST implemented @@ -87,8 +92,8 @@ def table_detail(request, table_id): # pragma: no cover # TODO: Permissions: Check if user has control / more direct access over this table and/or dataset? # Or just always use owner... try: - table = Dataset.objects.get(identifier=table_id) - except Dataset.DoesNotExist: + table = Table.objects.get(ownership_record_id=table_id) + except Table.DoesNotExist: return Response(errors.not_found_error(f"Table with ID {table_id} not found"), status=404) if request.method == "DELETE": @@ -96,78 +101,99 @@ def table_detail(request, table_id): # pragma: no cover return Response(status=204) -@api_view(["GET"]) -@permission_classes([OverrideOrSuperUserOnly]) -def chord_table_summary(_request, table_id): - try: - table = Dataset.objects.get(identifier=table_id) - phenopackets = Phenopacket.objects.filter(dataset=table) +def experiment_table_summary(table_id): + table = Table.objects.get(ownership_record_id=table_id) + experiments = Experiment.objects.filter(table=table) # TODO - diseases_counter = Counter() - phenotypic_features_counter = Counter() + return Response({ + "count": experiments.count(), + "data_type_specific": {}, # TODO + }) - biosamples_set = set() - individuals_set = set() - biosamples_cs = Counter() - biosamples_taxonomy = Counter() +def phenopacket_table_summary(table_id): + table = Table.objects.get(ownership_record_id=table_id) + phenopackets = Phenopacket.objects.filter(table=table) # TODO - individuals_sex = Counter() - individuals_k_sex = Counter() - individuals_taxonomy = Counter() + diseases_counter = Counter() + phenotypic_features_counter = Counter() - def count_individual(ind): - individuals_set.add(ind.id) - individuals_sex.update((ind.sex,)) - individuals_k_sex.update((ind.karyotypic_sex,)) - if ind.taxonomy is not None: - individuals_taxonomy.update((ind.taxonomy["id"],)) + biosamples_set = set() + individuals_set = set() - for p in phenopackets.prefetch_related("biosamples"): - for b in p.biosamples.all(): - biosamples_set.add(b.id) - biosamples_cs.update((b.is_control_sample,)) + biosamples_cs = Counter() + biosamples_taxonomy = Counter() - if b.taxonomy is not None: - biosamples_taxonomy.update((b.taxonomy["id"],)) + individuals_sex = Counter() + individuals_k_sex = Counter() + individuals_taxonomy = Counter() - if b.individual is not None: - count_individual(b.individual) + def count_individual(ind): + individuals_set.add(ind.id) + individuals_sex.update((ind.sex,)) + individuals_k_sex.update((ind.karyotypic_sex,)) + if ind.taxonomy is not None: + individuals_taxonomy.update((ind.taxonomy["id"],)) - for pf in b.phenotypic_features.all(): - phenotypic_features_counter.update((pf.pftype["id"],)) + for p in phenopackets.prefetch_related("biosamples"): + for b in p.biosamples.all(): + biosamples_set.add(b.id) + biosamples_cs.update((b.is_control_sample,)) - for d in p.diseases.all(): - diseases_counter.update((d.term["id"],)) + if b.taxonomy is not None: + biosamples_taxonomy.update((b.taxonomy["id"],)) - for pf in p.phenotypic_features.all(): + if b.individual is not None: + count_individual(b.individual) + + for pf in b.phenotypic_features.all(): phenotypic_features_counter.update((pf.pftype["id"],)) - # Currently, phenopacket subject is required so we can assume it's not None - count_individual(p.subject) - - return Response({ - "count": phenopackets.count(), - "data_type_specific": { - "biosamples": { - "count": len(biosamples_set), - "is_control_sample": dict(biosamples_cs), - "taxonomy": dict(biosamples_taxonomy), - }, - "diseases": dict(diseases_counter), - "individuals": { - "count": len(individuals_set), - "sex": {k: individuals_sex[k] for k in (s[0] for s in Individual.SEX)}, - "karyotypic_sex": {k: individuals_k_sex[k] for k in (s[0] for s in Individual.KARYOTYPIC_SEX)}, - "taxonomy": dict(individuals_taxonomy), - # TODO: age histogram - }, - "phenotypic_features": dict(phenotypic_features_counter), - } - }) - - except Dataset.DoesNotExist: + for d in p.diseases.all(): + diseases_counter.update((d.term["id"],)) + + for pf in p.phenotypic_features.all(): + phenotypic_features_counter.update((pf.pftype["id"],)) + + # Currently, phenopacket subject is required so we can assume it's not None + count_individual(p.subject) + + return Response({ + "count": phenopackets.count(), + "data_type_specific": { + "biosamples": { + "count": len(biosamples_set), + "is_control_sample": dict(biosamples_cs), + "taxonomy": dict(biosamples_taxonomy), + }, + "diseases": dict(diseases_counter), + "individuals": { + "count": len(individuals_set), + "sex": {k: individuals_sex[k] for k in (s[0] for s in Individual.SEX)}, + "karyotypic_sex": {k: individuals_k_sex[k] for k in (s[0] for s in Individual.KARYOTYPIC_SEX)}, + "taxonomy": dict(individuals_taxonomy), + # TODO: age histogram + }, + "phenotypic_features": dict(phenotypic_features_counter), + } + }) + + +SUMMARY_HANDLERS: Dict[str, Callable[[Any], Response]] = { + DATA_TYPE_EXPERIMENT: experiment_table_summary, + DATA_TYPE_PHENOPACKET: phenopacket_table_summary, +} + + +@api_view(["GET"]) +@permission_classes([OverrideOrSuperUserOnly]) +def chord_table_summary(_request, data_type: str, table_id): + try: + return SUMMARY_HANDLERS[data_type](table_id) + except Table.DoesNotExist: return Response(errors.not_found_error(f"Table with ID {table_id} not found"), status=404) + except KeyError: + return Response(errors.not_found_error(f"Date type {data_type} not found"), status=404) # TODO: CHORD-standardized logging @@ -176,13 +202,19 @@ def debug_log(message): # pragma: no cover print(f"[CHORD Metadata {datetime.now()}] [DEBUG] {message}", flush=True) -def phenopacket_results(query, params, key="id"): +def data_type_results(query, params, key="id"): with connection.cursor() as cursor: debug_log(f"Executing SQL:\n {query.as_string(cursor.connection)}") cursor.execute(query.as_string(cursor.connection), params) return set(dict(zip([col[0] for col in cursor.description], row))[key] for row in cursor.fetchall()) +def experiment_query_results(query, params): + # TODO: possibly a quite inefficient way of doing things... + # TODO: Prefetch related biosample or no? + return Experiment.objects.filter(id__in=data_type_results(query, params, "id")) + + def phenopacket_query_results(query, params): # TODO: possibly a quite inefficient way of doing things... # To expand further on this query : the select_related call @@ -191,7 +223,7 @@ def phenopacket_query_results(query, params): # sure that, for instance, when querying diseases, we won't make multiple call # for the same set of data return Phenopacket.objects.filter( - id__in=phenopacket_results(query, params, "id") + id__in=data_type_results(query, params, "id") ).select_related( 'subject', 'meta_data' @@ -201,7 +233,9 @@ def phenopacket_query_results(query, params): def search(request, internal_data=False): - if "data_type" not in request.data: + data_type = request.data.get("data_type") + + if not data_type: return Response(errors.bad_request_error("Missing data_type in request body"), status=400) if "query" not in request.data: @@ -209,7 +243,7 @@ def search(request, internal_data=False): start = datetime.now() - if request.data["data_type"] != PHENOPACKET_DATA_TYPE_ID: + if data_type not in DATA_TYPES: return Response( errors.bad_request_error(f"Missing or invalid data type (Specified: {request.data['data_type']})"), status=400 @@ -217,26 +251,30 @@ def search(request, internal_data=False): try: compiled_query, params = postgres.search_query_to_psycopg2_sql(request.data["query"], - PHENOPACKET_SEARCH_SCHEMA) + DATA_TYPES[data_type]["schema"]) except (SyntaxError, TypeError, ValueError) as e: return Response(errors.bad_request_error(f"Error compiling query (message: {str(e)})"), status=400) if not internal_data: - datasets = Dataset.objects.filter(identifier__in=phenopacket_results( + tables = Table.objects.filter(ownership_record_id__in=data_type_results( query=compiled_query, params=params, - key="dataset_id" + key="table_id" )) # TODO: Maybe can avoid hitting DB here - return Response(build_search_response([{"id": d.identifier, "data_type": PHENOPACKET_DATA_TYPE_ID} - for d in datasets], start)) + return Response(build_search_response([{"id": t.identifier, "data_type": DATA_TYPE_PHENOPACKET} + for t in tables], start)) + + # TODO: Dict-ify + serializer_class = PhenopacketSerializer if data_type == DATA_TYPE_PHENOPACKET else ExperimentSerializer + query_function = phenopacket_query_results if data_type == DATA_TYPE_PHENOPACKET else experiment_query_results return Response(build_search_response({ - dataset_id: { - "data_type": PHENOPACKET_DATA_TYPE_ID, - "matches": list(PhenopacketSerializer(p).data for p in dataset_phenopackets) - } for dataset_id, dataset_phenopackets in itertools.groupby( - phenopacket_query_results(compiled_query, params), - key=lambda p: str(p.dataset_id) + table_id: { + "data_type": data_type, + "matches": list(serializer_class(p).data for p in table_objects) + } for table_id, table_objects in itertools.groupby( + query_function(compiled_query, params), + key=lambda o: str(o.table_id) ) }, start)) @@ -325,14 +363,14 @@ def hits_for(resource_type: str): if not internal_data: # TODO: Maybe can avoid hitting DB here - datasets = Dataset.objects.filter(identifier__in=frozenset(p.dataset_id for p in phenopackets)) - return Response(build_search_response([{"id": d.identifier, "data_type": PHENOPACKET_DATA_TYPE_ID} + datasets = Table.objects.filter(ownership_record_id__in=frozenset(p.table_id for p in phenopackets)) + return Response(build_search_response([{"id": d.identifier, "data_type": DATA_TYPE_PHENOPACKET} for d in datasets], start)) return Response(build_search_response({ - dataset_id: { - "data_type": PHENOPACKET_DATA_TYPE_ID, - "matches": list(PhenopacketSerializer(p).data for p in dataset_phenopackets) - } for dataset_id, dataset_phenopackets in itertools.groupby(phenopackets, key=lambda p: str(p.dataset_id)) + table_id: { + "data_type": DATA_TYPE_PHENOPACKET, + "matches": list(PhenopacketSerializer(p).data for p in table_phenopackets) + } for table_id, table_phenopackets in itertools.groupby(phenopackets, key=lambda p: str(p.table_id)) }, start)) @@ -359,20 +397,20 @@ def chord_table_search(request, table_id, internal=False): return Response(errors.bad_request_error("Missing query in request body"), status=400) # Check that dataset exists - dataset = Dataset.objects.get(identifier=table_id) + table = Table.objects.get(ownership_record_id=table_id) try: compiled_query, params = postgres.search_query_to_psycopg2_sql(request.data["query"], - PHENOPACKET_SEARCH_SCHEMA) + DATA_TYPES[table.data_type]["schema"]) except (SyntaxError, TypeError, ValueError) as e: print("[CHORD Metadata] Error encountered compiling query {}:\n {}".format(request.data["query"], str(e))) return Response(errors.bad_request_error(f"Error compiling query (message: {str(e)})"), status=400) debug_log(f"Finished compiling query in {datetime.now() - start}") - query_results = phenopacket_query_results( - query=sql.SQL("{} AND dataset_id = {}").format(compiled_query, sql.Placeholder()), - params=params + (dataset.identifier,) + query_results = phenopacket_query_results( # TODO: Generic + query=sql.SQL("{} AND table_id = {}").format(compiled_query, sql.Placeholder()), + params=params + (table.identifier,) ) debug_log(f"Finished running query in {datetime.now() - start}") @@ -389,7 +427,7 @@ def chord_table_search(request, table_id, internal=False): @api_view(["POST"]) @permission_classes([AllowAny]) def chord_public_table_search(request, table_id): - # Search phenopacket data types in specific tables without leaking internal data + # Search data types in specific tables without leaking internal data return chord_table_search(request, table_id, internal=False) @@ -398,6 +436,6 @@ def chord_public_table_search(request, table_id): @api_view(["POST"]) @permission_classes([AllowAny]) def chord_private_table_search(request, table_id): - # Search phenopacket data types in specific tables + # Search data types in specific tables # Private search endpoints are protected by URL namespace, not by Django permissions. return chord_table_search(request, table_id, internal=True) diff --git a/chord_metadata_service/chord/workflows/experiments_json.wdl b/chord_metadata_service/chord/workflows/experiments_json.wdl new file mode 100644 index 000000000..e3817f973 --- /dev/null +++ b/chord_metadata_service/chord/workflows/experiments_json.wdl @@ -0,0 +1,17 @@ +workflow experiments_json { + File json_document + + call identity_task { + input: json_document_in = json_document + } +} + +task identity_task { + File json_document_in + command { + true + } + output { + File json_document = "${json_document_in}" + } +} diff --git a/chord_metadata_service/metadata/urls.py b/chord_metadata_service/metadata/urls.py index 11203bdf9..7f67b6a8a 100644 --- a/chord_metadata_service/metadata/urls.py +++ b/chord_metadata_service/metadata/urls.py @@ -16,7 +16,7 @@ from django.contrib import admin from django.urls import path, include from chord_metadata_service.restapi import api_views, urls as restapi_urls -from chord_metadata_service.chord import views_ingest, views_search +from chord_metadata_service.chord import urls as chord_urls from rest_framework.schemas import get_schema_view from rest_framework_swagger.views import get_swagger_view @@ -24,38 +24,18 @@ from .settings import DEBUG swagger_schema_view = get_swagger_view(title="Metadata Service API") +schema_view = get_schema_view( + title="Metadata Service API", + description="Metadata Service provides a phenotypic description of an Individual in the context of biomedical " + "research.", + version="0.1" +) urlpatterns = [ - path('api/schema', get_schema_view( - title="Metadata Service API", - description="Metadata Service provides a phenotypic description of an Individual " - "in the context of biomedical research.", - version="0.1" - ), - name='openapi-schema'), path('', swagger_schema_view), path('api/', include(restapi_urls)), + path('api/schema', schema_view, name='openapi-schema'), path('service-info', api_views.service_info, name="service-info"), - - path('workflows', views_ingest.workflow_list, name="workflows"), - path('workflows/', views_ingest.workflow_item, name="workflow-detail"), - path('workflows/.wdl', views_ingest.workflow_file, name="workflow-file"), - - path('private/ingest', views_ingest.ingest, name="ingest"), - - path('data-types', views_search.data_type_list, name="data-type-list"), - path('data-types/phenopacket', views_search.data_type_phenopacket, name="data-type-detail"), - path('data-types/phenopacket/schema', views_search.data_type_phenopacket_schema, name="data-type-schema"), - # TODO: Consistent snake or kebab - path('data-types/phenopacket/metadata_schema', views_search.data_type_phenopacket_metadata_schema, - name="data-type-metadata-schema"), - path('tables', views_search.table_list, name="table-list"), - path('tables/', views_search.table_detail, name="table-detail"), - path('tables//summary', views_search.chord_table_summary, name="table-summary"), - path('tables//search', views_search.chord_public_table_search, name="public-table-search"), - path('search', views_search.chord_search, name="search"), - path('fhir-search', views_search.fhir_public_search, name="fhir-search"), - path('private/fhir-search', views_search.fhir_private_search, name="fhir-private-search"), - path('private/search', views_search.chord_private_search, name="private-search"), - path('private/tables//search', views_search.chord_private_table_search, name="private-table-search"), -] + ([path('admin/', admin.site.urls)] if DEBUG else []) + *chord_urls.urlpatterns, # TODO: Use include? can we double up? + *([path('admin/', admin.site.urls)] if DEBUG else []), +] diff --git a/chord_metadata_service/restapi/urls.py b/chord_metadata_service/restapi/urls.py index bc03d97ed..a731acd61 100644 --- a/chord_metadata_service/restapi/urls.py +++ b/chord_metadata_service/restapi/urls.py @@ -11,6 +11,12 @@ router = routers.DefaultRouter(trailing_slash=False) +# CHORD app urls +router.register(r'projects', chord_views.ProjectViewSet) +router.register(r'datasets', chord_views.DatasetViewSet) +router.register(r'table_ownership', chord_views.TableOwnershipViewSet) +router.register(r'tables', chord_views.TableViewSet) + # Experiments app urls router.register(r'experiments', experiment_views.ExperimentViewSet) @@ -31,9 +37,6 @@ router.register(r'genomicinterpretations', phenopacket_views.GenomicInterpretationViewSet) router.register(r'diagnoses', phenopacket_views.DiagnosisViewSet) router.register(r'interpretations', phenopacket_views.InterpretationViewSet) -router.register(r'projects', chord_views.ProjectViewSet) -router.register(r'datasets', chord_views.DatasetViewSet) -router.register(r'table_ownership', chord_views.TableOwnershipViewSet) # mCode app urls router.register(r'geneticvariantstested', mcode_views.GeneticVariantTestedViewSet) From e29b66ddd0b752037e8917287af49d57bd9ea464 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Tue, 19 May 2020 15:36:18 -0400 Subject: [PATCH 039/190] Fix experiment tests for model updates --- .../experiments/tests/test_models.py | 63 ++++++++++--------- 1 file changed, 35 insertions(+), 28 deletions(-) diff --git a/chord_metadata_service/experiments/tests/test_models.py b/chord_metadata_service/experiments/tests/test_models.py index cd5e5b947..fefd29891 100644 --- a/chord_metadata_service/experiments/tests/test_models.py +++ b/chord_metadata_service/experiments/tests/test_models.py @@ -2,14 +2,18 @@ from django.core.exceptions import ValidationError from rest_framework import serializers from chord_metadata_service.patients.models import Individual +from chord_metadata_service.phenopackets.models import Biosample, Procedure from ..models import Experiment +from chord_metadata_service.phenopackets.tests.constants import VALID_PROCEDURE_1, VALID_INDIVIDUAL_1, valid_biosample_1 class ExperimentTest(TestCase): """ Test module for Experiment model """ def setUp(self): - Individual.objects.create(id='patient:1', sex='FEMALE', age={"age": "P25Y3M2D"}) + i = Individual.objects.create(**VALID_INDIVIDUAL_1) + p = Procedure.objects.create(**VALID_PROCEDURE_1) + self.biosample = Biosample.objects.create(**valid_biosample_1(i, p)) Experiment.objects.create( id='experiment:1', reference_registry_id='some_id', @@ -19,47 +23,50 @@ def setUp(self): molecule_ontology=[{"id": "ontology:1", "label": "Ontology term 1"}], molecule='total RNA', library_strategy='Bisulfite-Seq', - other_fields={"some_field": "value"} + other_fields={"some_field": "value"}, + biosample=self.biosample ) def create(self, **kwargs): - e = Experiment(**kwargs) + e = Experiment(id="experiment:2", **kwargs) e.full_clean() e.save() def test_validation(self): - individual_one = Individual.objects.get(id='patient:1') - # Invalid experiment_ontology - self.assertRaises(serializers.ValidationError, self.create, - id='experiment:2', - library_strategy='Bisulfite-Seq', - experiment_type='Chromatin Accessibility', - experiment_ontology=["invalid_value"], - individual=individual_one + self.assertRaises( + serializers.ValidationError, + self.create, + library_strategy='Bisulfite-Seq', + experiment_type='Chromatin Accessibility', + experiment_ontology=["invalid_value"], + biosample=self.biosample ) # Invalid molecule_ontology - self.assertRaises(serializers.ValidationError, self.create, - id='experiment:2', - library_strategy='Bisulfite-Seq', - experiment_type='Chromatin Accessibility', - molecule_ontology=[{"id": "some_id"}], - individual=individual_one + self.assertRaises( + serializers.ValidationError, + self.create, + library_strategy='Bisulfite-Seq', + experiment_type='Chromatin Accessibility', + molecule_ontology=[{"id": "some_id"}], + biosample=self.biosample ) # Invalid value in other_fields - self.assertRaises(serializers.ValidationError, self.create, - id='experiment:2', - library_strategy='Bisulfite-Seq', - experiment_type='Chromatin Accessibility', - other_fields={"some_field": "value", "invalid_value": 42}, - individual=individual_one + self.assertRaises( + serializers.ValidationError, + self.create, + library_strategy='Bisulfite-Seq', + experiment_type='Chromatin Accessibility', + other_fields={"some_field": "value", "invalid_value": 42}, + biosample=self.biosample ) - # Missing individual or biosamples - self.assertRaises(ValidationError, self.create, - id='experiment:2', - library_strategy='Bisulfite-Seq', - experiment_type='Chromatin Accessibility' + # Missing biosample + self.assertRaises( + ValidationError, + self.create, + library_strategy='Bisulfite-Seq', + experiment_type='Chromatin Accessibility' ) From a8c6e45927c3a3d02e80d38900d20186632057f4 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Tue, 19 May 2020 15:38:04 -0400 Subject: [PATCH 040/190] Prefetch ownership records and add dataset to table serializer --- chord_metadata_service/chord/api_views.py | 14 ++++++++++++-- chord_metadata_service/chord/models.py | 4 ++++ chord_metadata_service/chord/serializers.py | 17 +++++++++-------- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/chord_metadata_service/chord/api_views.py b/chord_metadata_service/chord/api_views.py index 9fff4d5ee..aad9f96a9 100644 --- a/chord_metadata_service/chord/api_views.py +++ b/chord_metadata_service/chord/api_views.py @@ -61,7 +61,7 @@ class TableOwnershipViewSet(CHORDPublicModelViewSet): post: Create a new relationship between a dataset (and optionally a specific biosample) and a table - in another service + in a data service """ queryset = TableOwnership.objects.all().order_by("table_id") @@ -69,5 +69,15 @@ class TableOwnershipViewSet(CHORDPublicModelViewSet): class TableViewSet(CHORDPublicModelViewSet): - queryset = Table.objects.all().order_by("ownership_record_id") + """ + get: + Return a list of tables + + post: + Create a new table + """ + + # TODO: Create TableOwnership if needed - here or model? + + queryset = Table.objects.all().prefetch_related("ownership_record").order_by("ownership_record_id") serializer_class = TableSerializer diff --git a/chord_metadata_service/chord/models.py b/chord_metadata_service/chord/models.py index 5b9dddaee..a3e4d8004 100644 --- a/chord_metadata_service/chord/models.py +++ b/chord_metadata_service/chord/models.py @@ -165,3 +165,7 @@ class Table(models.Model): @property def identifier(self): return self.ownership_record_id + + @property + def dataset(self): + return self.ownership_record.dataset diff --git a/chord_metadata_service/chord/serializers.py b/chord_metadata_service/chord/serializers.py index 9f2aff7d1..52f581a05 100644 --- a/chord_metadata_service/chord/serializers.py +++ b/chord_metadata_service/chord/serializers.py @@ -25,14 +25,6 @@ class Meta: fields = '__all__' -class TableSerializer(GenericSerializer): - identifier = serializers.CharField(read_only=True) - - class Meta: - model = Table - fields = "__all__" - - class DatasetSerializer(GenericSerializer): always_include = ( "description", @@ -157,3 +149,12 @@ def validate_title(self, value): class Meta: model = Project fields = '__all__' + + +class TableSerializer(GenericSerializer): + identifier = serializers.CharField(read_only=True) + dataset = DatasetSerializer(read_only=True, exclude_when_nested=["table_ownership"]) + + class Meta: + model = Table + fields = "__all__" From c502f651d81acdd1dfe3050382746ab3d593ea01 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Tue, 19 May 2020 15:39:01 -0400 Subject: [PATCH 041/190] Fix n_of_tables on dataset, table_summary issues --- chord_metadata_service/chord/models.py | 3 +-- chord_metadata_service/chord/views_search.py | 13 +++++-------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/chord_metadata_service/chord/models.py b/chord_metadata_service/chord/models.py index a3e4d8004..f97c29894 100644 --- a/chord_metadata_service/chord/models.py +++ b/chord_metadata_service/chord/models.py @@ -57,8 +57,7 @@ class Dataset(models.Model): @property def n_of_tables(self): - # TODO: No hard-code: +1 for phenopackets table - return TableOwnership.objects.filter(dataset=self).count() + 1 + return TableOwnership.objects.filter(dataset=self).count() # --------------------------- DATS model fields --------------------------- diff --git a/chord_metadata_service/chord/views_search.py b/chord_metadata_service/chord/views_search.py index bd3f243c4..693c46c89 100644 --- a/chord_metadata_service/chord/views_search.py +++ b/chord_metadata_service/chord/views_search.py @@ -101,8 +101,7 @@ def table_detail(request, table_id): # pragma: no cover return Response(status=204) -def experiment_table_summary(table_id): - table = Table.objects.get(ownership_record_id=table_id) +def experiment_table_summary(table): experiments = Experiment.objects.filter(table=table) # TODO return Response({ @@ -111,8 +110,7 @@ def experiment_table_summary(table_id): }) -def phenopacket_table_summary(table_id): - table = Table.objects.get(ownership_record_id=table_id) +def phenopacket_table_summary(table): phenopackets = Phenopacket.objects.filter(table=table) # TODO diseases_counter = Counter() @@ -187,13 +185,12 @@ def count_individual(ind): @api_view(["GET"]) @permission_classes([OverrideOrSuperUserOnly]) -def chord_table_summary(_request, data_type: str, table_id): +def chord_table_summary(_request, table_id): try: - return SUMMARY_HANDLERS[data_type](table_id) + table = Table.objects.get(ownership_record_id=table_id) + return SUMMARY_HANDLERS[table.data_type](table) except Table.DoesNotExist: return Response(errors.not_found_error(f"Table with ID {table_id} not found"), status=404) - except KeyError: - return Response(errors.not_found_error(f"Date type {data_type} not found"), status=404) # TODO: CHORD-standardized logging From 7abbeb8063c8f8bae7678d8a42d11d2987f46798 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Tue, 19 May 2020 15:39:13 -0400 Subject: [PATCH 042/190] Remove some redundant code --- chord_metadata_service/metadata/settings.py | 2 +- chord_metadata_service/restapi/urls.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/chord_metadata_service/metadata/settings.py b/chord_metadata_service/metadata/settings.py index e490b99cd..0a63a9f80 100644 --- a/chord_metadata_service/metadata/settings.py +++ b/chord_metadata_service/metadata/settings.py @@ -38,7 +38,7 @@ # CHORD-specific settings -CHORD_URL = os.environ.get("CHORD_URL", None) # Leave None if not specified, for running in other contexts +CHORD_URL = os.environ.get("CHORD_URL") # Leave None if not specified, for running in other contexts # SECURITY WARNING: Don't run with CHORD_PERMISSIONS turned off in production, # unless an alternative permissions system is in place. diff --git a/chord_metadata_service/restapi/urls.py b/chord_metadata_service/restapi/urls.py index a731acd61..215884a30 100644 --- a/chord_metadata_service/restapi/urls.py +++ b/chord_metadata_service/restapi/urls.py @@ -6,8 +6,6 @@ from chord_metadata_service.phenopackets import api_views as phenopacket_views from chord_metadata_service.mcode import api_views as mcode_views -# from .settings import DEBUG - router = routers.DefaultRouter(trailing_slash=False) From 894a8ab1c6b75d85ef74c341cbaadc25a1eff040 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Tue, 19 May 2020 15:39:23 -0400 Subject: [PATCH 043/190] Fix chord tests to use new table model --- .../chord/tests/constants.py | 24 ++++ .../chord/tests/test_api.py | 3 + .../chord/tests/test_api_ingest.py | 12 +- .../chord/tests/test_api_search.py | 121 ++++++++++-------- .../chord/tests/test_ingest.py | 19 ++- .../chord/tests/test_search.py | 13 +- 6 files changed, 123 insertions(+), 69 deletions(-) diff --git a/chord_metadata_service/chord/tests/constants.py b/chord_metadata_service/chord/tests/constants.py index 957e2af8c..8aad20eab 100644 --- a/chord_metadata_service/chord/tests/constants.py +++ b/chord_metadata_service/chord/tests/constants.py @@ -1,9 +1,14 @@ +import uuid + +from ..data_types import DATA_TYPE_PHENOPACKET + __all__ = [ "VALID_DATA_USE_1", "VALID_PROJECT_1", "VALID_DATS_CREATORS", "INVALID_DATS_CREATORS", "valid_dataset_1", + "valid_table_1", "dats_dataset", "TEST_SEARCH_QUERY_1", "TEST_SEARCH_QUERY_2", @@ -73,6 +78,25 @@ def valid_dataset_1(project_id): } +def valid_table_1(dataset_id, model_compatible=False): + table_id = str(uuid.uuid4()) + service_id = str(uuid.uuid4()) # TODO: Real service ID + return ( + { + "table_id": table_id, + "service_id": service_id, + "service_artifact": "metadata", + "data_type": DATA_TYPE_PHENOPACKET, # TODO: Remove + ("dataset_id" if model_compatible else "dataset"): dataset_id, + }, + { + ("ownership_record_id" if model_compatible else "ownership_record"): table_id, + "name": "Table 1", + "data_type": DATA_TYPE_PHENOPACKET, + } + ) + + def dats_dataset(project_id, creators): return { "version": "1.0", diff --git a/chord_metadata_service/chord/tests/test_api.py b/chord_metadata_service/chord/tests/test_api.py index 8069151e0..adfd9ffa2 100644 --- a/chord_metadata_service/chord/tests/test_api.py +++ b/chord_metadata_service/chord/tests/test_api.py @@ -105,3 +105,6 @@ def test_dats(self): # TODO: Create TableOwnership # TODO: Update TableOwnership # TODO: Delete TableOwnership +# TODO: Create Table +# TODO: Update Table +# TODO: Delete Table diff --git a/chord_metadata_service/chord/tests/test_api_ingest.py b/chord_metadata_service/chord/tests/test_api_ingest.py index 5e028e410..ca51b81e3 100644 --- a/chord_metadata_service/chord/tests/test_api_ingest.py +++ b/chord_metadata_service/chord/tests/test_api_ingest.py @@ -59,6 +59,12 @@ def setUp(self) -> None: content_type="application/json") self.dataset = r.json() + table_ownership, table_record = valid_table_1(self.dataset["identifier"]) + self.client.post(reverse("tableownership-list"), data=json.dumps(table_ownership), + content_type="application/json") + r = self.client.post(reverse("table-list"), data=json.dumps(table_record), content_type="application/json") + self.table = r.json() + @override_settings(AUTH_OVERRIDE=True) # For permissions def test_ingest(self): # No ingestion body @@ -69,19 +75,19 @@ def test_ingest(self): r = self.client.post(reverse("ingest"), data=json.dumps({}), content_type="application/json") self.assertEqual(r.status_code, status.HTTP_400_BAD_REQUEST) - # Non-existent dataset ID + # Non-existent table ID r = self.client.post(reverse("ingest"), data=json.dumps(generate_ingest(str(uuid4()))), content_type="application/json") self.assertEqual(r.status_code, status.HTTP_400_BAD_REQUEST) # Non-existent workflow ID - bad_wf = generate_ingest(self.dataset["identifier"]) + bad_wf = generate_ingest(self.table["identifier"]) bad_wf["workflow_id"] += "_invalid" r = self.client.post(reverse("ingest"), data=json.dumps(bad_wf), content_type="application/json") self.assertEqual(r.status_code, status.HTTP_400_BAD_REQUEST) # json_document not in output - bad_wf = generate_ingest(self.dataset["identifier"]) + bad_wf = generate_ingest(self.table["identifier"]) bad_wf["workflow_outputs"] = {} r = self.client.post(reverse("ingest"), data=json.dumps(bad_wf), content_type="application/json") self.assertEqual(r.status_code, status.HTTP_400_BAD_REQUEST) diff --git a/chord_metadata_service/chord/tests/test_api_search.py b/chord_metadata_service/chord/tests/test_api_search.py index ba2ccee2d..38581c5ad 100644 --- a/chord_metadata_service/chord/tests/test_api_search.py +++ b/chord_metadata_service/chord/tests/test_api_search.py @@ -8,14 +8,21 @@ from rest_framework import status from rest_framework.test import APITestCase +from chord_metadata_service.patients.models import Individual from chord_metadata_service.phenopackets.tests.constants import * -from chord_metadata_service.phenopackets.models import * -from chord_metadata_service.phenopackets.search_schemas import PHENOPACKET_SEARCH_SCHEMA +from chord_metadata_service.phenopackets.models import Biosample, MetaData, Phenopacket, Procedure from chord_metadata_service.chord.tests.es_mocks import SEARCH_SUCCESS -from .constants import * -from ..models import * -from ..views_search import PHENOPACKET_DATA_TYPE_ID, PHENOPACKET_METADATA_SCHEMA +from .constants import ( + VALID_PROJECT_1, + valid_dataset_1, + valid_table_1, + TEST_SEARCH_QUERY_1, + TEST_SEARCH_QUERY_2, + TEST_FHIR_SEARCH_QUERY, +) +from ..models import Project, Dataset, TableOwnership, Table +from ..data_types import DATA_TYPE_EXPERIMENT, DATA_TYPE_PHENOPACKET, DATA_TYPES class DataTypeTest(APITestCase): @@ -23,45 +30,45 @@ def test_data_type_list(self): r = self.client.get(reverse("data-type-list")) self.assertEqual(r.status_code, status.HTTP_200_OK) c = r.json() - self.assertEqual(len(c), 1) - self.assertEqual(c[0]["id"], PHENOPACKET_DATA_TYPE_ID) + self.assertEqual(len(c), 2) + ids = (c[0]["id"], c[1]["id"]) + self.assertIn(DATA_TYPE_EXPERIMENT, ids) + self.assertIn(DATA_TYPE_PHENOPACKET, ids) def test_data_type_detail(self): - r = self.client.get(reverse("data-type-detail")) # Only mounted with phenopacket right now + r = self.client.get(reverse("data-type-detail", kwargs={"data_type": DATA_TYPE_PHENOPACKET})) self.assertEqual(r.status_code, status.HTTP_200_OK) c = r.json() self.assertDictEqual(c, { - "id": PHENOPACKET_DATA_TYPE_ID, - "schema": PHENOPACKET_SEARCH_SCHEMA, - "metadata_schema": PHENOPACKET_METADATA_SCHEMA + "id": DATA_TYPE_PHENOPACKET, + **DATA_TYPES[DATA_TYPE_PHENOPACKET], }) def test_data_type_schema(self): - r = self.client.get(reverse("data-type-schema")) # Only mounted with phenopacket right now + r = self.client.get(reverse("data-type-schema", kwargs={"data_type": DATA_TYPE_PHENOPACKET})) self.assertEqual(r.status_code, status.HTTP_200_OK) c = r.json() - self.assertDictEqual(c, PHENOPACKET_SEARCH_SCHEMA) + self.assertDictEqual(c, DATA_TYPES[DATA_TYPE_PHENOPACKET]["schema"]) def test_data_type_metadata_schema(self): - r = self.client.get(reverse("data-type-metadata-schema")) # Only mounted with phenopacket right now + r = self.client.get(reverse("data-type-metadata-schema", kwargs={"data_type": DATA_TYPE_PHENOPACKET})) self.assertEqual(r.status_code, status.HTTP_200_OK) c = r.json() - self.assertDictEqual(c, PHENOPACKET_METADATA_SCHEMA) + self.assertDictEqual(c, DATA_TYPES[DATA_TYPE_PHENOPACKET]["metadata_schema"]) class TableTest(APITestCase): @staticmethod - def dataset_rep(dataset, created, updated): + def table_rep(table, created, updated): return { - "id": dataset["identifier"], - "name": dataset["title"], + "id": table["identifier"], + "name": table["name"], "metadata": { - "description": dataset["description"], - "project_id": dataset["project"], + "dataset_id": table["dataset"]["identifier"], "created": created, "updated": updated }, - "schema": PHENOPACKET_SEARCH_SCHEMA + "schema": DATA_TYPES[table["data_type"]]["schema"] } @override_settings(AUTH_OVERRIDE=True) # For permissions @@ -75,22 +82,27 @@ def setUp(self) -> None: content_type="application/json") self.dataset = r.json() - def test_table_list(self): + to, tr = valid_table_1(self.dataset["identifier"]) + self.client.post(reverse("tableownership-list"), data=json.dumps(to), content_type="application/json") + r = self.client.post(reverse("table-list"), data=json.dumps(tr), content_type="application/json") + self.table = r.json() + + def test_chord_table_list(self): # No data type specified - r = self.client.get(reverse("table-list")) + r = self.client.get(reverse("chord-table-list")) self.assertEqual(r.status_code, status.HTTP_400_BAD_REQUEST) - r = self.client.get(reverse("table-list"), {"data-type": PHENOPACKET_DATA_TYPE_ID}) + r = self.client.get(reverse("chord-table-list"), {"data-type": DATA_TYPE_PHENOPACKET}) self.assertEqual(r.status_code, status.HTTP_200_OK) c = r.json() self.assertEqual(len(c), 1) - self.assertEqual(c[0], self.dataset_rep(self.dataset, c[0]["metadata"]["created"], c[0]["metadata"]["updated"])) + self.assertEqual(c[0], self.table_rep(self.table, c[0]["metadata"]["created"], c[0]["metadata"]["updated"])) def test_table_summary(self): r = self.client.get(reverse("table-summary", kwargs={"table_id": str(uuid.uuid4())})) self.assertEqual(r.status_code, 404) - r = self.client.get(reverse("table-summary", kwargs={"table_id": self.dataset["identifier"]})) + r = self.client.get(reverse("table-summary", kwargs={"table_id": self.table["identifier"]})) s = r.json() self.assertEqual(s["count"], 0) # No phenopackets self.assertIn("data_type_specific", s) @@ -100,6 +112,9 @@ class SearchTest(APITestCase): def setUp(self) -> None: self.project = Project.objects.create(**VALID_PROJECT_1) self.dataset = Dataset.objects.create(**valid_dataset_1(self.project)) + to, tr = valid_table_1(self.dataset.identifier, model_compatible=True) + TableOwnership.objects.create(**to) + self.table = Table.objects.create(**tr) # Set up a dummy phenopacket @@ -117,7 +132,7 @@ def setUp(self) -> None: id="phenopacket_id:1", subject=self.individual, meta_data=self.meta_data, - dataset=self.dataset + table=self.table ) self.phenopacket.biosamples.set([self.biosample_1, self.biosample_2]) @@ -135,7 +150,7 @@ def test_common_search_2(self): def test_common_search_3(self): # No query - r = self.client.post(reverse("search"), data=json.dumps({"data_type": PHENOPACKET_DATA_TYPE_ID}), + r = self.client.post(reverse("search"), data=json.dumps({"data_type": DATA_TYPE_PHENOPACKET}), content_type="application/json") self.assertEqual(r.status_code, status.HTTP_400_BAD_REQUEST) @@ -150,7 +165,7 @@ def test_common_search_4(self): def test_common_search_5(self): # Bad syntax for query r = self.client.post(reverse("search"), data=json.dumps({ - "data_type": PHENOPACKET_DATA_TYPE_ID, + "data_type": DATA_TYPE_PHENOPACKET, "query": ["hello", "world"] }), content_type="application/json") self.assertEqual(r.status_code, status.HTTP_400_BAD_REQUEST) @@ -158,21 +173,21 @@ def test_common_search_5(self): def test_search_with_result(self): # Valid search with result r = self.client.post(reverse("search"), data=json.dumps({ - "data_type": PHENOPACKET_DATA_TYPE_ID, + "data_type": DATA_TYPE_PHENOPACKET, "query": TEST_SEARCH_QUERY_1 }), content_type="application/json") self.assertEqual(r.status_code, status.HTTP_200_OK) c = r.json() self.assertEqual(len(c["results"]), 1) self.assertDictEqual(c["results"][0], { - "id": str(self.dataset.identifier), - "data_type": PHENOPACKET_DATA_TYPE_ID + "id": str(self.table.identifier), + "data_type": DATA_TYPE_PHENOPACKET }) def test_search_without_result(self): # Valid search without result r = self.client.post(reverse("search"), data=json.dumps({ - "data_type": PHENOPACKET_DATA_TYPE_ID, + "data_type": DATA_TYPE_PHENOPACKET, "query": TEST_SEARCH_QUERY_2 }), content_type="application/json") self.assertEqual(r.status_code, status.HTTP_200_OK) @@ -182,45 +197,45 @@ def test_search_without_result(self): def test_private_search(self): # Valid search with result r = self.client.post(reverse("private-search"), data=json.dumps({ - "data_type": PHENOPACKET_DATA_TYPE_ID, + "data_type": DATA_TYPE_PHENOPACKET, "query": TEST_SEARCH_QUERY_1 }), content_type="application/json") self.assertEqual(r.status_code, status.HTTP_200_OK) c = r.json() - self.assertIn(str(self.dataset.identifier), c["results"]) - self.assertEqual(c["results"][str(self.dataset.identifier)]["data_type"], PHENOPACKET_DATA_TYPE_ID) - self.assertEqual(self.phenopacket.id, c["results"][str(self.dataset.identifier)]["matches"][0]["id"]) + self.assertIn(str(self.table.identifier), c["results"]) + self.assertEqual(c["results"][str(self.table.identifier)]["data_type"], DATA_TYPE_PHENOPACKET) + self.assertEqual(self.phenopacket.id, c["results"][str(self.table.identifier)]["matches"][0]["id"]) # TODO: Check schema? def test_private_table_search_1(self): # No body - r = self.client.post(reverse("public-table-search", args=[str(self.dataset.identifier)])) + r = self.client.post(reverse("public-table-search", args=[str(self.table.identifier)])) self.assertEqual(r.status_code, status.HTTP_400_BAD_REQUEST) - r = self.client.post(reverse("private-table-search", args=[str(self.dataset.identifier)])) + r = self.client.post(reverse("private-table-search", args=[str(self.table.identifier)])) self.assertEqual(r.status_code, status.HTTP_400_BAD_REQUEST) def test_private_table_search_2(self): # No query - r = self.client.post(reverse("public-table-search", args=[str(self.dataset.identifier)]), data=json.dumps({}), + r = self.client.post(reverse("public-table-search", args=[str(self.table.identifier)]), data=json.dumps({}), content_type="application/json") self.assertEqual(r.status_code, status.HTTP_400_BAD_REQUEST) - r = self.client.post(reverse("private-table-search", args=[str(self.dataset.identifier)]), data=json.dumps({}), + r = self.client.post(reverse("private-table-search", args=[str(self.table.identifier)]), data=json.dumps({}), content_type="application/json") self.assertEqual(r.status_code, status.HTTP_400_BAD_REQUEST) def test_private_table_search_3(self): # Bad syntax for query - r = self.client.post(reverse("public-table-search", args=[str(self.dataset.identifier)]), data=json.dumps({ + r = self.client.post(reverse("public-table-search", args=[str(self.table.identifier)]), data=json.dumps({ "query": ["hello", "world"] }), content_type="application/json") self.assertEqual(r.status_code, status.HTTP_400_BAD_REQUEST) - r = self.client.post(reverse("private-table-search", args=[str(self.dataset.identifier)]), data=json.dumps({ + r = self.client.post(reverse("private-table-search", args=[str(self.table.identifier)]), data=json.dumps({ "query": ["hello", "world"] }), content_type="application/json") self.assertEqual(r.status_code, status.HTTP_400_BAD_REQUEST) @@ -228,14 +243,14 @@ def test_private_table_search_3(self): def test_private_table_search_4(self): # Valid query with one result - r = self.client.post(reverse("public-table-search", args=[str(self.dataset.identifier)]), data=json.dumps({ + r = self.client.post(reverse("public-table-search", args=[str(self.table.identifier)]), data=json.dumps({ "query": TEST_SEARCH_QUERY_1 }), content_type="application/json") self.assertEqual(r.status_code, status.HTTP_200_OK) c = r.json() self.assertEqual(c, True) - r = self.client.post(reverse("private-table-search", args=[str(self.dataset.identifier)]), data=json.dumps({ + r = self.client.post(reverse("private-table-search", args=[str(self.table.identifier)]), data=json.dumps({ "query": TEST_SEARCH_QUERY_1 }), content_type="application/json") self.assertEqual(r.status_code, status.HTTP_200_OK) @@ -245,7 +260,7 @@ def test_private_table_search_4(self): def test_private_table_search_5(self): # Valid query: literal "true" - r = self.client.post(reverse("private-table-search", args=[str(self.dataset.identifier)]), data=json.dumps({ + r = self.client.post(reverse("private-table-search", args=[str(self.table.identifier)]), data=json.dumps({ "query": True }), content_type="application/json") self.assertEqual(r.status_code, status.HTTP_200_OK) @@ -258,7 +273,7 @@ def test_fhir_search(self, mocked_es): mocked_es.search.return_value = SEARCH_SUCCESS # Valid search with result r = self.client.post(reverse("fhir-search"), data=json.dumps({ - "data_type": PHENOPACKET_DATA_TYPE_ID, + "data_type": DATA_TYPE_PHENOPACKET, "query": TEST_FHIR_SEARCH_QUERY }), content_type="application/json") @@ -267,8 +282,8 @@ def test_fhir_search(self, mocked_es): self.assertEqual(len(c["results"]), 1) self.assertDictEqual(c["results"][0], { - "id": str(self.dataset.identifier), - "data_type": PHENOPACKET_DATA_TYPE_ID + "id": str(self.table.identifier), + "data_type": DATA_TYPE_PHENOPACKET }) @patch('chord_metadata_service.chord.views_search.es') @@ -276,13 +291,13 @@ def test_private_fhir_search(self, mocked_es): mocked_es.search.return_value = SEARCH_SUCCESS # Valid search with result r = self.client.post(reverse("fhir-private-search"), data=json.dumps({ - "data_type": PHENOPACKET_DATA_TYPE_ID, + "data_type": DATA_TYPE_PHENOPACKET, "query": TEST_FHIR_SEARCH_QUERY }), content_type="application/json") self.assertEqual(r.status_code, status.HTTP_200_OK) c = r.json() - self.assertIn(str(self.dataset.identifier), c["results"]) - self.assertEqual(c["results"][str(self.dataset.identifier)]["data_type"], PHENOPACKET_DATA_TYPE_ID) - self.assertEqual(self.phenopacket.id, c["results"][str(self.dataset.identifier)]["matches"][0]["id"]) + self.assertIn(str(self.table.identifier), c["results"]) + self.assertEqual(c["results"][str(self.table.identifier)]["data_type"], DATA_TYPE_PHENOPACKET) + self.assertEqual(self.phenopacket.id, c["results"][str(self.table.identifier)]["matches"][0]["id"]) diff --git a/chord_metadata_service/chord/tests/test_ingest.py b/chord_metadata_service/chord/tests/test_ingest.py index 9924cd56b..aac745c83 100644 --- a/chord_metadata_service/chord/tests/test_ingest.py +++ b/chord_metadata_service/chord/tests/test_ingest.py @@ -1,8 +1,12 @@ +import uuid + from django.test import TestCase from dateutil.parser import isoparse -from chord_metadata_service.chord.models import Project, Dataset -from chord_metadata_service.chord.views_ingest import create_phenotypic_feature, ingest_phenopacket +from chord_metadata_service.chord.data_types import DATA_TYPE_PHENOPACKET +from chord_metadata_service.chord.models import Project, Dataset, TableOwnership, Table +# noinspection PyProtectedMember +from chord_metadata_service.chord.ingest import create_phenotypic_feature, DATA_TYPE_INGEST_FUNCTION_MAP from chord_metadata_service.phenopackets.models import PhenotypicFeature, Phenopacket from .constants import VALID_DATA_USE_1 @@ -14,6 +18,11 @@ def setUp(self) -> None: p = Project.objects.create(title="Project 1", description="") self.d = Dataset.objects.create(title="Dataset 1", description="Some dataset", data_use=VALID_DATA_USE_1, project=p) + # TODO: Real service ID + # TODO: Remove data_type here + to = TableOwnership.objects.create(table_id=uuid.uuid4(), service_id=uuid.uuid4(), service_artifact="metadata", + data_type=DATA_TYPE_PHENOPACKET, dataset=self.d) + self.t = Table.objects.create(ownership_record=to, name="Table 1", data_type=DATA_TYPE_PHENOPACKET) def test_create_pf(self): p1 = create_phenotypic_feature({ @@ -31,8 +40,8 @@ def test_create_pf(self): self.assertEqual(p1.pk, p2.pk) - def test_ingesting_json(self): - p = ingest_phenopacket(EXAMPLE_INGEST, self.d.identifier) + def test_ingesting_phenopackets_json(self): + p = DATA_TYPE_INGEST_FUNCTION_MAP[DATA_TYPE_PHENOPACKET](EXAMPLE_INGEST, self.t.identifier) self.assertEqual(p.id, Phenopacket.objects.get(id=p.id).id) self.assertEqual(p.subject.id, EXAMPLE_INGEST["subject"]["id"]) @@ -60,6 +69,6 @@ def test_ingesting_json(self): # TODO: More # Test ingesting again - p2 = ingest_phenopacket(EXAMPLE_INGEST, self.d.identifier) + p2 = DATA_TYPE_INGEST_FUNCTION_MAP[DATA_TYPE_PHENOPACKET](EXAMPLE_INGEST, self.t.identifier) self.assertNotEqual(p.id, p2.id) # TODO: More diff --git a/chord_metadata_service/chord/tests/test_search.py b/chord_metadata_service/chord/tests/test_search.py index 96f5137c6..d1326c598 100644 --- a/chord_metadata_service/chord/tests/test_search.py +++ b/chord_metadata_service/chord/tests/test_search.py @@ -1,15 +1,12 @@ from django.test import TestCase from jsonschema import Draft7Validator -from chord_metadata_service.phenopackets.search_schemas import PHENOPACKET_SEARCH_SCHEMA -from ..views_search import PHENOPACKET_METADATA_SCHEMA +from ..data_types import DATA_TYPES class SchemaTest(TestCase): @staticmethod - def test_phenopacket_schema(): - Draft7Validator.check_schema(PHENOPACKET_SEARCH_SCHEMA) - - @staticmethod - def test_phenopacket_metadata_schema(): - Draft7Validator.check_schema(PHENOPACKET_METADATA_SCHEMA) + def test_data_type_schemas(): + for d in DATA_TYPES.values(): + Draft7Validator.check_schema(d["schema"]) + Draft7Validator.check_schema(d["metadata_schema"]) From 09b660e7b2f19cad3a0f3b6e51384ea4ec0c40b5 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Tue, 19 May 2020 16:17:17 -0400 Subject: [PATCH 044/190] Add GET for table detail, add data_type to table representation --- chord_metadata_service/chord/views_search.py | 32 ++++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/chord_metadata_service/chord/views_search.py b/chord_metadata_service/chord/views_search.py index 693c46c89..ef9786bb5 100644 --- a/chord_metadata_service/chord/views_search.py +++ b/chord_metadata_service/chord/views_search.py @@ -62,6 +62,20 @@ def data_type_metadata_schema(_request, data_type: str): return Response(DATA_TYPES[DATA_TYPE_PHENOPACKET]["metadata_schema"]) +def chord_table_representation(table: Table) -> dict: + return { + "id": table.identifier, + "name": table.name, + "metadata": { + "dataset_id": table.ownership_record.dataset_id, + "created": table.created.isoformat(), + "updated": table.updated.isoformat() + }, + "data_type": table.data_type, + "schema": DATA_TYPES[table.data_type]["schema"], + } + + @api_view(["GET"]) @permission_classes([AllowAny]) def table_list(request): @@ -71,21 +85,11 @@ def table_list(request): return Response(errors.bad_request_error(f"Missing or invalid data type(s) (Specified: {data_types})"), status=400) - return Response([{ - "id": t.identifier, - "name": t.name, - "metadata": { - "dataset_id": t.ownership_record.dataset_id, - "created": t.created.isoformat(), - "updated": t.updated.isoformat() - }, - "schema": DATA_TYPES[t.data_type]["schema"], - } for t in Table.objects.filter(data_type__in=data_types)]) + return Response([chord_table_representation(t) for t in Table.objects.filter(data_type__in=data_types)]) -# TODO: Remove pragma: no cover when GET/POST implemented -# TODO: Should this exist? managed -@api_view(["DELETE"]) +# TODO: Remove pragma: no cover when POST implemented +@api_view(["GET", "DELETE"]) @permission_classes([OverrideOrSuperUserOnly]) def table_detail(request, table_id): # pragma: no cover # TODO: Implement GET, POST @@ -100,6 +104,8 @@ def table_detail(request, table_id): # pragma: no cover table.delete() return Response(status=204) + return Response(chord_table_representation(table)) + def experiment_table_summary(table): experiments = Experiment.objects.filter(table=table) # TODO From a062449f278ad066199cb382931e4eaa4c7cb3f2 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Tue, 19 May 2020 17:00:45 -0400 Subject: [PATCH 045/190] Convert service_id on tableownership to not be strictly UUID --- .../migrations/0016_auto_20200519_2100.py | 18 ++++++++++++++++++ chord_metadata_service/chord/models.py | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 chord_metadata_service/chord/migrations/0016_auto_20200519_2100.py diff --git a/chord_metadata_service/chord/migrations/0016_auto_20200519_2100.py b/chord_metadata_service/chord/migrations/0016_auto_20200519_2100.py new file mode 100644 index 000000000..f4e4459b0 --- /dev/null +++ b/chord_metadata_service/chord/migrations/0016_auto_20200519_2100.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.12 on 2020-05-19 21:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('chord', '0015_table_data_type'), + ] + + operations = [ + migrations.AlterField( + model_name='tableownership', + name='service_id', + field=models.CharField(max_length=200), + ), + ] diff --git a/chord_metadata_service/chord/models.py b/chord_metadata_service/chord/models.py index f97c29894..1f318cb9a 100644 --- a/chord_metadata_service/chord/models.py +++ b/chord_metadata_service/chord/models.py @@ -135,7 +135,7 @@ class TableOwnership(models.Model): """ table_id = models.CharField(primary_key=True, max_length=200) - service_id = models.UUIDField(max_length=200) + service_id = models.CharField(max_length=200) service_artifact = models.CharField(max_length=200, default="") data_type = models.CharField(max_length=200) # TODO: Is this needed? TODO: Remove From e81aa1add36cc1bb33141fed075d8e0fb9713046 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Tue, 19 May 2020 17:01:21 -0400 Subject: [PATCH 046/190] Allow posting to chord table endpoint to create new tables --- chord_metadata_service/chord/views_search.py | 39 ++++++++++++++++++-- chord_metadata_service/metadata/settings.py | 3 +- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/chord_metadata_service/chord/views_search.py b/chord_metadata_service/chord/views_search.py index ef9786bb5..f0f610c12 100644 --- a/chord_metadata_service/chord/views_search.py +++ b/chord_metadata_service/chord/views_search.py @@ -1,4 +1,5 @@ import itertools +import uuid from collections import Counter from datetime import datetime @@ -15,14 +16,14 @@ from chord_metadata_service.experiments.models import Experiment from chord_metadata_service.experiments.serializers import ExperimentSerializer from chord_metadata_service.metadata.elastic import es -from chord_metadata_service.metadata.settings import DEBUG +from chord_metadata_service.metadata.settings import DEBUG, CHORD_SERVICE_ARTIFACT, CHORD_SERVICE_ID from chord_metadata_service.patients.models import Individual from chord_metadata_service.phenopackets.api_views import PHENOPACKET_PREFETCH from chord_metadata_service.phenopackets.models import Phenopacket from chord_metadata_service.phenopackets.serializers import PhenopacketSerializer from .data_types import DATA_TYPE_EXPERIMENT, DATA_TYPE_PHENOPACKET, DATA_TYPES -from .models import Table +from .models import Dataset, TableOwnership, Table from .permissions import OverrideOrSuperUserOnly @@ -76,9 +77,40 @@ def chord_table_representation(table: Table) -> dict: } -@api_view(["GET"]) +@api_view(["GET", "POST"]) @permission_classes([AllowAny]) def table_list(request): + if request.method == "POST": + name = request.POST.get("name", "").strip() + data_type = request.POST.get("data_type", "") + dataset = request.POST.get("dataset") + + if name == "": + return Response(errors.bad_request_error("Missing or blank name field"), status=400) + + if data_type not in DATA_TYPES: + return Response(errors.bad_request_error(f"Invalid data type for table: {data_type}"), status=400) + + table_id = str(uuid.uuid4()) + + table_ownership = TableOwnership.objects.create( + table_id=table_id, + service_id=CHORD_SERVICE_ID, + service_artifact=CHORD_SERVICE_ARTIFACT, + data_type=data_type, + dataset=Dataset.objects.get(identifier=dataset), + ) + + table = Table.objects.create( + ownership_record=table_ownership, + name=name, + data_type=data_type, + ) + + return Response(chord_table_representation(table)) + + # GET + data_types = request.query_params.getlist("data-type") if len(data_types) == 0 or next((dt for dt in data_types if dt not in DATA_TYPES), None) is not None: @@ -104,6 +136,7 @@ def table_detail(request, table_id): # pragma: no cover table.delete() return Response(status=204) + # GET return Response(chord_table_representation(table)) diff --git a/chord_metadata_service/metadata/settings.py b/chord_metadata_service/metadata/settings.py index 0a63a9f80..468cf8ba5 100644 --- a/chord_metadata_service/metadata/settings.py +++ b/chord_metadata_service/metadata/settings.py @@ -44,7 +44,8 @@ # unless an alternative permissions system is in place. CHORD_PERMISSIONS = os.environ.get("CHORD_PERMISSIONS", str(not DEBUG)).lower() == "true" -CHORD_SERVICE_TYPE = "ca.c3g.chord:metadata:{}".format(__version__) +CHORD_SERVICE_ARTIFACT = "metadata" +CHORD_SERVICE_TYPE = f"ca.c3g.chord:{CHORD_SERVICE_ARTIFACT}:{__version__}" CHORD_SERVICE_ID = os.environ.get("SERVICE_ID", CHORD_SERVICE_TYPE) # SECURITY WARNING: don't run with AUTH_OVERRIDE turned on in production! From 8daf977168f77c2e6913dff92968ac92f27075a4 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Tue, 19 May 2020 17:23:00 -0400 Subject: [PATCH 047/190] Fix not properly parsing json body for table creation --- chord_metadata_service/chord/views_search.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/chord_metadata_service/chord/views_search.py b/chord_metadata_service/chord/views_search.py index f0f610c12..352c770f1 100644 --- a/chord_metadata_service/chord/views_search.py +++ b/chord_metadata_service/chord/views_search.py @@ -1,4 +1,5 @@ import itertools +import json import uuid from collections import Counter @@ -81,9 +82,11 @@ def chord_table_representation(table: Table) -> dict: @permission_classes([AllowAny]) def table_list(request): if request.method == "POST": - name = request.POST.get("name", "").strip() - data_type = request.POST.get("data_type", "") - dataset = request.POST.get("dataset") + request_data = json.loads(request.body) # TODO: Handle JSON errors here + + name = request_data.get("name", "").strip() + data_type = request_data.get("data_type", "") + dataset = request_data.get("dataset") if name == "": return Response(errors.bad_request_error("Missing or blank name field"), status=400) From e2d32973d6300aaed15bb14470a2109021a53641 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Tue, 19 May 2020 21:22:58 -0400 Subject: [PATCH 048/190] add fhir patient and observation to phenopackets, examples to test --- .../restapi/fhir_data_testing.py | 409 ++++++++++++++++++ chord_metadata_service/restapi/fhir_utils.py | 62 ++- 2 files changed, 461 insertions(+), 10 deletions(-) create mode 100644 chord_metadata_service/restapi/fhir_data_testing.py diff --git a/chord_metadata_service/restapi/fhir_data_testing.py b/chord_metadata_service/restapi/fhir_data_testing.py new file mode 100644 index 000000000..0558801bc --- /dev/null +++ b/chord_metadata_service/restapi/fhir_data_testing.py @@ -0,0 +1,409 @@ +FHIR_PATIENT = { + "address": [ + { + "city": "Carver", + "country": "US", + "extension": [ + { + "extension": [ + { + "url": "latitude", + "valueDecimal": 41.875179 + }, + { + "url": "longitude", + "valueDecimal": -70.74671500000002 + } + ], + "url": "http://hl7.org/fhir/StructureDefinition/geolocation" + } + ], + "line": [ + "1087 Halvorson Light" + ], + "postalCode": "02330", + "state": "Massachusetts" + } + ], + "birthDate": "1991-02-10", + "communication": [ + { + "language": { + "coding": [ + { + "code": "pt", + "display": "Portuguese", + "system": "urn:ietf:bcp:47" + } + ], + "text": "Portuguese" + } + } + ], + "extension": [ + { + "extension": [ + { + "url": "ombCategory", + "valueCoding": { + "code": "2106-3", + "display": "White", + "system": "urn:oid:2.16.840.1.113883.6.238" + } + }, + { + "url": "text", + "valueString": "White" + } + ], + "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race" + }, + { + "extension": [ + { + "url": "ombCategory", + "valueCoding": { + "code": "2186-5", + "display": "Not Hispanic or Latino", + "system": "urn:oid:2.16.840.1.113883.6.238" + } + }, + { + "url": "text", + "valueString": "Not Hispanic or Latino" + } + ], + "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/patient-mothersMaidenName", + "valueString": "Krysta658 Terry864" + }, + { + "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex", + "valueCode": "M" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/birthPlace", + "valueAddress": { + "city": "Lisbon", + "country": "PT", + "state": "Estremadura" + } + }, + { + "url": "http://synthetichealth.github.io/synthea/disability-adjusted-life-years", + "valueDecimal": 0 + }, + { + "url": "http://synthetichealth.github.io/synthea/quality-adjusted-life-years", + "valueDecimal": 27 + } + ], + "gender": "male", + "id": "6f7acde5-db81-4361-82cf-886893a3280c", + "identifier": [ + { + "system": "https://github.com/synthetichealth/synthea", + "value": "a238ebf2-392b-44be-9a17-da07a15220e2" + }, + { + "system": "http://hospital.smarthealthit.org", + "type": { + "coding": [ + { + "code": "MR", + "display": "Medical Record Number", + "system": "http://hl7.org/fhir/v2/0203" + } + ], + "text": "Medical Record Number" + }, + "value": "a238ebf2-392b-44be-9a17-da07a15220e2" + }, + { + "system": "http://hl7.org/fhir/sid/us-ssn", + "type": { + "coding": [ + { + "code": "SB", + "display": "Social Security Number", + "system": "http://hl7.org/fhir/identifier-type" + } + ], + "text": "Social Security Number" + }, + "value": "999-99-7515" + }, + { + "system": "urn:oid:2.16.840.1.113883.4.3.25", + "type": { + "coding": [ + { + "code": "DL", + "display": "Driver's License", + "system": "http://hl7.org/fhir/v2/0203" + } + ], + "text": "Driver's License" + }, + "value": "S99942098" + }, + { + "system": "http://standardhealthrecord.org/fhir/StructureDefinition/passportNumber", + "type": { + "coding": [ + { + "code": "PPN", + "display": "Passport Number", + "system": "http://hl7.org/fhir/v2/0203" + } + ], + "text": "Passport Number" + }, + "value": "X19416767X" + } + ], + "maritalStatus": { + "coding": [ + { + "code": "M", + "display": "M", + "system": "http://hl7.org/fhir/v3/MaritalStatus" + } + ], + "text": "M" + }, + "meta": { + "lastUpdated": "2019-04-09T12:25:36.451316+00:00", + "versionId": "MTU1NDgxMjczNjQ1MTMxNjAwMA" + }, + "multipleBirthBoolean": False, + "name": [ + { + "family": "Hettinger594", + "given": [ + "Gregg522" + ], + "prefix": [ + "Mr." + ], + "use": "official" + } + ], + "resourceType": "Patient", + "telecom": [ + { + "system": "phone", + "use": "home", + "value": "555-282-3544" + } + ], + "text": { + "div": "
Generated by Synthea.Version identifier: v2.2.0-56-g113d8a2d\n . Person seed: 8417064283020065324 Population seed: 5
", + "status": "generated" + } +} + + +FHIR_OBSERVATION = { + "category": [ + { + "coding": [ + { + "code": "laboratory", + "display": "laboratory", + "system": "http://hl7.org/fhir/observation-category" + } + ] + } + ], + "code": { + "coding": [ + { + "code": "785-6", + "display": "MCH [Entitic mass] by Automated count", + "system": "http://loinc.org" + } + ], + "text": "MCH [Entitic mass] by Automated count" + }, + "context": { + "reference": "Encounter/2a0e0f6c-493f-4c5b-bf89-5f98aee24f21" + }, + "effectiveDateTime": "2014-03-02T10:12:53-05:00", + "id": "1c8d2ee3-2a7e-47f9-be16-abe4e9fa306b", + "issued": "2014-03-02T10:12:53.714-05:00", + "meta": { + "lastUpdated": "2019-04-09T12:25:36.531997+00:00", + "versionId": "MTU1NDgxMjczNjUzMTk5NzAwMA" + }, + "resourceType": "Observation", + "status": "final", + "subject": { + "reference": "Patient/6f7acde5-db81-4361-82cf-886893a3280c" + }, + "valueQuantity": { + "code": "pg", + "system": "http://unitsofmeasure.org", + "unit": "pg", + "value": 27.035236044041877 + } +} + + +FHIR_OBSERVATION_BUNDLE = { + "entry": [ + { + "fullUrl": "https://syntheticmass.mitre.org/v1/fhir/Observation/97684895-3a6e-4d46-9edd-31984bc7c3a6", + "resource": { + "category": [ + { + "coding": [ + { + "code": "laboratory", + "display": "laboratory", + "system": "http://hl7.org/fhir/observation-category" + } + ] + } + ], + "code": { + "coding": [ + { + "code": "718-7", + "display": "Hemoglobin [Mass/volume] in Blood", + "system": "http://loinc.org" + } + ], + "text": "Hemoglobin [Mass/volume] in Blood" + }, + "context": { + "reference": "Encounter/2a0e0f6c-493f-4c5b-bf89-5f98aee24f21" + }, + "effectiveDateTime": "2014-03-02T10:12:53-05:00", + "id": "97684895-3a6e-4d46-9edd-31984bc7c3a6", + "issued": "2014-03-02T10:12:53.714-05:00", + "meta": { + "lastUpdated": "2019-04-09T12:25:36.531998+00:00", + "versionId": "MTU1NDgxMjczNjUzMTk5ODAwMA" + }, + "resourceType": "Observation", + "status": "final", + "subject": { + "reference": "Patient/6f7acde5-db81-4361-82cf-886893a3280c" + }, + "valueQuantity": { + "code": "g/dL", + "system": "http://unitsofmeasure.org", + "unit": "g/dL", + "value": 17.14301557752162 + } + }, + "search": { + "mode": "match" + } + }, + { + "fullUrl": "https://syntheticmass.mitre.org/v1/fhir/Observation/1c8d2ee3-2a7e-47f9-be16-abe4e9fa306b", + "resource": { + "category": [ + { + "coding": [ + { + "code": "laboratory", + "display": "laboratory", + "system": "http://hl7.org/fhir/observation-category" + } + ] + } + ], + "code": { + "coding": [ + { + "code": "785-6", + "display": "MCH [Entitic mass] by Automated count", + "system": "http://loinc.org" + } + ], + "text": "MCH [Entitic mass] by Automated count" + }, + "context": { + "reference": "Encounter/2a0e0f6c-493f-4c5b-bf89-5f98aee24f21" + }, + "effectiveDateTime": "2014-03-02T10:12:53-05:00", + "id": "1c8d2ee3-2a7e-47f9-be16-abe4e9fa306b", + "issued": "2014-03-02T10:12:53.714-05:00", + "meta": { + "lastUpdated": "2019-04-09T12:25:36.531997+00:00", + "versionId": "MTU1NDgxMjczNjUzMTk5NzAwMA" + }, + "resourceType": "Observation", + "status": "final", + "subject": { + "reference": "Patient/6f7acde5-db81-4361-82cf-886893a3280c" + }, + "valueQuantity": { + "code": "pg", + "system": "http://unitsofmeasure.org", + "unit": "pg", + "value": 27.035236044041877 + } + }, + "search": { + "mode": "match" + } + }, + { + "fullUrl": "https://syntheticmass.mitre.org/v1/fhir/Observation/b0598af4-8ffe-43ba-84e5-b7fb49d3dcd7", + "resource": { + "category": [ + { + "coding": [ + { + "code": "laboratory", + "display": "laboratory", + "system": "http://hl7.org/fhir/observation-category" + } + ] + } + ], + "code": { + "coding": [ + { + "code": "777-3", + "display": "Platelets [#/volume] in Blood by Automated count", + "system": "http://loinc.org" + } + ], + "text": "Platelets [#/volume] in Blood by Automated count" + }, + "context": { + "reference": "Encounter/2a0e0f6c-493f-4c5b-bf89-5f98aee24f21" + }, + "effectiveDateTime": "2014-03-02T10:12:53-05:00", + "id": "b0598af4-8ffe-43ba-84e5-b7fb49d3dcd7", + "issued": "2014-03-02T10:12:53.714-05:00", + "meta": { + "lastUpdated": "2019-04-09T12:25:36.530961+00:00", + "versionId": "MTU1NDgxMjczNjUzMDk2MTAwMA" + }, + "resourceType": "Observation", + "status": "final", + "subject": { + "reference": "Patient/6f7acde5-db81-4361-82cf-886893a3280c" + }, + "valueQuantity": { + "code": "10*3/uL", + "system": "http://unitsofmeasure.org", + "unit": "10*3/uL", + "value": 306.49607523265786 + } + }, + "search": { + "mode": "match" + } + } + ], + "resourceType": "Bundle" +} \ No newline at end of file diff --git a/chord_metadata_service/restapi/fhir_utils.py b/chord_metadata_service/restapi/fhir_utils.py index 2894dd888..ece81d8db 100644 --- a/chord_metadata_service/restapi/fhir_utils.py +++ b/chord_metadata_service/restapi/fhir_utils.py @@ -2,11 +2,11 @@ from chord_metadata_service.restapi.semantic_mappings.phenopackets_on_fhir_mapping import PHENOPACKETS_ON_FHIR_MAPPING from chord_metadata_service.restapi.semantic_mappings.hl7_genomics_mapping import HL7_GENOMICS_MAPPING from fhirclient.models import (observation as obs, patient as p, extension, age, coding as c, - codeableconcept, specimen as s, identifier as fhir_indentifier, - annotation as a, range, quantity, fhirreference, - documentreference, attachment, fhirdate, condition as cond, - composition as comp - ) + codeableconcept, specimen as s, identifier as fhir_indentifier, + annotation as a, range, quantity, fhirreference, + documentreference, attachment, fhirdate, condition as cond, + composition as comp + ) ##################### Generic FHIR conversion functions ##################### @@ -20,7 +20,7 @@ def fhir_coding_util(obj): coding.code = obj['id'] if 'system' in obj.keys(): coding.system = obj['system'] - return coding + return coding def fhir_codeable_concept(obj): @@ -283,10 +283,10 @@ def fhir_obs_component_region_studied(obj): component = obs.ObservationComponent() component.code = fhir_codeable_concept(HL7_GENOMICS_MAPPING['gene']['gene_studied_code']) component.valueCodeableConcept = fhir_codeable_concept({ - "id": obj['id'], - "label": obj['symbol'], - "system": HL7_GENOMICS_MAPPING['gene']['gene_studied_value']['system'] - }) + "id": obj['id'], + "label": obj['symbol'], + "system": HL7_GENOMICS_MAPPING['gene']['gene_studied_value']['system'] + }) return component.as_json() @@ -384,3 +384,45 @@ def fhir_composition(obj): composition.section.append(section_content) return composition.as_json() + + +################################# FHIR to Phenopackets ################################# +# There is no guide to map FHIR to Phenopackets + +def patient_to_individual(obj): + """ FHIR Patient to Individual. """ + patient = p.Patient(obj) + individual = { + "id": patient.id, + "alternate_ids": [alternate_id.value for alternate_id in patient.identifier] + } + gender_to_sex = { + "male": "MALE", + "female": "FEMALE", + "other": "OTHER_SEX", + "unknown": "UNKNOWN_SEX" + } + individual["sex"] = gender_to_sex.get(patient.gender, "unknown") + individual["date_of_birth"] = patient.birthDate.isostring + if patient.active: + individual["active"] = patient.active + if patient.deceasedBoolean: + individual["deceased"] = patient.deceasedBoolean + print(individual) + return individual + + +def observation_to_phenotypic_feature(obj): + """ FHIR Observation to PhenotypicFeature. """ + observation = obs.Observation(obj) + codeable_concept = observation.code #CodeableConcept + phenotypic_feature = { + "id": observation.id, + "type": { + "id": codeable_concept.coding[0].code, + "label": codeable_concept.coding[0].display + } + } + if observation.specimen: #FK to Biosample + phenotypic_feature["biosample"] = observation.specimen.reference + return phenotypic_feature From 67f38cfb590c17ba46b53b15d79029e3ef87f07b Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Tue, 19 May 2020 22:10:37 -0400 Subject: [PATCH 049/190] add condition test data and function to convert to disease --- .../restapi/fhir_data_testing.py | 222 +++++++++++++++++- chord_metadata_service/restapi/fhir_utils.py | 19 +- 2 files changed, 239 insertions(+), 2 deletions(-) diff --git a/chord_metadata_service/restapi/fhir_data_testing.py b/chord_metadata_service/restapi/fhir_data_testing.py index 0558801bc..8da8eb279 100644 --- a/chord_metadata_service/restapi/fhir_data_testing.py +++ b/chord_metadata_service/restapi/fhir_data_testing.py @@ -406,4 +406,224 @@ } ], "resourceType": "Bundle" -} \ No newline at end of file +} + + +FHIR_CONDITION = { + "abatementDateTime": "2018-09-21T11:12:53-04:00", + "assertedDate": "2018-08-22T11:12:53-04:00", + "clinicalStatus": "resolved", + "code": { + "coding": [ + { + "code": "62106007", + "display": "Concussion with no loss of consciousness", + "system": "http://snomed.info/sct" + } + ], + "text": "Concussion with no loss of consciousness" + }, + "context": { + "reference": "Encounter/1d91f8e0-74f1-4071-a681-9d4fa0f9b93a" + }, + "id": "4f2c2598-7e60-4752-b603-b330ca166829", + "meta": { + "lastUpdated": "2019-04-09T12:25:36.531999+00:00", + "versionId": "MTU1NDgxMjczNjUzMTk5OTAwMA" + }, + "onsetDateTime": "2018-08-22T11:12:53-04:00", + "resourceType": "Condition", + "subject": { + "reference": "Patient/6f7acde5-db81-4361-82cf-886893a3280c" + }, + "verificationStatus": "confirmed" +} + + +FHIR_CONDITION_BUNDLE = { + "entry": [ + { + "fullUrl": "https://syntheticmass.mitre.org/v1/fhir/Condition/4f2c2598-7e60-4752-b603-b330ca166829", + "resource": { + "abatementDateTime": "2018-09-21T11:12:53-04:00", + "assertedDate": "2018-08-22T11:12:53-04:00", + "clinicalStatus": "resolved", + "code": { + "coding": [ + { + "code": "62106007", + "display": "Concussion with no loss of consciousness", + "system": "http://snomed.info/sct" + } + ], + "text": "Concussion with no loss of consciousness" + }, + "context": { + "reference": "Encounter/1d91f8e0-74f1-4071-a681-9d4fa0f9b93a" + }, + "id": "4f2c2598-7e60-4752-b603-b330ca166829", + "meta": { + "lastUpdated": "2019-04-09T12:25:36.531999+00:00", + "versionId": "MTU1NDgxMjczNjUzMTk5OTAwMA" + }, + "onsetDateTime": "2018-08-22T11:12:53-04:00", + "resourceType": "Condition", + "subject": { + "reference": "Patient/6f7acde5-db81-4361-82cf-886893a3280c" + }, + "verificationStatus": "confirmed" + }, + "search": { + "mode": "match" + } + }, + { + "fullUrl": "https://syntheticmass.mitre.org/v1/fhir/Condition/cc454676-ccdc-4792-a5c9-cfbedce2ab33", + "resource": { + "abatementDateTime": "2011-03-19T11:12:53-04:00", + "assertedDate": "2011-02-26T10:12:53-05:00", + "clinicalStatus": "resolved", + "code": { + "coding": [ + { + "code": "444814009", + "display": "Viral sinusitis (disorder)", + "system": "http://snomed.info/sct" + } + ], + "text": "Viral sinusitis (disorder)" + }, + "context": { + "reference": "Encounter/ee9bd275-49c9-4e40-bc78-ebe53bbfb123" + }, + "id": "cc454676-ccdc-4792-a5c9-cfbedce2ab33", + "meta": { + "lastUpdated": "2019-04-09T12:25:36.525019+00:00", + "versionId": "MTU1NDgxMjczNjUyNTAxOTAwMA" + }, + "onsetDateTime": "2011-02-26T10:12:53-05:00", + "resourceType": "Condition", + "subject": { + "reference": "Patient/6f7acde5-db81-4361-82cf-886893a3280c" + }, + "verificationStatus": "confirmed" + }, + "search": { + "mode": "match" + } + }, + { + "fullUrl": "https://syntheticmass.mitre.org/v1/fhir/Condition/bab430ff-5b09-4e4a-8871-6e4fcb84fa17", + "resource": { + "assertedDate": "2009-04-05T11:12:53-04:00", + "clinicalStatus": "active", + "code": { + "coding": [ + { + "code": "38341003", + "display": "Hypertension", + "system": "http://snomed.info/sct" + } + ], + "text": "Hypertension" + }, + "context": { + "reference": "Encounter/630a642d-0402-454c-9426-c399cf9b2aab" + }, + "id": "bab430ff-5b09-4e4a-8871-6e4fcb84fa17", + "meta": { + "lastUpdated": "2019-04-09T12:25:36.486194+00:00", + "versionId": "MTU1NDgxMjczNjQ4NjE5NDAwMA" + }, + "onsetDateTime": "2009-04-05T11:12:53-04:00", + "resourceType": "Condition", + "subject": { + "reference": "Patient/6f7acde5-db81-4361-82cf-886893a3280c" + }, + "verificationStatus": "confirmed" + }, + "search": { + "mode": "match" + } + }, + { + "fullUrl": "https://syntheticmass.mitre.org/v1/fhir/Condition/25b86d4b-5d09-47c6-9446-b93b067e63ec", + "resource": { + "abatementDateTime": "2009-01-31T10:12:53-05:00", + "assertedDate": "2009-01-10T10:12:53-05:00", + "clinicalStatus": "resolved", + "code": { + "coding": [ + { + "code": "75498004", + "display": "Acute bacterial sinusitis (disorder)", + "system": "http://snomed.info/sct" + } + ], + "text": "Acute bacterial sinusitis (disorder)" + }, + "context": { + "reference": "Encounter/051b0d30-03d3-4d6d-a070-f8d363ef277f" + }, + "id": "25b86d4b-5d09-47c6-9446-b93b067e63ec", + "meta": { + "lastUpdated": "2019-04-09T12:25:36.461769+00:00", + "versionId": "MTU1NDgxMjczNjQ2MTc2OTAwMA" + }, + "onsetDateTime": "2009-01-10T10:12:53-05:00", + "resourceType": "Condition", + "subject": { + "reference": "Patient/6f7acde5-db81-4361-82cf-886893a3280c" + }, + "verificationStatus": "confirmed" + }, + "search": { + "mode": "match" + } + }, + { + "fullUrl": "https://syntheticmass.mitre.org/v1/fhir/Condition/33d0016b-3d90-4647-b797-49d5b874537b", + "resource": { + "abatementDateTime": "2009-10-27T11:12:53-04:00", + "assertedDate": "2009-10-06T11:12:53-04:00", + "clinicalStatus": "resolved", + "code": { + "coding": [ + { + "code": "444814009", + "display": "Viral sinusitis (disorder)", + "system": "http://snomed.info/sct" + } + ], + "text": "Viral sinusitis (disorder)" + }, + "context": { + "reference": "Encounter/6956bf29-4bc2-4e41-af3d-1cd3d398eb84" + }, + "id": "33d0016b-3d90-4647-b797-49d5b874537b", + "meta": { + "lastUpdated": "2019-04-09T12:25:36.478091+00:00", + "versionId": "MTU1NDgxMjczNjQ3ODA5MTAwMA" + }, + "onsetDateTime": "2009-10-06T11:12:53-04:00", + "resourceType": "Condition", + "subject": { + "reference": "Patient/6f7acde5-db81-4361-82cf-886893a3280c" + }, + "verificationStatus": "confirmed" + }, + "search": { + "mode": "match" + } + } + ], + "link": [ + { + "relation": "search", + "url": "https://syntheticmass.mitre.org/v1/fhir/Condition/?subject%3Areference=Patient%2F6f7acde5-db81-4361-82cf-886893a3280c" + } + ], + "resourceType": "Bundle", + "total": 5, + "type": "searchset" +} diff --git a/chord_metadata_service/restapi/fhir_utils.py b/chord_metadata_service/restapi/fhir_utils.py index ece81d8db..5440e25f2 100644 --- a/chord_metadata_service/restapi/fhir_utils.py +++ b/chord_metadata_service/restapi/fhir_utils.py @@ -413,7 +413,7 @@ def patient_to_individual(obj): def observation_to_phenotypic_feature(obj): - """ FHIR Observation to PhenotypicFeature. """ + """ FHIR Observation to Phenopackets PhenotypicFeature. """ observation = obs.Observation(obj) codeable_concept = observation.code #CodeableConcept phenotypic_feature = { @@ -421,8 +421,25 @@ def observation_to_phenotypic_feature(obj): "type": { "id": codeable_concept.coding[0].code, "label": codeable_concept.coding[0].display + # TODO collect system info in metadata } } if observation.specimen: #FK to Biosample phenotypic_feature["biosample"] = observation.specimen.reference return phenotypic_feature + + +def condition_to_disease(obj): + """ FHIR Condition to Phenopackets Disease. """ + condition = cond.Condition(obj) + codeable_concept = condition.code # CodeableConcept + disease = { + "id": condition.id, + "type": { + "id": codeable_concept.coding[0].code, + "label": codeable_concept.coding[0].display + # TODO collect system info in metadata + } + } + # condition.stage.type is only in FHIR 4.0.0 version + return disease From 60c1ef1e0b84ab75918c854afcd40cb798cf43fa Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Thu, 21 May 2020 10:58:05 -0400 Subject: [PATCH 050/190] Fix permissions issues with CHORD table routes --- chord_metadata_service/chord/permissions.py | 13 ++++++++++++- chord_metadata_service/chord/views_search.py | 6 +++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/chord_metadata_service/chord/permissions.py b/chord_metadata_service/chord/permissions.py index 442746306..ca4a24324 100644 --- a/chord_metadata_service/chord/permissions.py +++ b/chord_metadata_service/chord/permissions.py @@ -1,5 +1,16 @@ from django.conf import settings -from rest_framework.permissions import BasePermission +from rest_framework.permissions import BasePermission, SAFE_METHODS + + +__all__ = [ + "ReadOnly", + "OverrideOrSuperUserOnly", +] + + +class ReadOnly(BasePermission): + def has_permission(self, request, view): + return request.method in SAFE_METHODS class OverrideOrSuperUserOnly(BasePermission): diff --git a/chord_metadata_service/chord/views_search.py b/chord_metadata_service/chord/views_search.py index 352c770f1..79ee1e778 100644 --- a/chord_metadata_service/chord/views_search.py +++ b/chord_metadata_service/chord/views_search.py @@ -25,7 +25,7 @@ from .data_types import DATA_TYPE_EXPERIMENT, DATA_TYPE_PHENOPACKET, DATA_TYPES from .models import Dataset, TableOwnership, Table -from .permissions import OverrideOrSuperUserOnly +from .permissions import ReadOnly, OverrideOrSuperUserOnly @api_view(["GET"]) @@ -79,7 +79,7 @@ def chord_table_representation(table: Table) -> dict: @api_view(["GET", "POST"]) -@permission_classes([AllowAny]) +@permission_classes([OverrideOrSuperUserOnly | ReadOnly]) def table_list(request): if request.method == "POST": request_data = json.loads(request.body) # TODO: Handle JSON errors here @@ -125,7 +125,7 @@ def table_list(request): # TODO: Remove pragma: no cover when POST implemented @api_view(["GET", "DELETE"]) -@permission_classes([OverrideOrSuperUserOnly]) +@permission_classes([OverrideOrSuperUserOnly | ReadOnly]) def table_detail(request, table_id): # pragma: no cover # TODO: Implement GET, POST # TODO: Permissions: Check if user has control / more direct access over this table and/or dataset? From 1d8bedc6b1c57f485503e19afa9577c96950742a Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Thu, 21 May 2020 10:58:18 -0400 Subject: [PATCH 051/190] Add ordering to experiment search schema --- .../experiments/search_schemas.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/chord_metadata_service/experiments/search_schemas.py b/chord_metadata_service/experiments/search_schemas.py index ebc4e2676..f86e635f5 100644 --- a/chord_metadata_service/experiments/search_schemas.py +++ b/chord_metadata_service/experiments/search_schemas.py @@ -13,37 +13,37 @@ EXPERIMENT_SEARCH_SCHEMA = tag_schema_with_search_properties(schemas.EXPERIMENT_SCHEMA, { "properties": { "id": { - "search": {"database": {"field": models.Experiment._meta.pk.column}} + "search": {"order": 0, "database": {"field": models.Experiment._meta.pk.column}} }, "reference_registry_id": { - "search": search_optional_str(0, queryable="internal"), + "search": search_optional_str(1, queryable="internal"), }, "qc_flags": { "items": { "search": search_optional_str(0), }, - "search": {"database": {"type": "array"}} + "search": {"order": 2, "database": {"type": "array"}} }, "experiment_type": { - "search": search_optional_str(1, queryable="internal"), + "search": search_optional_str(3), }, "experiment_ontology": { "items": ONTOLOGY_SEARCH_SCHEMA, # TODO: Specific ontology? - "search": {"database": {"type": "jsonb"}} + "search": {"order": 4, "database": {"type": "jsonb"}} }, "molecule": { - "search": search_optional_eq(2), + "search": search_optional_eq(5), }, "molecule_ontology": { "items": ONTOLOGY_SEARCH_SCHEMA, # TODO: Specific ontology? - "search": {"database": {"type": "jsonb"}} + "search": {"order": 6, "database": {"type": "jsonb"}} }, "library_strategy": { - "search": search_optional_eq(3), + "search": search_optional_eq(7), }, # TODO: other_fields: ? "biosample": { - "search": search_optional_eq(4, queryable="internal"), + "search": search_optional_eq(8, queryable="internal"), }, }, "search": { From e801909448cf07e776b7ae0f062a8c836c543136 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 25 May 2020 09:13:30 -0400 Subject: [PATCH 052/190] Fix test --- chord_metadata_service/chord/tests/test_api_search.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/chord_metadata_service/chord/tests/test_api_search.py b/chord_metadata_service/chord/tests/test_api_search.py index 38581c5ad..52c52594a 100644 --- a/chord_metadata_service/chord/tests/test_api_search.py +++ b/chord_metadata_service/chord/tests/test_api_search.py @@ -68,7 +68,8 @@ def table_rep(table, created, updated): "created": created, "updated": updated }, - "schema": DATA_TYPES[table["data_type"]]["schema"] + "data_type": table["data_type"], + "schema": DATA_TYPES[table["data_type"]]["schema"], } @override_settings(AUTH_OVERRIDE=True) # For permissions From e72ef73d29f7de96266822ef2476de864bd08e6d Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 25 May 2020 09:19:58 -0400 Subject: [PATCH 053/190] Update requirements --- requirements.txt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/requirements.txt b/requirements.txt index 0a59ac116..16393b76a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,8 +3,8 @@ attrs==19.3.0 Babel==2.8.0 certifi==2020.4.5.1 chardet==3.0.4 -chord_lib==0.9.0 -codecov==2.0.22 +chord-lib==0.9.0 +codecov==2.1.3 colorama==0.4.3 coreapi==2.3.3 coreschema==0.0.4 @@ -27,10 +27,10 @@ Jinja2==2.11.2 jsonschema==3.2.0 Markdown==3.2.1 MarkupSafe==1.1.1 -more-itertools==8.2.0 +more-itertools==8.3.0 nose==1.3.7 openapi-codec==1.3.2 -packaging==20.3 +packaging==20.4 psycopg2-binary==2.8.5 Pygments==2.6.1 pyparsing==2.4.7 @@ -40,11 +40,11 @@ pytz==2020.1 PyYAML==5.3.1 rdflib==4.2.2 rdflib-jsonld==0.4.0 -redis==3.5.1 +redis==3.5.2 requests==2.23.0 rfc3987==1.3.8 simplejson==3.17.0 -six==1.14.0 +six==1.15.0 snowballstemmer==2.0.0 Sphinx==2.4.4 sphinx-rtd-theme==0.4.3 From 43769698cdc6b4d97de9324f31a68410b25c3f9e Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 25 May 2020 12:14:51 -0400 Subject: [PATCH 054/190] Remove some star imports in chord.tests --- chord_metadata_service/chord/tests/test_api.py | 11 +++++++++-- chord_metadata_service/chord/tests/test_api_ingest.py | 2 +- chord_metadata_service/chord/tests/test_api_search.py | 7 ++++++- chord_metadata_service/chord/tests/test_models.py | 3 ++- 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/chord_metadata_service/chord/tests/test_api.py b/chord_metadata_service/chord/tests/test_api.py index adfd9ffa2..47849606d 100644 --- a/chord_metadata_service/chord/tests/test_api.py +++ b/chord_metadata_service/chord/tests/test_api.py @@ -4,8 +4,15 @@ from django.urls import reverse from rest_framework import status from rest_framework.test import APITestCase -from .constants import * -from ..models import * +from .constants import ( + VALID_PROJECT_1, + VALID_DATA_USE_1, + valid_dataset_1, + dats_dataset, + VALID_DATS_CREATORS, + INVALID_DATS_CREATORS, +) +from ..models import Project, Dataset class CreateProjectTest(APITestCase): diff --git a/chord_metadata_service/chord/tests/test_api_ingest.py b/chord_metadata_service/chord/tests/test_api_ingest.py index ca51b81e3..25dd1eb5d 100644 --- a/chord_metadata_service/chord/tests/test_api_ingest.py +++ b/chord_metadata_service/chord/tests/test_api_ingest.py @@ -6,7 +6,7 @@ from rest_framework.test import APITestCase from uuid import uuid4 -from .constants import * +from .constants import VALID_PROJECT_1, valid_dataset_1, valid_table_1 from ..views_ingest import METADATA_WORKFLOWS diff --git a/chord_metadata_service/chord/tests/test_api_search.py b/chord_metadata_service/chord/tests/test_api_search.py index 52c52594a..c9b9cfc2f 100644 --- a/chord_metadata_service/chord/tests/test_api_search.py +++ b/chord_metadata_service/chord/tests/test_api_search.py @@ -9,7 +9,12 @@ from rest_framework.test import APITestCase from chord_metadata_service.patients.models import Individual -from chord_metadata_service.phenopackets.tests.constants import * +from chord_metadata_service.phenopackets.tests.constants import ( + VALID_PROCEDURE_1, + valid_biosample_1, + valid_biosample_2, + VALID_META_DATA_1, +) from chord_metadata_service.phenopackets.models import Biosample, MetaData, Phenopacket, Procedure from chord_metadata_service.chord.tests.es_mocks import SEARCH_SUCCESS diff --git a/chord_metadata_service/chord/tests/test_models.py b/chord_metadata_service/chord/tests/test_models.py index fc14a1477..5f11d1cc0 100644 --- a/chord_metadata_service/chord/tests/test_models.py +++ b/chord_metadata_service/chord/tests/test_models.py @@ -1,6 +1,7 @@ from django.test import TestCase from uuid import uuid4 -from ..models import * +from ..data_types import DATA_TYPE_PHENOPACKET +from ..models import Project, Dataset, TableOwnership, Table from .constants import VALID_DATA_USE_1 From 1bc9285561b408b61d5b325565c885f4e3fcbe0d Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 25 May 2020 12:16:05 -0400 Subject: [PATCH 055/190] Add basic table model test --- .../chord/tests/test_models.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/chord_metadata_service/chord/tests/test_models.py b/chord_metadata_service/chord/tests/test_models.py index 5f11d1cc0..5a6001ab9 100644 --- a/chord_metadata_service/chord/tests/test_models.py +++ b/chord_metadata_service/chord/tests/test_models.py @@ -69,3 +69,23 @@ def test_table_ownership(self): self.assertIn(t, d.table_ownership.all()) self.assertEqual(str(t), f"{str(d)} -> {t.table_id}") + + +class TableTest(TestCase): + def setUp(self) -> None: + p = Project.objects.create(title="Project 1", description="") + self.d = Dataset.objects.create(title="Dataset 1", description="", data_use=VALID_DATA_USE_1, project=p) + to = TableOwnership.objects.create( + table_id=TABLE_ID, + service_id=SERVICE_ID, + service_artifact="variant", + data_type="variant", + dataset=self.d + ) + Table.objects.create(ownership_record=to, name="Table 1", data_type=DATA_TYPE_PHENOPACKET) + + def test_table(self): + t = Table.objects.get(ownership_record_id=TABLE_ID) + self.assertEqual(t.data_type, DATA_TYPE_PHENOPACKET) + self.assertEqual(t.identifier, TABLE_ID) + self.assertEqual(t.dataset, self.d) From e341548ab8f381e40b252dc7159064a4b3402101 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 25 May 2020 12:32:34 -0400 Subject: [PATCH 056/190] Misc. cleanup --- chord_metadata_service/chord/admin.py | 7 +++- chord_metadata_service/chord/ingest.py | 10 +++--- chord_metadata_service/chord/serializers.py | 2 +- .../chord/tests/test_api_ingest.py | 36 +++++++++---------- chord_metadata_service/chord/views_ingest.py | 4 +-- 5 files changed, 31 insertions(+), 28 deletions(-) diff --git a/chord_metadata_service/chord/admin.py b/chord_metadata_service/chord/admin.py index 11f475f24..abdc96641 100644 --- a/chord_metadata_service/chord/admin.py +++ b/chord_metadata_service/chord/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from .models import * +from .models import Project, Dataset, TableOwnership, Table @admin.register(Project) @@ -16,3 +16,8 @@ class DatasetAdmin(admin.ModelAdmin): @admin.register(TableOwnership) class TableOwnershipAdmin(admin.ModelAdmin): pass + + +@admin.register(Table) +class TableAdmin(admin.ModelAdmin): + pass diff --git a/chord_metadata_service/chord/ingest.py b/chord_metadata_service/chord/ingest.py index 285108a2a..8a2c8b2c8 100644 --- a/chord_metadata_service/chord/ingest.py +++ b/chord_metadata_service/chord/ingest.py @@ -73,10 +73,10 @@ def create_phenotypic_feature(pf): description=pf.get("description", ""), pftype=pf["type"], negated=pf.get("negated", False), - severity=pf.get("severity", None), + severity=pf.get("severity"), modifier=pf.get("modifier", []), # TODO: Validate ontology term in schema... onset=pf.get("onset", None), - evidence=pf.get("evidence", None) # TODO: Separate class? + evidence=pf.get("evidence") # TODO: Separate class? ) pf_obj.save() @@ -84,7 +84,7 @@ def create_phenotypic_feature(pf): def _query_and_check_nulls(obj: dict, key: str, transform: Callable = lambda x: x): - value = obj.get(key, None) + value = obj.get(key) return {f"{key}__isnull": True} if value is None else {key: transform(value)} @@ -128,7 +128,7 @@ def ingest_phenopacket(phenopacket_data, table_id) -> pm.Phenopacket: new_phenopacket_id = phenopacket_data.get("id", str(uuid.uuid4())) - subject = phenopacket_data.get("subject", None) + subject = phenopacket_data.get("subject") phenotypic_features = phenopacket_data.get("phenotypic_features", []) biosamples = phenopacket_data.get("biosamples", []) genes = phenopacket_data.get("genes", []) @@ -209,7 +209,7 @@ def ingest_phenopacket(phenopacket_data, table_id) -> pm.Phenopacket: meta_data_obj = pm.MetaData( created_by=meta_data["created_by"], - submitted_by=meta_data.get("submitted_by", None), + submitted_by=meta_data.get("submitted_by"), phenopacket_schema_version="1.0.0-RC3", external_references=meta_data.get("external_references", []) ) diff --git a/chord_metadata_service/chord/serializers.py b/chord_metadata_service/chord/serializers.py index 52f581a05..91c44e21c 100644 --- a/chord_metadata_service/chord/serializers.py +++ b/chord_metadata_service/chord/serializers.py @@ -9,7 +9,7 @@ from .schemas import LINKED_FIELD_SETS_SCHEMA -__all__ = ["ProjectSerializer", "DatasetSerializer", "TableOwnershipSerializer"] +__all__ = ["ProjectSerializer", "DatasetSerializer", "TableOwnershipSerializer", "TableSerializer"] ############################################################# diff --git a/chord_metadata_service/chord/tests/test_api_ingest.py b/chord_metadata_service/chord/tests/test_api_ingest.py index 25dd1eb5d..19bc30c2b 100644 --- a/chord_metadata_service/chord/tests/test_api_ingest.py +++ b/chord_metadata_service/chord/tests/test_api_ingest.py @@ -10,7 +10,7 @@ from ..views_ingest import METADATA_WORKFLOWS -def generate_ingest(table_id): +def generate_phenopackets_ingest(table_id): return { "table_id": table_id, "workflow_id": "phenopackets_json", @@ -66,30 +66,28 @@ def setUp(self) -> None: self.table = r.json() @override_settings(AUTH_OVERRIDE=True) # For permissions - def test_ingest(self): + def test_phenopackets_ingest(self): # No ingestion body r = self.client.post(reverse("ingest"), content_type="application/json") self.assertEqual(r.status_code, status.HTTP_400_BAD_REQUEST) - # Invalid ingestion request - r = self.client.post(reverse("ingest"), data=json.dumps({}), content_type="application/json") - self.assertEqual(r.status_code, status.HTTP_400_BAD_REQUEST) + bad_ingest_bodies = ( + # Invalid ingest request + {}, - # Non-existent table ID - r = self.client.post(reverse("ingest"), data=json.dumps(generate_ingest(str(uuid4()))), - content_type="application/json") - self.assertEqual(r.status_code, status.HTTP_400_BAD_REQUEST) + # Non-existent table ID + generate_phenopackets_ingest(str(uuid4())), - # Non-existent workflow ID - bad_wf = generate_ingest(self.table["identifier"]) - bad_wf["workflow_id"] += "_invalid" - r = self.client.post(reverse("ingest"), data=json.dumps(bad_wf), content_type="application/json") - self.assertEqual(r.status_code, status.HTTP_400_BAD_REQUEST) + # Non-existent workflow ID + {**generate_phenopackets_ingest(self.table["identifier"]), "workflow_id": "phenopackets_json_invalid"}, - # json_document not in output - bad_wf = generate_ingest(self.table["identifier"]) - bad_wf["workflow_outputs"] = {} - r = self.client.post(reverse("ingest"), data=json.dumps(bad_wf), content_type="application/json") - self.assertEqual(r.status_code, status.HTTP_400_BAD_REQUEST) + # json_document not in output + {**generate_phenopackets_ingest(self.table["identifier"]), "workflow_outputs": {}}, + ) + + for data in bad_ingest_bodies: + print(data, flush=True) + r = self.client.post(reverse("ingest"), data=json.dumps(data), content_type="application/json") + self.assertEqual(r.status_code, status.HTTP_400_BAD_REQUEST) # TODO: More diff --git a/chord_metadata_service/chord/views_ingest.py b/chord_metadata_service/chord/views_ingest.py index 5175c575f..c2a1c345a 100644 --- a/chord_metadata_service/chord/views_ingest.py +++ b/chord_metadata_service/chord/views_ingest.py @@ -13,8 +13,8 @@ from chord_lib.responses import errors from chord_lib.workflows import get_workflow, get_workflow_resource, workflow_exists -from chord_metadata_service.chord.ingest import METADATA_WORKFLOWS, WORKFLOWS_PATH, DATA_TYPE_INGEST_FUNCTION_MAP -from chord_metadata_service.chord.models import Table +from .ingest import METADATA_WORKFLOWS, WORKFLOWS_PATH, DATA_TYPE_INGEST_FUNCTION_MAP +from .models import Table class WDLRenderer(BaseRenderer): From f56539d49d5431af2c1a83e6b36d58027eabc459 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 25 May 2020 13:49:13 -0400 Subject: [PATCH 057/190] Fix some app config names --- chord_metadata_service/chord/apps.py | 2 +- chord_metadata_service/metadata/settings.py | 4 ++-- chord_metadata_service/restapi/apps.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/chord_metadata_service/chord/apps.py b/chord_metadata_service/chord/apps.py index 76f5509a6..70f7fa3e1 100644 --- a/chord_metadata_service/chord/apps.py +++ b/chord_metadata_service/chord/apps.py @@ -2,4 +2,4 @@ class ChordConfig(AppConfig): - name = 'chord' + name = 'chord_metadata_service.chord' diff --git a/chord_metadata_service/metadata/settings.py b/chord_metadata_service/metadata/settings.py index 468cf8ba5..07ae9ba68 100644 --- a/chord_metadata_service/metadata/settings.py +++ b/chord_metadata_service/metadata/settings.py @@ -62,12 +62,12 @@ 'django.contrib.messages', 'django.contrib.staticfiles', - 'chord_metadata_service.chord', + 'chord_metadata_service.chord.apps.ChordConfig', 'chord_metadata_service.experiments.apps.ExperimentsConfig', 'chord_metadata_service.patients.apps.PatientsConfig', 'chord_metadata_service.phenopackets.apps.PhenopacketsConfig', 'chord_metadata_service.mcode.apps.McodeConfig', - 'chord_metadata_service.restapi', + 'chord_metadata_service.resources.apps.ResourcesConfig', 'rest_framework', 'django_nose', diff --git a/chord_metadata_service/restapi/apps.py b/chord_metadata_service/restapi/apps.py index a0a4de169..6ab8d482d 100644 --- a/chord_metadata_service/restapi/apps.py +++ b/chord_metadata_service/restapi/apps.py @@ -2,4 +2,4 @@ class RestapiConfig(AppConfig): - name = 'restapi' + name = 'chord_metadata_service.restapi' From a27b0ee51e1834f549554b8b4d0cf6f090b02e86 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 25 May 2020 13:49:38 -0400 Subject: [PATCH 058/190] Add initial resources app --- chord_metadata_service/metadata/settings.py | 1 + chord_metadata_service/resources/__init__.py | 0 chord_metadata_service/resources/admin.py | 3 +++ chord_metadata_service/resources/apps.py | 5 +++++ chord_metadata_service/resources/migrations/__init__.py | 0 chord_metadata_service/resources/models.py | 3 +++ chord_metadata_service/resources/tests.py | 3 +++ chord_metadata_service/resources/views.py | 3 +++ 8 files changed, 18 insertions(+) create mode 100644 chord_metadata_service/resources/__init__.py create mode 100644 chord_metadata_service/resources/admin.py create mode 100644 chord_metadata_service/resources/apps.py create mode 100644 chord_metadata_service/resources/migrations/__init__.py create mode 100644 chord_metadata_service/resources/models.py create mode 100644 chord_metadata_service/resources/tests.py create mode 100644 chord_metadata_service/resources/views.py diff --git a/chord_metadata_service/metadata/settings.py b/chord_metadata_service/metadata/settings.py index 07ae9ba68..f07ca1aff 100644 --- a/chord_metadata_service/metadata/settings.py +++ b/chord_metadata_service/metadata/settings.py @@ -68,6 +68,7 @@ 'chord_metadata_service.phenopackets.apps.PhenopacketsConfig', 'chord_metadata_service.mcode.apps.McodeConfig', 'chord_metadata_service.resources.apps.ResourcesConfig', + 'chord_metadata_service.restapi.apps.RestapiConfig', 'rest_framework', 'django_nose', diff --git a/chord_metadata_service/resources/__init__.py b/chord_metadata_service/resources/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/chord_metadata_service/resources/admin.py b/chord_metadata_service/resources/admin.py new file mode 100644 index 000000000..8c38f3f3d --- /dev/null +++ b/chord_metadata_service/resources/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/chord_metadata_service/resources/apps.py b/chord_metadata_service/resources/apps.py new file mode 100644 index 000000000..2fbdc6314 --- /dev/null +++ b/chord_metadata_service/resources/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ResourcesConfig(AppConfig): + name = 'chord_metadata_service.resources' diff --git a/chord_metadata_service/resources/migrations/__init__.py b/chord_metadata_service/resources/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/chord_metadata_service/resources/models.py b/chord_metadata_service/resources/models.py new file mode 100644 index 000000000..71a836239 --- /dev/null +++ b/chord_metadata_service/resources/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/chord_metadata_service/resources/tests.py b/chord_metadata_service/resources/tests.py new file mode 100644 index 000000000..7ce503c2d --- /dev/null +++ b/chord_metadata_service/resources/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/chord_metadata_service/resources/views.py b/chord_metadata_service/resources/views.py new file mode 100644 index 000000000..91ea44a21 --- /dev/null +++ b/chord_metadata_service/resources/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. From 1c0a2362b5703b5340ab3590877b56d47710c703 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 25 May 2020 14:08:04 -0400 Subject: [PATCH 059/190] Remove unneeded resources files Move test_descriptions to the right app --- chord_metadata_service/resources/tests.py | 3 --- chord_metadata_service/resources/views.py | 3 --- .../{phenopackets => restapi}/tests/test_descriptions.py | 0 3 files changed, 6 deletions(-) delete mode 100644 chord_metadata_service/resources/tests.py delete mode 100644 chord_metadata_service/resources/views.py rename chord_metadata_service/{phenopackets => restapi}/tests/test_descriptions.py (100%) diff --git a/chord_metadata_service/resources/tests.py b/chord_metadata_service/resources/tests.py deleted file mode 100644 index 7ce503c2d..000000000 --- a/chord_metadata_service/resources/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/chord_metadata_service/resources/views.py b/chord_metadata_service/resources/views.py deleted file mode 100644 index 91ea44a21..000000000 --- a/chord_metadata_service/resources/views.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.shortcuts import render - -# Create your views here. diff --git a/chord_metadata_service/phenopackets/tests/test_descriptions.py b/chord_metadata_service/restapi/tests/test_descriptions.py similarity index 100% rename from chord_metadata_service/phenopackets/tests/test_descriptions.py rename to chord_metadata_service/restapi/tests/test_descriptions.py From 467612731256d907c296e978ba232107e411b78d Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 25 May 2020 14:11:59 -0400 Subject: [PATCH 060/190] Move resources info to the new app --- chord_metadata_service/phenopackets/admin.py | 5 -- .../phenopackets/api_views.py | 15 +---- .../phenopackets/descriptions.py | 25 +------ chord_metadata_service/phenopackets/models.py | 24 +------ .../phenopackets/schemas.py | 35 ++-------- .../phenopackets/search_schemas.py | 32 +-------- .../phenopackets/serializers.py | 8 +-- chord_metadata_service/resources/admin.py | 7 +- chord_metadata_service/resources/api_views.py | 23 +++++++ .../resources/descriptions.py | 66 +++++++++++++++++++ chord_metadata_service/resources/models.py | 28 +++++++- chord_metadata_service/resources/schemas.py | 35 ++++++++++ .../resources/search_schemas.py | 41 ++++++++++++ .../resources/serializers.py | 12 ++++ .../resources/tests/__init__.py | 0 .../restapi/tests/test_descriptions.py | 2 +- 16 files changed, 220 insertions(+), 138 deletions(-) create mode 100644 chord_metadata_service/resources/api_views.py create mode 100644 chord_metadata_service/resources/descriptions.py create mode 100644 chord_metadata_service/resources/schemas.py create mode 100644 chord_metadata_service/resources/search_schemas.py create mode 100644 chord_metadata_service/resources/serializers.py create mode 100644 chord_metadata_service/resources/tests/__init__.py diff --git a/chord_metadata_service/phenopackets/admin.py b/chord_metadata_service/phenopackets/admin.py index fee3b0e35..531f85ca1 100644 --- a/chord_metadata_service/phenopackets/admin.py +++ b/chord_metadata_service/phenopackets/admin.py @@ -2,11 +2,6 @@ from .models import * -@admin.register(Resource) -class ResourceAdmin(admin.ModelAdmin): - pass - - @admin.register(MetaData) class MetaDataAdmin(admin.ModelAdmin): pass diff --git a/chord_metadata_service/phenopackets/api_views.py b/chord_metadata_service/phenopackets/api_views.py index 57b2ad0a8..f4c4a65c3 100644 --- a/chord_metadata_service/phenopackets/api_views.py +++ b/chord_metadata_service/phenopackets/api_views.py @@ -4,7 +4,7 @@ from rest_framework.permissions import AllowAny from rest_framework.response import Response -from chord_metadata_service.restapi.api_renderers import * +from chord_metadata_service.restapi.api_renderers import PhenopacketsRenderer, FHIRRenderer from chord_metadata_service.restapi.pagination import LargeResultsSetPagination from chord_metadata_service.phenopackets.schemas import PHENOPACKET_SCHEMA from .models import * @@ -98,19 +98,6 @@ class DiseaseViewSet(ExtendedPhenopacketsModelViewSet): serializer_class = DiseaseSerializer -class ResourceViewSet(PhenopacketsModelViewSet): - """ - get: - Return a list of all existing resources - - post: - Create a new resource - - """ - queryset = Resource.objects.all().order_by("id") - serializer_class = ResourceSerializer - - META_DATA_PREFETCH = ( "resources", ) diff --git a/chord_metadata_service/phenopackets/descriptions.py b/chord_metadata_service/phenopackets/descriptions.py index 036885ce2..d39b7b82b 100644 --- a/chord_metadata_service/phenopackets/descriptions.py +++ b/chord_metadata_service/phenopackets/descriptions.py @@ -36,36 +36,13 @@ from chord_metadata_service.patients.descriptions import INDIVIDUAL +from chord_metadata_service.resources.descriptions import RESOURCE from chord_metadata_service.restapi.description_utils import EXTRA_PROPERTIES, ontology_class # If description and help are specified separately, the Django help text differs from the schema description. Otherwise, # the data type is a string which fills both roles. -RESOURCE = { - "description": "A description of an external resource used for referencing an object.", - "properties": { - "id": { - "description": "Unique researcher-specified identifier for the resource.", - "help": "For OBO ontologies, the value of this string MUST always be the official OBO ID, which is always " - "equivalent to the ID prefix in lower case. For other resources use the prefix in " - "identifiers.org." - }, - "name": { - "description": "Human-readable name for the resource.", - "help": "The full name of the resource or ontology referred to by the id element." - }, - "namespace_prefix": "Prefix for objects from this resource. In the case of ontology resources, this should be " - "the CURIE prefix.", - "url": "Resource URL. In the case of ontologies, this should be an OBO or OWL file. Other resources should " - "link to the official or top-level url.", - "version": "The version of the resource or ontology used to make the annotation.", - "iri_prefix": "The IRI prefix, when used with the namespace prefix and an object ID, should resolve the term " - "or object from the resource in question.", - **EXTRA_PROPERTIES - } -} - EXTERNAL_REFERENCE = { "description": "An encoding of information about a reference to an external resource.", "properties": { diff --git a/chord_metadata_service/phenopackets/models.py b/chord_metadata_service/phenopackets/models.py index 072e7957e..02208b11c 100644 --- a/chord_metadata_service/phenopackets/models.py +++ b/chord_metadata_service/phenopackets/models.py @@ -3,6 +3,7 @@ from django.core.exceptions import ValidationError from django.contrib.postgres.fields import JSONField, ArrayField from chord_metadata_service.patients.models import Individual +from chord_metadata_service.resources.models import Resource from chord_metadata_service.restapi.description_utils import rec_help from chord_metadata_service.restapi.models import IndexableMixin from chord_metadata_service.restapi.schema_utils import schema_list @@ -29,29 +30,6 @@ ############################################################# -class Resource(models.Model): - """ - Class to represent a description of an external resource - used for referencing an object - - FHIR: CodeSystem - """ - - # resource_id e.g. "id": "uniprot" - id = models.CharField(primary_key=True, max_length=200, help_text=rec_help(d.RESOURCE, "id")) - name = models.CharField(max_length=200, help_text=rec_help(d.RESOURCE, "name")) - namespace_prefix = models.CharField(max_length=200, help_text=rec_help(d.RESOURCE, "namespace_prefix")) - url = models.URLField(max_length=200, help_text=rec_help(d.RESOURCE, "url")) - version = models.CharField(max_length=200, help_text=rec_help(d.RESOURCE, "version")) - iri_prefix = models.URLField(max_length=200, help_text=rec_help(d.RESOURCE, "iri_prefix")) - extra_properties = JSONField(blank=True, null=True, help_text=rec_help(d.RESOURCE, "extra_properties")) - created = models.DateTimeField(auto_now=True) - updated = models.DateTimeField(auto_now_add=True) - - def __str__(self): - return str(self.id) - - class MetaData(models.Model): """ Class to store structured definitions of the resources diff --git a/chord_metadata_service/phenopackets/schemas.py b/chord_metadata_service/phenopackets/schemas.py index a373ecc4e..33817646b 100644 --- a/chord_metadata_service/phenopackets/schemas.py +++ b/chord_metadata_service/phenopackets/schemas.py @@ -1,7 +1,7 @@ # Individual schemas for validation of JSONField values -import chord_metadata_service.phenopackets.descriptions as descriptions from chord_metadata_service.patients.schemas import INDIVIDUAL_SCHEMA +from chord_metadata_service.resources.schemas import RESOURCE_SCHEMA from chord_metadata_service.restapi.description_utils import describe_schema from chord_metadata_service.restapi.schemas import ( AGE, @@ -11,11 +11,12 @@ ONTOLOGY_CLASS, ) +from . import descriptions + __all__ = [ "ALLELE_SCHEMA", "PHENOPACKET_EXTERNAL_REFERENCE_SCHEMA", - "PHENOPACKET_RESOURCE_SCHEMA", "PHENOPACKET_UPDATE_SCHEMA", "PHENOPACKET_META_DATA_SCHEMA", "PHENOPACKET_EVIDENCE_SCHEMA", @@ -86,34 +87,6 @@ }, descriptions.EXTERNAL_REFERENCE) -PHENOPACKET_RESOURCE_SCHEMA = describe_schema({ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", # TODO - "properties": { - "id": { - "type": "string", - }, - "name": { - "type": "string", - }, - "namespace_prefix": { - "type": "string", - }, - "url": { - "type": "string", - }, - "version": { - "type": "string", - }, - "iri_prefix": { - "type": "string", - }, - "extra_properties": EXTRA_PROPERTIES_SCHEMA - }, - "required": ["id", "name", "namespace_prefix", "url", "version", "iri_prefix"], -}, descriptions.RESOURCE) - - PHENOPACKET_UPDATE_SCHEMA = describe_schema({ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "chord_metadata_service:update_schema", @@ -153,7 +126,7 @@ }, "resources": { "type": "array", - "items": PHENOPACKET_RESOURCE_SCHEMA, + "items": RESOURCE_SCHEMA, }, "updates": { "type": "array", diff --git a/chord_metadata_service/phenopackets/search_schemas.py b/chord_metadata_service/phenopackets/search_schemas.py index f2163c43e..66f1f479a 100644 --- a/chord_metadata_service/phenopackets/search_schemas.py +++ b/chord_metadata_service/phenopackets/search_schemas.py @@ -1,5 +1,6 @@ from . import models, schemas 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 ( search_optional_eq, search_optional_str, @@ -86,37 +87,6 @@ def _tag_with_database_attrs(schema: dict, db_attrs: dict): }, }) -RESOURCE_SEARCH_SCHEMA = tag_schema_with_search_properties(schemas.PHENOPACKET_RESOURCE_SCHEMA, { - "properties": { - "id": { - "search": search_optional_str(0) - }, - "name": { - "search": search_optional_str(1, multiple=True) - }, - "namespace_prefix": { - "search": search_optional_str(2, multiple=True) - }, - "url": { - "search": search_optional_str(3, multiple=True) - }, - "version": { - "search": search_optional_str(4, multiple=True) - }, - "iri_prefix": { - "search": search_optional_str(5, multiple=True) - } - }, - "search": { - "database": { - "relationship": { - "type": "MANY_TO_ONE", - "foreign_key": "resource_id" # TODO: No hard-code, from M2M - } - } - } -}) - UPDATE_SEARCH_SCHEMA = tag_schema_with_search_properties(schemas.PHENOPACKET_UPDATE_SCHEMA, { "properties": { # TODO: timestamp diff --git a/chord_metadata_service/phenopackets/serializers.py b/chord_metadata_service/phenopackets/serializers.py index 4c1a0fe16..f2437556c 100644 --- a/chord_metadata_service/phenopackets/serializers.py +++ b/chord_metadata_service/phenopackets/serializers.py @@ -1,7 +1,6 @@ import re from rest_framework import serializers from .models import ( - Resource, MetaData, PhenotypicFeature, Procedure, @@ -15,12 +14,12 @@ Diagnosis, Interpretation, ) +from chord_metadata_service.resources.serializers import ResourceSerializer from chord_metadata_service.restapi import fhir_utils from chord_metadata_service.restapi.serializers import GenericSerializer __all__ = [ - "ResourceSerializer", "MetaDataSerializer", "PhenotypicFeatureSerializer", "ProcedureSerializer", @@ -43,11 +42,6 @@ # # ############################################################# -class ResourceSerializer(GenericSerializer): - class Meta: - model = Resource - fields = '__all__' - class MetaDataSerializer(GenericSerializer): resources = ResourceSerializer(read_only=True, many=True) diff --git a/chord_metadata_service/resources/admin.py b/chord_metadata_service/resources/admin.py index 8c38f3f3d..a646858bd 100644 --- a/chord_metadata_service/resources/admin.py +++ b/chord_metadata_service/resources/admin.py @@ -1,3 +1,8 @@ from django.contrib import admin -# Register your models here. +from .models import Resource + + +@admin.register(Resource) +class ResourceAdmin(admin.ModelAdmin): + pass diff --git a/chord_metadata_service/resources/api_views.py b/chord_metadata_service/resources/api_views.py new file mode 100644 index 000000000..4c80377bf --- /dev/null +++ b/chord_metadata_service/resources/api_views.py @@ -0,0 +1,23 @@ +from rest_framework import viewsets +from rest_framework.settings import api_settings + +from chord_metadata_service.restapi.api_renderers import PhenopacketsRenderer +from chord_metadata_service.restapi.pagination import LargeResultsSetPagination + +from .models import Resource +from .serializers import ResourceSerializer + + +class ResourceViewSet(viewsets.ModelViewSet): + """ + get: + Return a list of all existing resources + + post: + Create a new resource + + """ + queryset = Resource.objects.all().order_by("id") + serializer_class = ResourceSerializer + renderer_classes = (*api_settings.DEFAULT_RENDERER_CLASSES, PhenopacketsRenderer) + pagination_class = LargeResultsSetPagination diff --git a/chord_metadata_service/resources/descriptions.py b/chord_metadata_service/resources/descriptions.py new file mode 100644 index 000000000..0814b73de --- /dev/null +++ b/chord_metadata_service/resources/descriptions.py @@ -0,0 +1,66 @@ +# Portions of this text copyright (c) 2019-2020 the Canadian Centre for Computational Genomics; licensed under the +# GNU Lesser General Public License version 3. + +# Portions of this text (c) 2019 Julius OB Jacobsen, Peter N Robinson, Christopher J Mungall; taken from the +# Phenopackets documentation: https://phenopackets-schema.readthedocs.io +# Licensed under the BSD 3-Clause License: +# BSD 3-Clause License +# +# Portions Copyright (c) 2018, PhenoPackets +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# * Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +from chord_metadata_service.restapi.description_utils import EXTRA_PROPERTIES + + +__all__ = ["RESOURCE"] + + +RESOURCE = { + "description": "A description of an external resource used for referencing an object.", + "properties": { + "id": { + "description": "Unique researcher-specified identifier for the resource.", + "help": "For OBO ontologies, the value of this string MUST always be the official OBO ID, which is always " + "equivalent to the ID prefix in lower case. For other resources use the prefix in " + "identifiers.org." + }, + "name": { + "description": "Human-readable name for the resource.", + "help": "The full name of the resource or ontology referred to by the id element." + }, + "namespace_prefix": "Prefix for objects from this resource. In the case of ontology resources, this should be " + "the CURIE prefix.", + "url": "Resource URL. In the case of ontologies, this should be an OBO or OWL file. Other resources should " + "link to the official or top-level url.", + "version": "The version of the resource or ontology used to make the annotation.", + "iri_prefix": "The IRI prefix, when used with the namespace prefix and an object ID, should resolve the term " + "or object from the resource in question.", + **EXTRA_PROPERTIES + } +} diff --git a/chord_metadata_service/resources/models.py b/chord_metadata_service/resources/models.py index 71a836239..30d14751d 100644 --- a/chord_metadata_service/resources/models.py +++ b/chord_metadata_service/resources/models.py @@ -1,3 +1,29 @@ +from django.contrib.postgres.fields import JSONField from django.db import models -# Create your models here. +from chord_metadata_service.restapi.description_utils import rec_help + +from . import descriptions as d + + +class Resource(models.Model): + """ + Class to represent a description of an external resource + used for referencing an object + + FHIR: CodeSystem + """ + + # resource_id e.g. "id": "uniprot" + id = models.CharField(primary_key=True, max_length=200, help_text=rec_help(d.RESOURCE, "id")) + name = models.CharField(max_length=200, help_text=rec_help(d.RESOURCE, "name")) + namespace_prefix = models.CharField(max_length=200, help_text=rec_help(d.RESOURCE, "namespace_prefix")) + url = models.URLField(max_length=200, help_text=rec_help(d.RESOURCE, "url")) + version = models.CharField(max_length=200, help_text=rec_help(d.RESOURCE, "version")) + iri_prefix = models.URLField(max_length=200, help_text=rec_help(d.RESOURCE, "iri_prefix")) + extra_properties = JSONField(blank=True, null=True, help_text=rec_help(d.RESOURCE, "extra_properties")) + created = models.DateTimeField(auto_now=True) + updated = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return str(self.id) diff --git a/chord_metadata_service/resources/schemas.py b/chord_metadata_service/resources/schemas.py new file mode 100644 index 000000000..bb12d2134 --- /dev/null +++ b/chord_metadata_service/resources/schemas.py @@ -0,0 +1,35 @@ +from chord_metadata_service.restapi.description_utils import describe_schema +from chord_metadata_service.restapi.schemas import EXTRA_PROPERTIES_SCHEMA + +from . import descriptions + + +__all__ = ["RESOURCE_SCHEMA"] + + +RESOURCE_SCHEMA = describe_schema({ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", # TODO + "properties": { + "id": { + "type": "string", + }, + "name": { + "type": "string", + }, + "namespace_prefix": { + "type": "string", + }, + "url": { + "type": "string", + }, + "version": { + "type": "string", + }, + "iri_prefix": { + "type": "string", + }, + "extra_properties": EXTRA_PROPERTIES_SCHEMA + }, + "required": ["id", "name", "namespace_prefix", "url", "version", "iri_prefix"], +}, descriptions.RESOURCE) diff --git a/chord_metadata_service/resources/search_schemas.py b/chord_metadata_service/resources/search_schemas.py new file mode 100644 index 000000000..2dd69099a --- /dev/null +++ b/chord_metadata_service/resources/search_schemas.py @@ -0,0 +1,41 @@ +from chord_metadata_service.restapi.schema_utils import ( + search_optional_str, + tag_schema_with_search_properties, +) + +from . import schemas + + +__all__ = ["RESOURCE_SEARCH_SCHEMA"] + + +RESOURCE_SEARCH_SCHEMA = tag_schema_with_search_properties(schemas.RESOURCE_SCHEMA, { + "properties": { + "id": { + "search": search_optional_str(0) + }, + "name": { + "search": search_optional_str(1, multiple=True) + }, + "namespace_prefix": { + "search": search_optional_str(2, multiple=True) + }, + "url": { + "search": search_optional_str(3, multiple=True) + }, + "version": { + "search": search_optional_str(4, multiple=True) + }, + "iri_prefix": { + "search": search_optional_str(5, multiple=True) + } + }, + "search": { + "database": { + "relationship": { + "type": "MANY_TO_ONE", # TODO: Only in some cases - phenopacket + "foreign_key": "resource_id" # TODO: No hard-code, from M2M + } + } + } +}) diff --git a/chord_metadata_service/resources/serializers.py b/chord_metadata_service/resources/serializers.py new file mode 100644 index 000000000..d761c379f --- /dev/null +++ b/chord_metadata_service/resources/serializers.py @@ -0,0 +1,12 @@ +from chord_metadata_service.restapi.serializers import GenericSerializer + +from .models import Resource + + +__all__ = ["ResourceSerializer"] + + +class ResourceSerializer(GenericSerializer): + class Meta: + model = Resource + fields = '__all__' diff --git a/chord_metadata_service/resources/tests/__init__.py b/chord_metadata_service/resources/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/chord_metadata_service/restapi/tests/test_descriptions.py b/chord_metadata_service/restapi/tests/test_descriptions.py index fb271aaf0..e31cb1884 100644 --- a/chord_metadata_service/restapi/tests/test_descriptions.py +++ b/chord_metadata_service/restapi/tests/test_descriptions.py @@ -1,5 +1,5 @@ from django.test import TestCase -from chord_metadata_service.restapi import description_utils as du +from . import description_utils as du TEST_SCHEMA_1 = {"type": "string"} From aacdf8f836cacaa196225378e73e9be7a4b0f88b Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Mon, 25 May 2020 15:41:44 -0400 Subject: [PATCH 061/190] add fhir ingest function --- .../restapi/fhir_ingest_utils.py | 178 ++++++++++++++++++ chord_metadata_service/restapi/fhir_utils.py | 59 ------ chord_metadata_service/restapi/urls.py | 4 +- 3 files changed, 181 insertions(+), 60 deletions(-) create mode 100644 chord_metadata_service/restapi/fhir_ingest_utils.py diff --git a/chord_metadata_service/restapi/fhir_ingest_utils.py b/chord_metadata_service/restapi/fhir_ingest_utils.py new file mode 100644 index 000000000..10585f4b3 --- /dev/null +++ b/chord_metadata_service/restapi/fhir_ingest_utils.py @@ -0,0 +1,178 @@ +import json +import uuid + +from fhirclient.models import observation as obs, patient as p, condition as cond, specimen as s + +from rest_framework.decorators import api_view, permission_classes +from rest_framework.permissions import AllowAny +from rest_framework.response import Response +from chord_lib.responses.errors import * + +from chord_metadata_service.chord.models import * +from chord_metadata_service.phenopackets.models import * + +FHIR_INGEST = { + "dataset_id": "", + "patients": "", + "observations": "", + "conditions": "", + "specimens": "" +} + + +@api_view(["POST"]) +@permission_classes([AllowAny]) +def ingest_fhir(request): + if not request.data["dataset_id"]: + return Response(bad_request_error(f"Dataset ID is required."), status=404) + else: + if not Dataset.objects.filter(identifier=request.data["dataset_id"]).exists(): + return Response(bad_request_error(f"Dataset with ID {request.data['dataset_id']} does not exist"), status=400) + + if not request.data["patients"]: + return Response(bad_request_error(f"Patients data is required."), status=404) + + + # create new phenopacket for each individual, it uuid, subject, meta_data and dataset_id + # fake metadata + meta_data_obj = MetaData.objects.create( + created_by="unknown", + submitted_by="unknown", + phenopacket_schema_version="1.0.0-RC3", + external_references=[] + ) + + with open(request.data["patients"], "r") as p_file: + try: + patients_data = json.load(p_file) + if isinstance(patients_data, dict): + # List of FHIR resources is of ResourceType "Bundle" + for item in patients_data["entry"]: + individual_data = patient_to_individual(item["resource"]) + individual, _ = Individual.objects.get_or_create(**individual_data) + phenopacket = Phenopacket.objects.create( + id=str(uuid.uuid4()), + subject=individual, + meta_data=meta_data_obj, + dataset=Dataset.objects.get(identifier=request.data["dataset_id"]) + ) + print(f'Phenopacket {phenopacket.id} created') + except json.decoder.JSONDecodeError as e: + return Response(bad_request_error(f"Invalid JSON provided (message: {e})"), status=400) + + + return Response(status=204) + + +################################# FHIR to Phenopackets ################################# +# There is no guide to map FHIR to Phenopackets + +def patient_to_individual(obj): + """ FHIR Patient to Individual. """ + + patient = p.Patient(obj) + individual = { + "id": patient.id + } + if patient.identifier: + individual["alternate_ids"] = [alternate_id.value for alternate_id in patient.identifier] + gender_to_sex = { + "male": "MALE", + "female": "FEMALE", + "other": "OTHER_SEX", + "unknown": "UNKNOWN_SEX" + } + if patient.gender: + individual["sex"] = gender_to_sex.get(patient.gender, "unknown") + if patient.birthDate: + individual["date_of_birth"] = patient.birthDate.isostring + if patient.active: + individual["active"] = patient.active + if patient.deceasedBoolean: + individual["deceased"] = patient.deceasedBoolean + return individual + + +def observation_to_phenotypic_feature(obj): + """ FHIR Observation to Phenopackets PhenotypicFeature. """ + + observation = obs.Observation(obj) + codeable_concept = observation.code # CodeableConcept + phenotypic_feature = { + # id is an integer AutoField, store legacy id in description + "description": observation.id, + "pftype": { + "id": codeable_concept.coding[0].code, + "label": codeable_concept.coding[0].display + # TODO collect system info in metadata + } + } + if observation.specimen: # FK to Biosample + phenotypic_feature["biosample"] = observation.specimen.reference + return phenotypic_feature + + +def condition_to_disease(obj): + """ FHIR Condition to Phenopackets Disease. """ + + condition = cond.Condition(obj) + codeable_concept = condition.code # CodeableConcept + disease = { + "id": condition.id, + "type": { + "id": codeable_concept.coding[0].code, + "label": codeable_concept.coding[0].display + # TODO collect system info in metadata + } + } + # condition.stage.type is only in FHIR 4.0.0 version + return disease + + +def diagnostic_report_to_interpretation(obj): + """ FHIR DiagnosticReport to Phenopackets Interpretation. """ + # it hardly maps at all + return + + +def procedure_to_procedure(obj): + """ FHIR Procedure to Phenopackets Procedure. + The main semantic difference: + - phenopackets procedure is a procedure performed to extract a biosample; + - fhir procedure is a procedure performed on or for a patient + (e.g. documentation of patient's medication) + """ + + return + + +def specimen_to_biosample(obj): + """ FHIR Specimen to Phenopackets Biosample. """ + + specimen = s.Specimen(obj) + biosample = { + "id": specimen.id + } + if specimen.subject: + biosample["individual"] = specimen.subject.reference + if specimen.type: + codeable_concept = specimen.type # CodeableConcept + biosample["sampled_tissue"] = { + "id": codeable_concept.coding[0].code, + "label": codeable_concept.coding[0].display + # TODO collect system info in metadata + } + if specimen.collection: + method_codeable_concept = specimen.collection.method + bodysite_codeable_concept = specimen.collection.bodySite + biosample["procedure"] = { + "code": { + "id": method_codeable_concept.coding[0].code, + "label": method_codeable_concept.coding[0].display + }, + "body_site": { + "id": bodysite_codeable_concept.coding[0].code, + "label": bodysite_codeable_concept.coding[0].display + } + } + return biosample diff --git a/chord_metadata_service/restapi/fhir_utils.py b/chord_metadata_service/restapi/fhir_utils.py index 5440e25f2..60dcf8d35 100644 --- a/chord_metadata_service/restapi/fhir_utils.py +++ b/chord_metadata_service/restapi/fhir_utils.py @@ -384,62 +384,3 @@ def fhir_composition(obj): composition.section.append(section_content) return composition.as_json() - - -################################# FHIR to Phenopackets ################################# -# There is no guide to map FHIR to Phenopackets - -def patient_to_individual(obj): - """ FHIR Patient to Individual. """ - patient = p.Patient(obj) - individual = { - "id": patient.id, - "alternate_ids": [alternate_id.value for alternate_id in patient.identifier] - } - gender_to_sex = { - "male": "MALE", - "female": "FEMALE", - "other": "OTHER_SEX", - "unknown": "UNKNOWN_SEX" - } - individual["sex"] = gender_to_sex.get(patient.gender, "unknown") - individual["date_of_birth"] = patient.birthDate.isostring - if patient.active: - individual["active"] = patient.active - if patient.deceasedBoolean: - individual["deceased"] = patient.deceasedBoolean - print(individual) - return individual - - -def observation_to_phenotypic_feature(obj): - """ FHIR Observation to Phenopackets PhenotypicFeature. """ - observation = obs.Observation(obj) - codeable_concept = observation.code #CodeableConcept - phenotypic_feature = { - "id": observation.id, - "type": { - "id": codeable_concept.coding[0].code, - "label": codeable_concept.coding[0].display - # TODO collect system info in metadata - } - } - if observation.specimen: #FK to Biosample - phenotypic_feature["biosample"] = observation.specimen.reference - return phenotypic_feature - - -def condition_to_disease(obj): - """ FHIR Condition to Phenopackets Disease. """ - condition = cond.Condition(obj) - codeable_concept = condition.code # CodeableConcept - disease = { - "id": condition.id, - "type": { - "id": codeable_concept.coding[0].code, - "label": codeable_concept.coding[0].display - # TODO collect system info in metadata - } - } - # condition.stage.type is only in FHIR 4.0.0 version - return disease diff --git a/chord_metadata_service/restapi/urls.py b/chord_metadata_service/restapi/urls.py index bc03d97ed..def48b2a3 100644 --- a/chord_metadata_service/restapi/urls.py +++ b/chord_metadata_service/restapi/urls.py @@ -5,7 +5,7 @@ from chord_metadata_service.patients import api_views as individual_views from chord_metadata_service.phenopackets import api_views as phenopacket_views from chord_metadata_service.mcode import api_views as mcode_views - +from .fhir_ingest_utils import ingest_fhir # from .settings import DEBUG @@ -55,4 +55,6 @@ name="experiment-schema"), path('mcode_schema', mcode_views.get_mcode_schema, name="mcode-schema"), + # ingest fhir + path('fhir/ingest', ingest_fhir, name="ingest-fhir"), ] From f13c1b577066db3ccbce69dccace90476ad923f3 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Mon, 25 May 2020 15:44:32 -0400 Subject: [PATCH 062/190] add observations to ingest --- .../restapi/fhir_ingest_utils.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/chord_metadata_service/restapi/fhir_ingest_utils.py b/chord_metadata_service/restapi/fhir_ingest_utils.py index 10585f4b3..14290e36c 100644 --- a/chord_metadata_service/restapi/fhir_ingest_utils.py +++ b/chord_metadata_service/restapi/fhir_ingest_utils.py @@ -60,6 +60,22 @@ def ingest_fhir(request): except json.decoder.JSONDecodeError as e: return Response(bad_request_error(f"Invalid JSON provided (message: {e})"), status=400) + with open(request.data["observations"], "r") as obs_file: + try: + observations_data = json.load(obs_file) + for item in observations_data["entry"]: + phenotypic_feature_data = observation_to_phenotypic_feature(item["resource"]) + if not item["resource"]["subject"]: + return Response(bad_request_error(f"Subject is required."), status=404) + # FHIR test data has reference object in a format "ResourceType/uuid" + subject = item["resource"]["subject"]["reference"].split('Patient/')[1] # Individual ID + phenotypic_feature, _ = PhenotypicFeature.objects.get_or_create( + phenopacket=Phenopacket.objects.get(subject=Individual.objects.get(id=subject)), + **phenotypic_feature_data + ) + except json.decoder.JSONDecodeError as e: + return Response(bad_request_error(f"Invalid JSON provided (message: {e})"), status=400) + return Response(status=204) From 2be8c136b073934da74a9e419631d8026d8cff49 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Mon, 25 May 2020 16:14:17 -0400 Subject: [PATCH 063/190] add condition to ingest --- .../restapi/fhir_ingest_utils.py | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/chord_metadata_service/restapi/fhir_ingest_utils.py b/chord_metadata_service/restapi/fhir_ingest_utils.py index 14290e36c..31b5bcc42 100644 --- a/chord_metadata_service/restapi/fhir_ingest_utils.py +++ b/chord_metadata_service/restapi/fhir_ingest_utils.py @@ -76,6 +76,23 @@ def ingest_fhir(request): except json.decoder.JSONDecodeError as e: return Response(bad_request_error(f"Invalid JSON provided (message: {e})"), status=400) + with open(request.data["conditions"]) as c_file: + try: + conditions_data = json.load(c_file) + for item in conditions_data["entry"]: + disease_data = condition_to_disease(item["resource"]) + disease = Disease.objects.create(**disease_data) + if not item["resource"]["subject"]: + return Response(bad_request_error(f"Subject is required."), status=404) + # FHIR test data has reference object in a format "ResourceType/uuid" + subject = item["resource"]["subject"]["reference"].split('Patient/')[1] # Individual ID + # a1.publications.add(p1) + phenopacket = Phenopacket.objects.get(subject=Individual.objects.get(id=subject)) + phenopacket.diseases.add(disease) + + except json.decoder.JSONDecodeError as e: + return Response(bad_request_error(f"Invalid JSON provided (message: {e})"), status=400) + return Response(status=204) @@ -134,8 +151,9 @@ def condition_to_disease(obj): condition = cond.Condition(obj) codeable_concept = condition.code # CodeableConcept disease = { - "id": condition.id, - "type": { + # id is an integer AutoField, legacy id can be a string + # "id": condition.id, + "term": { "id": codeable_concept.coding[0].code, "label": codeable_concept.coding[0].display # TODO collect system info in metadata From e7b12bc4a093a9d61ed654af73a9db753d79ded9 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Mon, 25 May 2020 16:41:06 -0400 Subject: [PATCH 064/190] add specimen to ingest --- .../restapi/fhir_ingest_utils.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/chord_metadata_service/restapi/fhir_ingest_utils.py b/chord_metadata_service/restapi/fhir_ingest_utils.py index 31b5bcc42..f4b81d9e6 100644 --- a/chord_metadata_service/restapi/fhir_ingest_utils.py +++ b/chord_metadata_service/restapi/fhir_ingest_utils.py @@ -93,6 +93,23 @@ def ingest_fhir(request): except json.decoder.JSONDecodeError as e: return Response(bad_request_error(f"Invalid JSON provided (message: {e})"), status=400) + with open(request.data["specimens"], "r") as s_file: + try: + specimens_data = json.load(s_file) + for item in specimens_data["entry"]: + biosample_data = specimen_to_biosample(item["resource"]) + procedure, _ = Procedure.objects.get_or_create(**biosample_data["procedure"]) + individual_id = biosample_data["individual"].split('Patient/')[1] # Individual ID + Biosample.objects.create( + id=biosample_data["id"], + procedure=procedure, + individual=Individual.objects.get(id=individual_id), + sampled_tissue=biosample_data["sampled_tissue"] + ) + + except json.decoder.JSONDecodeError as e: + return Response(bad_request_error(f"Invalid JSON provided (message: {e})"), status=400) + return Response(status=204) From 0d7103591fe0d3b0d7cd407dc3d84be404c1573c Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Mon, 25 May 2020 19:48:20 -0400 Subject: [PATCH 065/190] add fhir ingest schema --- .../restapi/fhir_ingest_utils.py | 168 +++++++++++------- 1 file changed, 100 insertions(+), 68 deletions(-) diff --git a/chord_metadata_service/restapi/fhir_ingest_utils.py b/chord_metadata_service/restapi/fhir_ingest_utils.py index f4b81d9e6..212ce26eb 100644 --- a/chord_metadata_service/restapi/fhir_ingest_utils.py +++ b/chord_metadata_service/restapi/fhir_ingest_utils.py @@ -1,5 +1,6 @@ import json import uuid +import jsonschema from fhirclient.models import observation as obs, patient as p, condition as cond, specimen as s @@ -11,37 +12,64 @@ from chord_metadata_service.chord.models import * from chord_metadata_service.phenopackets.models import * -FHIR_INGEST = { - "dataset_id": "", - "patients": "", - "observations": "", - "conditions": "", - "specimens": "" + +FHIR_INGEST_SCHEMA = { + "$id": "chord_metadata_service_fhir_ingest_schema", + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "FHIR Ingest schema", + "type": "object", + "properties": { + "dataset_id": {"type": "string"}, + "patients": {"type": "string", "description": "Path to a patients file location."}, + "observations": {"type": "string", "description": "Path to an observations file location."}, + "conditions": {"type": "string", "description": "Path to a conditions file location."}, + "specimens": {"type": "string", "description": "Path to a specimens file location."}, + "metadata": { + "type": "object", + "properties": { + "created_by": {"type": "string"} + }, + "required": ["created_by"] + } + }, + "required": [ + "dataset_id", + "patients", + "metadata" + ], } +def _parse_reference(ref): + """ FHIR test data has reference object in a format "ResourceType/uuid" """ + return ref.split('/')[-1] + + @api_view(["POST"]) @permission_classes([AllowAny]) def ingest_fhir(request): - if not request.data["dataset_id"]: - return Response(bad_request_error(f"Dataset ID is required."), status=404) - else: - if not Dataset.objects.filter(identifier=request.data["dataset_id"]).exists(): - return Response(bad_request_error(f"Dataset with ID {request.data['dataset_id']} does not exist"), status=400) - - if not request.data["patients"]: - return Response(bad_request_error(f"Patients data is required."), status=404) - - - # create new phenopacket for each individual, it uuid, subject, meta_data and dataset_id + try: + jsonschema.validate(request.data, FHIR_INGEST_SCHEMA) + except jsonschema.exceptions.ValidationError: + v = jsonschema.Draft7Validator(FHIR_INGEST_SCHEMA) + errors = [e for e in v.iter_errors(request.data)] + for i, error in enumerate(errors, 1): + return Response(bad_request_error( + f"{i} Validation error in {'.'.join(str(v) for v in error.path)}: {error.message}" + )) + if not Dataset.objects.filter(identifier=request.data["dataset_id"]).exists(): + return Response(bad_request_error(f"Dataset with ID {request.data['dataset_id']} does not exist"), + status=400) + + # create new phenopacket for each individual # fake metadata meta_data_obj = MetaData.objects.create( - created_by="unknown", - submitted_by="unknown", + created_by=request.data["metadata"]["created_by"], phenopacket_schema_version="1.0.0-RC3", external_references=[] ) + # patients-individuals with open(request.data["patients"], "r") as p_file: try: patients_data = json.load(p_file) @@ -60,56 +88,59 @@ def ingest_fhir(request): except json.decoder.JSONDecodeError as e: return Response(bad_request_error(f"Invalid JSON provided (message: {e})"), status=400) - with open(request.data["observations"], "r") as obs_file: - try: - observations_data = json.load(obs_file) - for item in observations_data["entry"]: - phenotypic_feature_data = observation_to_phenotypic_feature(item["resource"]) - if not item["resource"]["subject"]: - return Response(bad_request_error(f"Subject is required."), status=404) - # FHIR test data has reference object in a format "ResourceType/uuid" - subject = item["resource"]["subject"]["reference"].split('Patient/')[1] # Individual ID - phenotypic_feature, _ = PhenotypicFeature.objects.get_or_create( - phenopacket=Phenopacket.objects.get(subject=Individual.objects.get(id=subject)), - **phenotypic_feature_data - ) - except json.decoder.JSONDecodeError as e: - return Response(bad_request_error(f"Invalid JSON provided (message: {e})"), status=400) - - with open(request.data["conditions"]) as c_file: - try: - conditions_data = json.load(c_file) - for item in conditions_data["entry"]: - disease_data = condition_to_disease(item["resource"]) - disease = Disease.objects.create(**disease_data) - if not item["resource"]["subject"]: - return Response(bad_request_error(f"Subject is required."), status=404) - # FHIR test data has reference object in a format "ResourceType/uuid" - subject = item["resource"]["subject"]["reference"].split('Patient/')[1] # Individual ID - # a1.publications.add(p1) - phenopacket = Phenopacket.objects.get(subject=Individual.objects.get(id=subject)) - phenopacket.diseases.add(disease) - - except json.decoder.JSONDecodeError as e: - return Response(bad_request_error(f"Invalid JSON provided (message: {e})"), status=400) - - with open(request.data["specimens"], "r") as s_file: - try: - specimens_data = json.load(s_file) - for item in specimens_data["entry"]: - biosample_data = specimen_to_biosample(item["resource"]) - procedure, _ = Procedure.objects.get_or_create(**biosample_data["procedure"]) - individual_id = biosample_data["individual"].split('Patient/')[1] # Individual ID - Biosample.objects.create( - id=biosample_data["id"], - procedure=procedure, - individual=Individual.objects.get(id=individual_id), - sampled_tissue=biosample_data["sampled_tissue"] - ) - - except json.decoder.JSONDecodeError as e: - return Response(bad_request_error(f"Invalid JSON provided (message: {e})"), status=400) + # observations-phenotypicFeatures + if "observations" in request.data: + with open(request.data["observations"], "r") as obs_file: + try: + observations_data = json.load(obs_file) + for item in observations_data["entry"]: + phenotypic_feature_data = observation_to_phenotypic_feature(item["resource"]) + if not item["resource"]["subject"]: + return Response(bad_request_error(f"Subject is required."), status=404) + + subject = _parse_reference(item["resource"]["subject"]["reference"]) + phenotypic_feature, _ = PhenotypicFeature.objects.get_or_create( + phenopacket=Phenopacket.objects.get(subject=Individual.objects.get(id=subject)), + **phenotypic_feature_data + ) + except json.decoder.JSONDecodeError as e: + return Response(bad_request_error(f"Invalid JSON provided (message: {e})"), status=400) + + # conditions-diseases + if "conditions" in request.data: + with open(request.data["conditions"]) as c_file: + try: + conditions_data = json.load(c_file) + for item in conditions_data["entry"]: + disease_data = condition_to_disease(item["resource"]) + disease = Disease.objects.create(**disease_data) + if not item["resource"]["subject"]: + return Response(bad_request_error(f"Subject is required."), status=404) + subject = _parse_reference(item["resource"]["subject"]["reference"]) + phenopacket = Phenopacket.objects.get(subject=Individual.objects.get(id=subject)) + phenopacket.diseases.add(disease) + + except json.decoder.JSONDecodeError as e: + return Response(bad_request_error(f"Invalid JSON provided (message: {e})"), status=400) + + # specimens-biosamples + if "specimens" in request.data: + with open(request.data["specimens"], "r") as s_file: + try: + specimens_data = json.load(s_file) + for item in specimens_data["entry"]: + biosample_data = specimen_to_biosample(item["resource"]) + procedure, _ = Procedure.objects.get_or_create(**biosample_data["procedure"]) + individual_id = _parse_reference(biosample_data["individual"]) + Biosample.objects.create( + id=biosample_data["id"], + procedure=procedure, + individual=Individual.objects.get(id=individual_id), + sampled_tissue=biosample_data["sampled_tissue"] + ) + except json.decoder.JSONDecodeError as e: + return Response(bad_request_error(f"Invalid JSON provided (message: {e})"), status=400) return Response(status=204) @@ -150,6 +181,7 @@ def observation_to_phenotypic_feature(obj): codeable_concept = observation.code # CodeableConcept phenotypic_feature = { # id is an integer AutoField, store legacy id in description + # TODO change "description": observation.id, "pftype": { "id": codeable_concept.coding[0].code, From 76ac4474a06ddbd0bcd5a50f858558ae439b8c4c Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Mon, 25 May 2020 20:00:07 -0400 Subject: [PATCH 066/190] add fhir data examples --- chord_metadata_service/metadata/settings.py | 2 +- .../restapi/fhir_data_testing.py | 514 +++++++++++------- examples/conditions.json | 72 +++ examples/observations.json | 155 ++++++ examples/patients.json | 431 +++++++++++++++ 5 files changed, 980 insertions(+), 194 deletions(-) create mode 100644 examples/conditions.json create mode 100644 examples/observations.json create mode 100644 examples/patients.json diff --git a/chord_metadata_service/metadata/settings.py b/chord_metadata_service/metadata/settings.py index e490b99cd..8e0534349 100644 --- a/chord_metadata_service/metadata/settings.py +++ b/chord_metadata_service/metadata/settings.py @@ -146,7 +146,7 @@ DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', - 'NAME': os.environ.get("POSTGRES_DATABASE", 'metadata'), + 'NAME': os.environ.get("POSTGRES_DATABASE", 'metadatadevelop'), 'USER': os.environ.get("POSTGRES_USER", 'admin'), 'PASSWORD': os.environ.get("POSTGRES_PASSWORD", 'admin'), diff --git a/chord_metadata_service/restapi/fhir_data_testing.py b/chord_metadata_service/restapi/fhir_data_testing.py index 8da8eb279..6e518e8fd 100644 --- a/chord_metadata_service/restapi/fhir_data_testing.py +++ b/chord_metadata_service/restapi/fhir_data_testing.py @@ -1,211 +1,210 @@ FHIR_PATIENT = { - "address": [ - { - "city": "Carver", - "country": "US", - "extension": [ + "address": [ { - "extension": [ - { - "url": "latitude", - "valueDecimal": 41.875179 - }, - { - "url": "longitude", - "valueDecimal": -70.74671500000002 + "city": "Carver", + "country": "US", + "extension": [ + { + "extension": [ + { + "url": "latitude", + "valueDecimal": 41.875179 + }, + { + "url": "longitude", + "valueDecimal": -70.74671500000002 + } + ], + "url": "http://hl7.org/fhir/StructureDefinition/geolocation" + } + ], + "line": [ + "1087 Halvorson Light" + ], + "postalCode": "02330", + "state": "Massachusetts" + } + ], + "birthDate": "1991-02-10", + "communication": [ + { + "language": { + "coding": [ + { + "code": "pt", + "display": "Portuguese", + "system": "urn:ietf:bcp:47" + } + ], + "text": "Portuguese" } - ], - "url": "http://hl7.org/fhir/StructureDefinition/geolocation" } - ], - "line": [ - "1087 Halvorson Light" - ], - "postalCode": "02330", - "state": "Massachusetts" - } - ], - "birthDate": "1991-02-10", - "communication": [ - { - "language": { - "coding": [ - { - "code": "pt", - "display": "Portuguese", - "system": "urn:ietf:bcp:47" - } - ], - "text": "Portuguese" - } - } - ], - "extension": [ - { - "extension": [ - { - "url": "ombCategory", - "valueCoding": { - "code": "2106-3", - "display": "White", - "system": "urn:oid:2.16.840.1.113883.6.238" - } + ], + "extension": [ + { + "extension": [ + { + "url": "ombCategory", + "valueCoding": { + "code": "2106-3", + "display": "White", + "system": "urn:oid:2.16.840.1.113883.6.238" + } + }, + { + "url": "text", + "valueString": "White" + } + ], + "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race" + }, + { + "extension": [ + { + "url": "ombCategory", + "valueCoding": { + "code": "2186-5", + "display": "Not Hispanic or Latino", + "system": "urn:oid:2.16.840.1.113883.6.238" + } + }, + { + "url": "text", + "valueString": "Not Hispanic or Latino" + } + ], + "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/patient-mothersMaidenName", + "valueString": "Krysta658 Terry864" + }, + { + "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex", + "valueCode": "M" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/birthPlace", + "valueAddress": { + "city": "Lisbon", + "country": "PT", + "state": "Estremadura" + } }, { - "url": "text", - "valueString": "White" + "url": "http://synthetichealth.github.io/synthea/disability-adjusted-life-years", + "valueDecimal": 0 + }, + { + "url": "http://synthetichealth.github.io/synthea/quality-adjusted-life-years", + "valueDecimal": 27 } - ], - "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race" - }, - { - "extension": [ - { - "url": "ombCategory", - "valueCoding": { - "code": "2186-5", - "display": "Not Hispanic or Latino", - "system": "urn:oid:2.16.840.1.113883.6.238" - } + ], + "gender": "male", + "id": "6f7acde5-db81-4361-82cf-886893a3280c", + "identifier": [ + { + "system": "https://github.com/synthetichealth/synthea", + "value": "a238ebf2-392b-44be-9a17-da07a15220e2" + }, + { + "system": "http://hospital.smarthealthit.org", + "type": { + "coding": [ + { + "code": "MR", + "display": "Medical Record Number", + "system": "http://hl7.org/fhir/v2/0203" + } + ], + "text": "Medical Record Number" + }, + "value": "a238ebf2-392b-44be-9a17-da07a15220e2" + }, + { + "system": "http://hl7.org/fhir/sid/us-ssn", + "type": { + "coding": [ + { + "code": "SB", + "display": "Social Security Number", + "system": "http://hl7.org/fhir/identifier-type" + } + ], + "text": "Social Security Number" + }, + "value": "999-99-7515" }, { - "url": "text", - "valueString": "Not Hispanic or Latino" + "system": "urn:oid:2.16.840.1.113883.4.3.25", + "type": { + "coding": [ + { + "code": "DL", + "display": "Driver's License", + "system": "http://hl7.org/fhir/v2/0203" + } + ], + "text": "Driver's License" + }, + "value": "S99942098" + }, + { + "system": "http://standardhealthrecord.org/fhir/StructureDefinition/passportNumber", + "type": { + "coding": [ + { + "code": "PPN", + "display": "Passport Number", + "system": "http://hl7.org/fhir/v2/0203" + } + ], + "text": "Passport Number" + }, + "value": "X19416767X" } - ], - "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity" - }, - { - "url": "http://hl7.org/fhir/StructureDefinition/patient-mothersMaidenName", - "valueString": "Krysta658 Terry864" - }, - { - "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex", - "valueCode": "M" - }, - { - "url": "http://hl7.org/fhir/StructureDefinition/birthPlace", - "valueAddress": { - "city": "Lisbon", - "country": "PT", - "state": "Estremadura" - } - }, - { - "url": "http://synthetichealth.github.io/synthea/disability-adjusted-life-years", - "valueDecimal": 0 - }, - { - "url": "http://synthetichealth.github.io/synthea/quality-adjusted-life-years", - "valueDecimal": 27 - } - ], - "gender": "male", - "id": "6f7acde5-db81-4361-82cf-886893a3280c", - "identifier": [ - { - "system": "https://github.com/synthetichealth/synthea", - "value": "a238ebf2-392b-44be-9a17-da07a15220e2" - }, - { - "system": "http://hospital.smarthealthit.org", - "type": { - "coding": [ - { - "code": "MR", - "display": "Medical Record Number", - "system": "http://hl7.org/fhir/v2/0203" - } - ], - "text": "Medical Record Number" - }, - "value": "a238ebf2-392b-44be-9a17-da07a15220e2" - }, - { - "system": "http://hl7.org/fhir/sid/us-ssn", - "type": { + ], + "maritalStatus": { "coding": [ - { - "code": "SB", - "display": "Social Security Number", - "system": "http://hl7.org/fhir/identifier-type" - } + { + "code": "M", + "display": "M", + "system": "http://hl7.org/fhir/v3/MaritalStatus" + } ], - "text": "Social Security Number" - }, - "value": "999-99-7515" + "text": "M" }, - { - "system": "urn:oid:2.16.840.1.113883.4.3.25", - "type": { - "coding": [ - { - "code": "DL", - "display": "Driver's License", - "system": "http://hl7.org/fhir/v2/0203" - } - ], - "text": "Driver's License" - }, - "value": "S99942098" + "meta": { + "lastUpdated": "2019-04-09T12:25:36.451316+00:00", + "versionId": "MTU1NDgxMjczNjQ1MTMxNjAwMA" }, - { - "system": "http://standardhealthrecord.org/fhir/StructureDefinition/passportNumber", - "type": { - "coding": [ - { - "code": "PPN", - "display": "Passport Number", - "system": "http://hl7.org/fhir/v2/0203" - } - ], - "text": "Passport Number" - }, - "value": "X19416767X" - } - ], - "maritalStatus": { - "coding": [ - { - "code": "M", - "display": "M", - "system": "http://hl7.org/fhir/v3/MaritalStatus" - } + "multipleBirthBoolean": False, + "name": [ + { + "family": "Hettinger594", + "given": [ + "Gregg522" + ], + "prefix": [ + "Mr." + ], + "use": "official" + } ], - "text": "M" - }, - "meta": { - "lastUpdated": "2019-04-09T12:25:36.451316+00:00", - "versionId": "MTU1NDgxMjczNjQ1MTMxNjAwMA" - }, - "multipleBirthBoolean": False, - "name": [ - { - "family": "Hettinger594", - "given": [ - "Gregg522" - ], - "prefix": [ - "Mr." - ], - "use": "official" - } - ], - "resourceType": "Patient", - "telecom": [ - { - "system": "phone", - "use": "home", - "value": "555-282-3544" + "resourceType": "Patient", + "telecom": [ + { + "system": "phone", + "use": "home", + "value": "555-282-3544" + } + ], + "text": { + "div": "
Generated by Synthea.Version identifier: v2.2.0-56-g113d8a2d\n . Person seed: 8417064283020065324 Population seed: 5
", + "status": "generated" } - ], - "text": { - "div": "
Generated by Synthea.Version identifier: v2.2.0-56-g113d8a2d\n . Person seed: 8417064283020065324 Population seed: 5
", - "status": "generated" - } } - FHIR_OBSERVATION = { "category": [ { @@ -251,7 +250,6 @@ } } - FHIR_OBSERVATION_BUNDLE = { "entry": [ { @@ -408,7 +406,6 @@ "resourceType": "Bundle" } - FHIR_CONDITION = { "abatementDateTime": "2018-09-21T11:12:53-04:00", "assertedDate": "2018-08-22T11:12:53-04:00", @@ -439,7 +436,6 @@ "verificationStatus": "confirmed" } - FHIR_CONDITION_BUNDLE = { "entry": [ { @@ -627,3 +623,135 @@ "total": 5, "type": "searchset" } + +FHIR_DIAGNOSTIC_REPORT = { + "code": { + "coding": [ + { + "code": "58410-2", + "display": "Complete blood count (hemogram) panel - Blood by Automated count", + "system": "http://loinc.org" + } + ], + "text": "Complete blood count (hemogram) panel - Blood by Automated count" + }, + "context": { + "reference": "Encounter/2a0e0f6c-493f-4c5b-bf89-5f98aee24f21" + }, + "effectiveDateTime": "2014-03-02T10:12:53-05:00", + "id": "ba93319c-3e5f-4074-ac2a-ff12e369a612", + "issued": "2014-03-02T10:12:53.714-05:00", + "meta": { + "lastUpdated": "2019-04-09T12:25:36.527685+00:00", + "versionId": "MTU1NDgxMjczNjUyNzY4NTAwMA" + }, + "resourceType": "DiagnosticReport", + "result": [ + { + "display": "Leukocytes [#/volume] in Blood by Automated count", + "reference": "Observation/324cc373-8b87-4354-bf62-afafe93a760c" + }, + { + "display": "Erythrocytes [#/volume] in Blood by Automated count", + "reference": "Observation/c90d0267-b660-48b8-813c-2ef0fa46e0f7" + }, + { + "display": "Hemoglobin [Mass/volume] in Blood", + "reference": "Observation/97684895-3a6e-4d46-9edd-31984bc7c3a6" + }, + { + "display": "Hematocrit [Volume Fraction] of Blood by Automated count", + "reference": "Observation/f0fcb049-32da-4da0-8687-540e494a3a26" + }, + { + "display": "MCV [Entitic volume] by Automated count", + "reference": "Observation/5814b42c-be27-4ceb-ba63-66e235e22b8f" + }, + { + "display": "MCH [Entitic mass] by Automated count", + "reference": "Observation/1c8d2ee3-2a7e-47f9-be16-abe4e9fa306b" + }, + { + "display": "MCHC [Mass/volume] by Automated count", + "reference": "Observation/6d24fd3b-f895-4f4c-a851-9b53ebd3cf49" + }, + { + "display": "Erythrocyte distribution width [Entitic volume] by Automated count", + "reference": "Observation/a4e4a6d9-dfb4-4a7c-bd6b-1599e3c16ec9" + }, + { + "display": "Platelets [#/volume] in Blood by Automated count", + "reference": "Observation/b0598af4-8ffe-43ba-84e5-b7fb49d3dcd7" + }, + { + "display": "Platelet distribution width [Entitic volume] in Blood by Automated count", + "reference": "Observation/7a9e943a-6638-47cb-931f-18c1b9d7ba3f" + }, + { + "display": "Platelet mean volume [Entitic volume] in Blood by Automated count", + "reference": "Observation/0adc582c-c620-4508-89e8-79ac134a6aa0" + } + ], + "status": "final", + "subject": { + "reference": "Patient/6f7acde5-db81-4361-82cf-886893a3280c" + } +} + +# the example is taken from hapi fhir server +# the example is modified, subject patient added +FHIR_SPECIMEN = { + "resourceType": "Specimen", + "id": "1168252", + "meta": { + "versionId": "1", + "lastUpdated": "2020-05-19T01:47:46.112+00:00" + }, + "identifier": [{ + "system": "urn:ietf:rfc:3986", + "value": "urn:uuid:bc269666-9972-11ea-a751-acde48001122" + }], + "accessionIdentifier": { + "system": "http://mghpathology.org/identifiers/specimens", + "value": "urn:id:TEST20-0002_A" + }, + "type": { + "coding": [{ + "system": "http://snomed.info/sct", + "code": "445405002", + "display": "Specimen obtained by surgical procedure" + }] + }, + "collection": { + "method": { + "coding": [{ + "system": "snowmed", + "code": "69466000", + "display": "Unknown procedure" + }] + }, + "bodySite": { + "coding": [{ + "system": "snowmed", + "code": "87100004", + "display": "Topography unknown" + }] + } + }, + "container": [{ + "identifier": [{ + "system": "urn:ietf:rfc:3986", + "value": "urn:uuid:bc269918-9972-11ea-a751-acde48001122" + }], + "type": { + "coding": [{ + "system": "http://snomed.info/sct", + "code": "434711009", + "display": "Specimen container" + }] + } + }], + "subject": { + "reference": "Patient/6f7acde5-db81-4361-82cf-886893a3280c" + } +} diff --git a/examples/conditions.json b/examples/conditions.json new file mode 100644 index 000000000..f7374e7b9 --- /dev/null +++ b/examples/conditions.json @@ -0,0 +1,72 @@ +{ + "entry": [ + { + "fullUrl": "https://syntheticmass.mitre.org/v1/fhir/Condition/bab430ff-5b09-4e4a-8871-6e4fcb84fa17", + "resource": { + "assertedDate": "2009-04-05T11:12:53-04:00", + "clinicalStatus": "active", + "code": { + "coding": [ + { + "code": "38341003", + "display": "Hypertension", + "system": "http://snomed.info/sct" + } + ], + "text": "Hypertension" + }, + "context": { + "reference": "Encounter/630a642d-0402-454c-9426-c399cf9b2aab" + }, + "id": "bab430ff-5b09-4e4a-8871-6e4fcb84fa17", + "meta": { + "lastUpdated": "2019-04-09T12:25:36.486194+00:00", + "versionId": "MTU1NDgxMjczNjQ4NjE5NDAwMA" + }, + "onsetDateTime": "2009-04-05T11:12:53-04:00", + "resourceType": "Condition", + "subject": { + "reference": "Patient/6f7acde5-db81-4361-82cf-886893a3280c" + }, + "verificationStatus": "confirmed" + }, + "search": { + "mode": "match" + } + }, + { + "fullUrl": "https://syntheticmass.mitre.org/v1/fhir/Condition/0e2a23c7-90ef-4e0a-b9da-c7869104f8f6", + "resource": { + "assertedDate": "1961-06-27T21:21:32-04:00", + "clinicalStatus": "active", + "code": { + "coding": [ + { + "code": "38341003", + "display": "Hypertension", + "system": "http://snomed.info/sct" + } + ], + "text": "Hypertension" + }, + "context": { + "reference": "Encounter/bdba928a-6a8a-4998-bdb9-4190029cfd56" + }, + "id": "0e2a23c7-90ef-4e0a-b9da-c7869104f8f6", + "meta": { + "lastUpdated": "2019-04-09T12:25:35.399801+00:00", + "versionId": "MTU1NDgxMjczNTM5OTgwMTAwMA" + }, + "onsetDateTime": "1961-06-27T21:21:32-04:00", + "resourceType": "Condition", + "subject": { + "reference": "Patient/728b270d-d7de-4143-82fe-d3ccd92cebe4" + }, + "verificationStatus": "confirmed" + }, + "search": { + "mode": "match" + } + } + ] +} \ No newline at end of file diff --git a/examples/observations.json b/examples/observations.json new file mode 100644 index 000000000..45da2ea84 --- /dev/null +++ b/examples/observations.json @@ -0,0 +1,155 @@ +{ + "entry": [ + { + "fullUrl": "https://syntheticmass.mitre.org/v1/fhir/Observation/97684895-3a6e-4d46-9edd-31984bc7c3a6", + "resource": { + "category": [ + { + "coding": [ + { + "code": "laboratory", + "display": "laboratory", + "system": "http://hl7.org/fhir/observation-category" + } + ] + } + ], + "code": { + "coding": [ + { + "code": "718-7", + "display": "Hemoglobin [Mass/volume] in Blood", + "system": "http://loinc.org" + } + ], + "text": "Hemoglobin [Mass/volume] in Blood" + }, + "context": { + "reference": "Encounter/2a0e0f6c-493f-4c5b-bf89-5f98aee24f21" + }, + "effectiveDateTime": "2014-03-02T10:12:53-05:00", + "id": "97684895-3a6e-4d46-9edd-31984bc7c3a6", + "issued": "2014-03-02T10:12:53.714-05:00", + "meta": { + "lastUpdated": "2019-04-09T12:25:36.531998+00:00", + "versionId": "MTU1NDgxMjczNjUzMTk5ODAwMA" + }, + "resourceType": "Observation", + "status": "final", + "subject": { + "reference": "Patient/6f7acde5-db81-4361-82cf-886893a3280c" + }, + "valueQuantity": { + "code": "g/dL", + "system": "http://unitsofmeasure.org", + "unit": "g/dL", + "value": 17.14301557752162 + } + }, + "search": { + "mode": "match" + } + }, + { + "fullUrl": "https://syntheticmass.mitre.org/v1/fhir/Observation/1c8d2ee3-2a7e-47f9-be16-abe4e9fa306b", + "resource": { + "category": [ + { + "coding": [ + { + "code": "laboratory", + "display": "laboratory", + "system": "http://hl7.org/fhir/observation-category" + } + ] + } + ], + "code": { + "coding": [ + { + "code": "785-6", + "display": "MCH [Entitic mass] by Automated count", + "system": "http://loinc.org" + } + ], + "text": "MCH [Entitic mass] by Automated count" + }, + "context": { + "reference": "Encounter/2a0e0f6c-493f-4c5b-bf89-5f98aee24f21" + }, + "effectiveDateTime": "2014-03-02T10:12:53-05:00", + "id": "1c8d2ee3-2a7e-47f9-be16-abe4e9fa306b", + "issued": "2014-03-02T10:12:53.714-05:00", + "meta": { + "lastUpdated": "2019-04-09T12:25:36.531997+00:00", + "versionId": "MTU1NDgxMjczNjUzMTk5NzAwMA" + }, + "resourceType": "Observation", + "status": "final", + "subject": { + "reference": "Patient/6f7acde5-db81-4361-82cf-886893a3280c" + }, + "valueQuantity": { + "code": "pg", + "system": "http://unitsofmeasure.org", + "unit": "pg", + "value": 27.035236044041877 + } + }, + "search": { + "mode": "match" + } + }, + { + "fullUrl": "https://syntheticmass.mitre.org/v1/fhir/Observation/b0598af4-8ffe-43ba-84e5-b7fb49d3dcd7", + "resource": { + "category": [ + { + "coding": [ + { + "code": "laboratory", + "display": "laboratory", + "system": "http://hl7.org/fhir/observation-category" + } + ] + } + ], + "code": { + "coding": [ + { + "code": "777-3", + "display": "Platelets [#/volume] in Blood by Automated count", + "system": "http://loinc.org" + } + ], + "text": "Platelets [#/volume] in Blood by Automated count" + }, + "context": { + "reference": "Encounter/2a0e0f6c-493f-4c5b-bf89-5f98aee24f21" + }, + "effectiveDateTime": "2014-03-02T10:12:53-05:00", + "id": "b0598af4-8ffe-43ba-84e5-b7fb49d3dcd7", + "issued": "2014-03-02T10:12:53.714-05:00", + "meta": { + "lastUpdated": "2019-04-09T12:25:36.530961+00:00", + "versionId": "MTU1NDgxMjczNjUzMDk2MTAwMA" + }, + "resourceType": "Observation", + "status": "final", + "subject": { + "reference": "Patient/728b270d-d7de-4143-82fe-d3ccd92cebe4" + }, + "valueQuantity": { + "code": "10*3/uL", + "system": "http://unitsofmeasure.org", + "unit": "10*3/uL", + "value": 306.49607523265786 + } + }, + "search": { + "mode": "match" + } + } + ], + "resourceType": "Bundle" +} \ No newline at end of file diff --git a/examples/patients.json b/examples/patients.json new file mode 100644 index 000000000..c9093fe21 --- /dev/null +++ b/examples/patients.json @@ -0,0 +1,431 @@ +{ + "resourceType": "Bundle", + "total": 154259, + "type": "searchset", + "entry": [ + { + "resource": { + "meta": { + "lastUpdated": "2019-04-09T12:25:36.451316+00:00", + "versionId": "MTU1NDgxMjczNjQ1MTMxNjAwMA" + }, + "identifier": [ + { + "system": "https://github.com/synthetichealth/synthea", + "value": "a238ebf2-392b-44be-9a17-da07a15220e2" + }, + { + "system": "http://hospital.smarthealthit.org", + "value": "a238ebf2-392b-44be-9a17-da07a15220e2", + "type": { + "coding": [ + { + "system": "http://hl7.org/fhir/v2/0203", + "code": "MR", + "display": "Medical Record Number" + } + ], + "text": "Medical Record Number" + } + }, + { + "system": "http://hl7.org/fhir/sid/us-ssn", + "value": "999-99-7515", + "type": { + "coding": [ + { + "system": "http://hl7.org/fhir/identifier-type", + "code": "SB", + "display": "Social Security Number" + } + ], + "text": "Social Security Number" + } + }, + { + "system": "urn:oid:2.16.840.1.113883.4.3.25", + "value": "S99942098", + "type": { + "coding": [ + { + "system": "http://hl7.org/fhir/v2/0203", + "code": "DL", + "display": "Driver's License" + } + ], + "text": "Driver's License" + } + }, + { + "system": "http://standardhealthrecord.org/fhir/StructureDefinition/passportNumber", + "value": "X19416767X", + "type": { + "coding": [ + { + "system": "http://hl7.org/fhir/v2/0203", + "code": "PPN", + "display": "Passport Number" + } + ], + "text": "Passport Number" + } + } + ], + "name": [ + { + "given": [ + "Gregg522" + ], + "prefix": [ + "Mr." + ], + "family": "Hettinger594", + "use": "official" + } + ], + "telecom": [ + { + "system": "phone", + "value": "555-282-3544", + "use": "home" + } + ], + "extension": [ + { + "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race", + "extension": [ + { + "url": "ombCategory", + "valueCoding": { + "system": "urn:oid:2.16.840.1.113883.6.238", + "code": "2106-3", + "display": "White" + } + }, + { + "valueString": "White", + "url": "text" + } + ] + }, + { + "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity", + "extension": [ + { + "url": "ombCategory", + "valueCoding": { + "system": "urn:oid:2.16.840.1.113883.6.238", + "code": "2186-5", + "display": "Not Hispanic or Latino" + } + }, + { + "valueString": "Not Hispanic or Latino", + "url": "text" + } + ] + }, + { + "valueString": "Krysta658 Terry864", + "url": "http://hl7.org/fhir/StructureDefinition/patient-mothersMaidenName" + }, + { + "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex", + "valueCode": "M" + }, + { + "valueAddress": { + "state": "Estremadura", + "country": "PT", + "city": "Lisbon" + }, + "url": "http://hl7.org/fhir/StructureDefinition/birthPlace" + }, + { + "url": "http://synthetichealth.github.io/synthea/disability-adjusted-life-years", + "valueDecimal": 0 + }, + { + "url": "http://synthetichealth.github.io/synthea/quality-adjusted-life-years", + "valueDecimal": 27 + } + ], + "address": [ + { + "state": "Massachusetts", + "postalCode": "02330", + "city": "Carver", + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/geolocation", + "extension": [ + { + "url": "latitude", + "valueDecimal": 41.875179 + }, + { + "url": "longitude", + "valueDecimal": -70.74671500000002 + } + ] + } + ], + "line": [ + "1087 Halvorson Light" + ], + "country": "US" + } + ], + "resourceType": "Patient", + "communication": [ + { + "language": { + "coding": [ + { + "system": "urn:ietf:bcp:47", + "code": "pt", + "display": "Portuguese" + } + ], + "text": "Portuguese" + } + } + ], + "text": { + "status": "generated", + "div": "
Generated by Synthea.Version identifier: v2.2.0-56-g113d8a2d\n . Person seed: 8417064283020065324 Population seed: 5
" + }, + "gender": "male", + "multipleBirthBoolean": false, + "birthDate": "1991-02-10", + "maritalStatus": { + "coding": [ + { + "system": "http://hl7.org/fhir/v3/MaritalStatus", + "code": "M", + "display": "M" + } + ], + "text": "M" + }, + "id": "6f7acde5-db81-4361-82cf-886893a3280c" + }, + "fullUrl": "https://syntheticmass.mitre.org/v1/fhir/Patient/6f7acde5-db81-4361-82cf-886893a3280c", + "search": { + "mode": "match" + } + }, + { + "resource": { + "meta": { + "lastUpdated": "2019-04-09T12:25:35.392600+00:00", + "versionId": "MTU1NDgxMjczNTM5MjYwMDAwMA" + }, + "identifier": [ + { + "system": "https://github.com/synthetichealth/synthea", + "value": "b02886ea-3925-41d4-b606-377ae05eea78" + }, + { + "system": "http://hospital.smarthealthit.org", + "value": "b02886ea-3925-41d4-b606-377ae05eea78", + "type": { + "coding": [ + { + "system": "http://hl7.org/fhir/v2/0203", + "code": "MR", + "display": "Medical Record Number" + } + ], + "text": "Medical Record Number" + } + }, + { + "system": "http://hl7.org/fhir/sid/us-ssn", + "value": "999-14-7088", + "type": { + "coding": [ + { + "system": "http://hl7.org/fhir/identifier-type", + "code": "SB", + "display": "Social Security Number" + } + ], + "text": "Social Security Number" + } + }, + { + "system": "urn:oid:2.16.840.1.113883.4.3.25", + "value": "S99933145", + "type": { + "coding": [ + { + "system": "http://hl7.org/fhir/v2/0203", + "code": "DL", + "display": "Driver's License" + } + ], + "text": "Driver's License" + } + }, + { + "system": "http://standardhealthrecord.org/fhir/StructureDefinition/passportNumber", + "value": "X51147095X", + "type": { + "coding": [ + { + "system": "http://hl7.org/fhir/v2/0203", + "code": "PPN", + "display": "Passport Number" + } + ], + "text": "Passport Number" + } + } + ], + "name": [ + { + "given": [ + "Jonathan639" + ], + "prefix": [ + "Mr." + ], + "family": "Prosacco716", + "use": "official" + } + ], + "telecom": [ + { + "system": "phone", + "value": "555-719-3748", + "use": "home" + } + ], + "extension": [ + { + "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race", + "extension": [ + { + "url": "ombCategory", + "valueCoding": { + "system": "urn:oid:2.16.840.1.113883.6.238", + "code": "2106-3", + "display": "White" + } + }, + { + "valueString": "White", + "url": "text" + } + ] + }, + { + "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity", + "extension": [ + { + "url": "ombCategory", + "valueCoding": { + "system": "urn:oid:2.16.840.1.113883.6.238", + "code": "2186-5", + "display": "Not Hispanic or Latino" + } + }, + { + "valueString": "Not Hispanic or Latino", + "url": "text" + } + ] + }, + { + "valueString": "Corinna386 Kulas532", + "url": "http://hl7.org/fhir/StructureDefinition/patient-mothersMaidenName" + }, + { + "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex", + "valueCode": "M" + }, + { + "valueAddress": { + "state": "Massachusetts", + "country": "US", + "city": "Sandwich" + }, + "url": "http://hl7.org/fhir/StructureDefinition/birthPlace" + }, + { + "url": "http://synthetichealth.github.io/synthea/disability-adjusted-life-years", + "valueDecimal": 2.1052411837922023 + }, + { + "url": "http://synthetichealth.github.io/synthea/quality-adjusted-life-years", + "valueDecimal": 72.8947588162078 + } + ], + "address": [ + { + "state": "Massachusetts", + "postalCode": "02052", + "city": "Medfield", + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/geolocation", + "extension": [ + { + "url": "latitude", + "valueDecimal": 42.187011 + }, + { + "url": "longitude", + "valueDecimal": -71.30040799999998 + } + ] + } + ], + "line": [ + "1038 Ratke Throughway Apt 10" + ], + "country": "US" + } + ], + "resourceType": "Patient", + "communication": [ + { + "language": { + "coding": [ + { + "system": "urn:ietf:bcp:47", + "code": "en-US", + "display": "English" + } + ], + "text": "English" + } + } + ], + "text": { + "status": "generated", + "div": "
Generated by Synthea.Version identifier: v2.2.0-56-g113d8a2d\n . Person seed: -5708762760784740351 Population seed: 5
" + }, + "gender": "male", + "multipleBirthBoolean": false, + "birthDate": "1943-06-08", + "maritalStatus": { + "coding": [ + { + "system": "http://hl7.org/fhir/v3/MaritalStatus", + "code": "M", + "display": "M" + } + ], + "text": "M" + }, + "id": "728b270d-d7de-4143-82fe-d3ccd92cebe4" + }, + "fullUrl": "https://syntheticmass.mitre.org/v1/fhir/Patient/728b270d-d7de-4143-82fe-d3ccd92cebe4", + "search": { + "mode": "match" + } + } + ] +} \ No newline at end of file From 8f3e2ed4768bd374985677f6fb51e40bd6590ead Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Mon, 25 May 2020 20:01:51 -0400 Subject: [PATCH 067/190] fix my local db in settings --- chord_metadata_service/metadata/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chord_metadata_service/metadata/settings.py b/chord_metadata_service/metadata/settings.py index 8e0534349..e490b99cd 100644 --- a/chord_metadata_service/metadata/settings.py +++ b/chord_metadata_service/metadata/settings.py @@ -146,7 +146,7 @@ DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', - 'NAME': os.environ.get("POSTGRES_DATABASE", 'metadatadevelop'), + 'NAME': os.environ.get("POSTGRES_DATABASE", 'metadata'), 'USER': os.environ.get("POSTGRES_USER", 'admin'), 'PASSWORD': os.environ.get("POSTGRES_PASSWORD", 'admin'), From 24af3a6e14534f674c8e2c50faca654e909cacb9 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Mon, 25 May 2020 20:03:05 -0400 Subject: [PATCH 068/190] remove test file --- .../restapi/fhir_data_testing.py | 757 ------------------ 1 file changed, 757 deletions(-) delete mode 100644 chord_metadata_service/restapi/fhir_data_testing.py diff --git a/chord_metadata_service/restapi/fhir_data_testing.py b/chord_metadata_service/restapi/fhir_data_testing.py deleted file mode 100644 index 6e518e8fd..000000000 --- a/chord_metadata_service/restapi/fhir_data_testing.py +++ /dev/null @@ -1,757 +0,0 @@ -FHIR_PATIENT = { - "address": [ - { - "city": "Carver", - "country": "US", - "extension": [ - { - "extension": [ - { - "url": "latitude", - "valueDecimal": 41.875179 - }, - { - "url": "longitude", - "valueDecimal": -70.74671500000002 - } - ], - "url": "http://hl7.org/fhir/StructureDefinition/geolocation" - } - ], - "line": [ - "1087 Halvorson Light" - ], - "postalCode": "02330", - "state": "Massachusetts" - } - ], - "birthDate": "1991-02-10", - "communication": [ - { - "language": { - "coding": [ - { - "code": "pt", - "display": "Portuguese", - "system": "urn:ietf:bcp:47" - } - ], - "text": "Portuguese" - } - } - ], - "extension": [ - { - "extension": [ - { - "url": "ombCategory", - "valueCoding": { - "code": "2106-3", - "display": "White", - "system": "urn:oid:2.16.840.1.113883.6.238" - } - }, - { - "url": "text", - "valueString": "White" - } - ], - "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race" - }, - { - "extension": [ - { - "url": "ombCategory", - "valueCoding": { - "code": "2186-5", - "display": "Not Hispanic or Latino", - "system": "urn:oid:2.16.840.1.113883.6.238" - } - }, - { - "url": "text", - "valueString": "Not Hispanic or Latino" - } - ], - "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity" - }, - { - "url": "http://hl7.org/fhir/StructureDefinition/patient-mothersMaidenName", - "valueString": "Krysta658 Terry864" - }, - { - "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex", - "valueCode": "M" - }, - { - "url": "http://hl7.org/fhir/StructureDefinition/birthPlace", - "valueAddress": { - "city": "Lisbon", - "country": "PT", - "state": "Estremadura" - } - }, - { - "url": "http://synthetichealth.github.io/synthea/disability-adjusted-life-years", - "valueDecimal": 0 - }, - { - "url": "http://synthetichealth.github.io/synthea/quality-adjusted-life-years", - "valueDecimal": 27 - } - ], - "gender": "male", - "id": "6f7acde5-db81-4361-82cf-886893a3280c", - "identifier": [ - { - "system": "https://github.com/synthetichealth/synthea", - "value": "a238ebf2-392b-44be-9a17-da07a15220e2" - }, - { - "system": "http://hospital.smarthealthit.org", - "type": { - "coding": [ - { - "code": "MR", - "display": "Medical Record Number", - "system": "http://hl7.org/fhir/v2/0203" - } - ], - "text": "Medical Record Number" - }, - "value": "a238ebf2-392b-44be-9a17-da07a15220e2" - }, - { - "system": "http://hl7.org/fhir/sid/us-ssn", - "type": { - "coding": [ - { - "code": "SB", - "display": "Social Security Number", - "system": "http://hl7.org/fhir/identifier-type" - } - ], - "text": "Social Security Number" - }, - "value": "999-99-7515" - }, - { - "system": "urn:oid:2.16.840.1.113883.4.3.25", - "type": { - "coding": [ - { - "code": "DL", - "display": "Driver's License", - "system": "http://hl7.org/fhir/v2/0203" - } - ], - "text": "Driver's License" - }, - "value": "S99942098" - }, - { - "system": "http://standardhealthrecord.org/fhir/StructureDefinition/passportNumber", - "type": { - "coding": [ - { - "code": "PPN", - "display": "Passport Number", - "system": "http://hl7.org/fhir/v2/0203" - } - ], - "text": "Passport Number" - }, - "value": "X19416767X" - } - ], - "maritalStatus": { - "coding": [ - { - "code": "M", - "display": "M", - "system": "http://hl7.org/fhir/v3/MaritalStatus" - } - ], - "text": "M" - }, - "meta": { - "lastUpdated": "2019-04-09T12:25:36.451316+00:00", - "versionId": "MTU1NDgxMjczNjQ1MTMxNjAwMA" - }, - "multipleBirthBoolean": False, - "name": [ - { - "family": "Hettinger594", - "given": [ - "Gregg522" - ], - "prefix": [ - "Mr." - ], - "use": "official" - } - ], - "resourceType": "Patient", - "telecom": [ - { - "system": "phone", - "use": "home", - "value": "555-282-3544" - } - ], - "text": { - "div": "
Generated by Synthea.Version identifier: v2.2.0-56-g113d8a2d\n . Person seed: 8417064283020065324 Population seed: 5
", - "status": "generated" - } -} - -FHIR_OBSERVATION = { - "category": [ - { - "coding": [ - { - "code": "laboratory", - "display": "laboratory", - "system": "http://hl7.org/fhir/observation-category" - } - ] - } - ], - "code": { - "coding": [ - { - "code": "785-6", - "display": "MCH [Entitic mass] by Automated count", - "system": "http://loinc.org" - } - ], - "text": "MCH [Entitic mass] by Automated count" - }, - "context": { - "reference": "Encounter/2a0e0f6c-493f-4c5b-bf89-5f98aee24f21" - }, - "effectiveDateTime": "2014-03-02T10:12:53-05:00", - "id": "1c8d2ee3-2a7e-47f9-be16-abe4e9fa306b", - "issued": "2014-03-02T10:12:53.714-05:00", - "meta": { - "lastUpdated": "2019-04-09T12:25:36.531997+00:00", - "versionId": "MTU1NDgxMjczNjUzMTk5NzAwMA" - }, - "resourceType": "Observation", - "status": "final", - "subject": { - "reference": "Patient/6f7acde5-db81-4361-82cf-886893a3280c" - }, - "valueQuantity": { - "code": "pg", - "system": "http://unitsofmeasure.org", - "unit": "pg", - "value": 27.035236044041877 - } -} - -FHIR_OBSERVATION_BUNDLE = { - "entry": [ - { - "fullUrl": "https://syntheticmass.mitre.org/v1/fhir/Observation/97684895-3a6e-4d46-9edd-31984bc7c3a6", - "resource": { - "category": [ - { - "coding": [ - { - "code": "laboratory", - "display": "laboratory", - "system": "http://hl7.org/fhir/observation-category" - } - ] - } - ], - "code": { - "coding": [ - { - "code": "718-7", - "display": "Hemoglobin [Mass/volume] in Blood", - "system": "http://loinc.org" - } - ], - "text": "Hemoglobin [Mass/volume] in Blood" - }, - "context": { - "reference": "Encounter/2a0e0f6c-493f-4c5b-bf89-5f98aee24f21" - }, - "effectiveDateTime": "2014-03-02T10:12:53-05:00", - "id": "97684895-3a6e-4d46-9edd-31984bc7c3a6", - "issued": "2014-03-02T10:12:53.714-05:00", - "meta": { - "lastUpdated": "2019-04-09T12:25:36.531998+00:00", - "versionId": "MTU1NDgxMjczNjUzMTk5ODAwMA" - }, - "resourceType": "Observation", - "status": "final", - "subject": { - "reference": "Patient/6f7acde5-db81-4361-82cf-886893a3280c" - }, - "valueQuantity": { - "code": "g/dL", - "system": "http://unitsofmeasure.org", - "unit": "g/dL", - "value": 17.14301557752162 - } - }, - "search": { - "mode": "match" - } - }, - { - "fullUrl": "https://syntheticmass.mitre.org/v1/fhir/Observation/1c8d2ee3-2a7e-47f9-be16-abe4e9fa306b", - "resource": { - "category": [ - { - "coding": [ - { - "code": "laboratory", - "display": "laboratory", - "system": "http://hl7.org/fhir/observation-category" - } - ] - } - ], - "code": { - "coding": [ - { - "code": "785-6", - "display": "MCH [Entitic mass] by Automated count", - "system": "http://loinc.org" - } - ], - "text": "MCH [Entitic mass] by Automated count" - }, - "context": { - "reference": "Encounter/2a0e0f6c-493f-4c5b-bf89-5f98aee24f21" - }, - "effectiveDateTime": "2014-03-02T10:12:53-05:00", - "id": "1c8d2ee3-2a7e-47f9-be16-abe4e9fa306b", - "issued": "2014-03-02T10:12:53.714-05:00", - "meta": { - "lastUpdated": "2019-04-09T12:25:36.531997+00:00", - "versionId": "MTU1NDgxMjczNjUzMTk5NzAwMA" - }, - "resourceType": "Observation", - "status": "final", - "subject": { - "reference": "Patient/6f7acde5-db81-4361-82cf-886893a3280c" - }, - "valueQuantity": { - "code": "pg", - "system": "http://unitsofmeasure.org", - "unit": "pg", - "value": 27.035236044041877 - } - }, - "search": { - "mode": "match" - } - }, - { - "fullUrl": "https://syntheticmass.mitre.org/v1/fhir/Observation/b0598af4-8ffe-43ba-84e5-b7fb49d3dcd7", - "resource": { - "category": [ - { - "coding": [ - { - "code": "laboratory", - "display": "laboratory", - "system": "http://hl7.org/fhir/observation-category" - } - ] - } - ], - "code": { - "coding": [ - { - "code": "777-3", - "display": "Platelets [#/volume] in Blood by Automated count", - "system": "http://loinc.org" - } - ], - "text": "Platelets [#/volume] in Blood by Automated count" - }, - "context": { - "reference": "Encounter/2a0e0f6c-493f-4c5b-bf89-5f98aee24f21" - }, - "effectiveDateTime": "2014-03-02T10:12:53-05:00", - "id": "b0598af4-8ffe-43ba-84e5-b7fb49d3dcd7", - "issued": "2014-03-02T10:12:53.714-05:00", - "meta": { - "lastUpdated": "2019-04-09T12:25:36.530961+00:00", - "versionId": "MTU1NDgxMjczNjUzMDk2MTAwMA" - }, - "resourceType": "Observation", - "status": "final", - "subject": { - "reference": "Patient/6f7acde5-db81-4361-82cf-886893a3280c" - }, - "valueQuantity": { - "code": "10*3/uL", - "system": "http://unitsofmeasure.org", - "unit": "10*3/uL", - "value": 306.49607523265786 - } - }, - "search": { - "mode": "match" - } - } - ], - "resourceType": "Bundle" -} - -FHIR_CONDITION = { - "abatementDateTime": "2018-09-21T11:12:53-04:00", - "assertedDate": "2018-08-22T11:12:53-04:00", - "clinicalStatus": "resolved", - "code": { - "coding": [ - { - "code": "62106007", - "display": "Concussion with no loss of consciousness", - "system": "http://snomed.info/sct" - } - ], - "text": "Concussion with no loss of consciousness" - }, - "context": { - "reference": "Encounter/1d91f8e0-74f1-4071-a681-9d4fa0f9b93a" - }, - "id": "4f2c2598-7e60-4752-b603-b330ca166829", - "meta": { - "lastUpdated": "2019-04-09T12:25:36.531999+00:00", - "versionId": "MTU1NDgxMjczNjUzMTk5OTAwMA" - }, - "onsetDateTime": "2018-08-22T11:12:53-04:00", - "resourceType": "Condition", - "subject": { - "reference": "Patient/6f7acde5-db81-4361-82cf-886893a3280c" - }, - "verificationStatus": "confirmed" -} - -FHIR_CONDITION_BUNDLE = { - "entry": [ - { - "fullUrl": "https://syntheticmass.mitre.org/v1/fhir/Condition/4f2c2598-7e60-4752-b603-b330ca166829", - "resource": { - "abatementDateTime": "2018-09-21T11:12:53-04:00", - "assertedDate": "2018-08-22T11:12:53-04:00", - "clinicalStatus": "resolved", - "code": { - "coding": [ - { - "code": "62106007", - "display": "Concussion with no loss of consciousness", - "system": "http://snomed.info/sct" - } - ], - "text": "Concussion with no loss of consciousness" - }, - "context": { - "reference": "Encounter/1d91f8e0-74f1-4071-a681-9d4fa0f9b93a" - }, - "id": "4f2c2598-7e60-4752-b603-b330ca166829", - "meta": { - "lastUpdated": "2019-04-09T12:25:36.531999+00:00", - "versionId": "MTU1NDgxMjczNjUzMTk5OTAwMA" - }, - "onsetDateTime": "2018-08-22T11:12:53-04:00", - "resourceType": "Condition", - "subject": { - "reference": "Patient/6f7acde5-db81-4361-82cf-886893a3280c" - }, - "verificationStatus": "confirmed" - }, - "search": { - "mode": "match" - } - }, - { - "fullUrl": "https://syntheticmass.mitre.org/v1/fhir/Condition/cc454676-ccdc-4792-a5c9-cfbedce2ab33", - "resource": { - "abatementDateTime": "2011-03-19T11:12:53-04:00", - "assertedDate": "2011-02-26T10:12:53-05:00", - "clinicalStatus": "resolved", - "code": { - "coding": [ - { - "code": "444814009", - "display": "Viral sinusitis (disorder)", - "system": "http://snomed.info/sct" - } - ], - "text": "Viral sinusitis (disorder)" - }, - "context": { - "reference": "Encounter/ee9bd275-49c9-4e40-bc78-ebe53bbfb123" - }, - "id": "cc454676-ccdc-4792-a5c9-cfbedce2ab33", - "meta": { - "lastUpdated": "2019-04-09T12:25:36.525019+00:00", - "versionId": "MTU1NDgxMjczNjUyNTAxOTAwMA" - }, - "onsetDateTime": "2011-02-26T10:12:53-05:00", - "resourceType": "Condition", - "subject": { - "reference": "Patient/6f7acde5-db81-4361-82cf-886893a3280c" - }, - "verificationStatus": "confirmed" - }, - "search": { - "mode": "match" - } - }, - { - "fullUrl": "https://syntheticmass.mitre.org/v1/fhir/Condition/bab430ff-5b09-4e4a-8871-6e4fcb84fa17", - "resource": { - "assertedDate": "2009-04-05T11:12:53-04:00", - "clinicalStatus": "active", - "code": { - "coding": [ - { - "code": "38341003", - "display": "Hypertension", - "system": "http://snomed.info/sct" - } - ], - "text": "Hypertension" - }, - "context": { - "reference": "Encounter/630a642d-0402-454c-9426-c399cf9b2aab" - }, - "id": "bab430ff-5b09-4e4a-8871-6e4fcb84fa17", - "meta": { - "lastUpdated": "2019-04-09T12:25:36.486194+00:00", - "versionId": "MTU1NDgxMjczNjQ4NjE5NDAwMA" - }, - "onsetDateTime": "2009-04-05T11:12:53-04:00", - "resourceType": "Condition", - "subject": { - "reference": "Patient/6f7acde5-db81-4361-82cf-886893a3280c" - }, - "verificationStatus": "confirmed" - }, - "search": { - "mode": "match" - } - }, - { - "fullUrl": "https://syntheticmass.mitre.org/v1/fhir/Condition/25b86d4b-5d09-47c6-9446-b93b067e63ec", - "resource": { - "abatementDateTime": "2009-01-31T10:12:53-05:00", - "assertedDate": "2009-01-10T10:12:53-05:00", - "clinicalStatus": "resolved", - "code": { - "coding": [ - { - "code": "75498004", - "display": "Acute bacterial sinusitis (disorder)", - "system": "http://snomed.info/sct" - } - ], - "text": "Acute bacterial sinusitis (disorder)" - }, - "context": { - "reference": "Encounter/051b0d30-03d3-4d6d-a070-f8d363ef277f" - }, - "id": "25b86d4b-5d09-47c6-9446-b93b067e63ec", - "meta": { - "lastUpdated": "2019-04-09T12:25:36.461769+00:00", - "versionId": "MTU1NDgxMjczNjQ2MTc2OTAwMA" - }, - "onsetDateTime": "2009-01-10T10:12:53-05:00", - "resourceType": "Condition", - "subject": { - "reference": "Patient/6f7acde5-db81-4361-82cf-886893a3280c" - }, - "verificationStatus": "confirmed" - }, - "search": { - "mode": "match" - } - }, - { - "fullUrl": "https://syntheticmass.mitre.org/v1/fhir/Condition/33d0016b-3d90-4647-b797-49d5b874537b", - "resource": { - "abatementDateTime": "2009-10-27T11:12:53-04:00", - "assertedDate": "2009-10-06T11:12:53-04:00", - "clinicalStatus": "resolved", - "code": { - "coding": [ - { - "code": "444814009", - "display": "Viral sinusitis (disorder)", - "system": "http://snomed.info/sct" - } - ], - "text": "Viral sinusitis (disorder)" - }, - "context": { - "reference": "Encounter/6956bf29-4bc2-4e41-af3d-1cd3d398eb84" - }, - "id": "33d0016b-3d90-4647-b797-49d5b874537b", - "meta": { - "lastUpdated": "2019-04-09T12:25:36.478091+00:00", - "versionId": "MTU1NDgxMjczNjQ3ODA5MTAwMA" - }, - "onsetDateTime": "2009-10-06T11:12:53-04:00", - "resourceType": "Condition", - "subject": { - "reference": "Patient/6f7acde5-db81-4361-82cf-886893a3280c" - }, - "verificationStatus": "confirmed" - }, - "search": { - "mode": "match" - } - } - ], - "link": [ - { - "relation": "search", - "url": "https://syntheticmass.mitre.org/v1/fhir/Condition/?subject%3Areference=Patient%2F6f7acde5-db81-4361-82cf-886893a3280c" - } - ], - "resourceType": "Bundle", - "total": 5, - "type": "searchset" -} - -FHIR_DIAGNOSTIC_REPORT = { - "code": { - "coding": [ - { - "code": "58410-2", - "display": "Complete blood count (hemogram) panel - Blood by Automated count", - "system": "http://loinc.org" - } - ], - "text": "Complete blood count (hemogram) panel - Blood by Automated count" - }, - "context": { - "reference": "Encounter/2a0e0f6c-493f-4c5b-bf89-5f98aee24f21" - }, - "effectiveDateTime": "2014-03-02T10:12:53-05:00", - "id": "ba93319c-3e5f-4074-ac2a-ff12e369a612", - "issued": "2014-03-02T10:12:53.714-05:00", - "meta": { - "lastUpdated": "2019-04-09T12:25:36.527685+00:00", - "versionId": "MTU1NDgxMjczNjUyNzY4NTAwMA" - }, - "resourceType": "DiagnosticReport", - "result": [ - { - "display": "Leukocytes [#/volume] in Blood by Automated count", - "reference": "Observation/324cc373-8b87-4354-bf62-afafe93a760c" - }, - { - "display": "Erythrocytes [#/volume] in Blood by Automated count", - "reference": "Observation/c90d0267-b660-48b8-813c-2ef0fa46e0f7" - }, - { - "display": "Hemoglobin [Mass/volume] in Blood", - "reference": "Observation/97684895-3a6e-4d46-9edd-31984bc7c3a6" - }, - { - "display": "Hematocrit [Volume Fraction] of Blood by Automated count", - "reference": "Observation/f0fcb049-32da-4da0-8687-540e494a3a26" - }, - { - "display": "MCV [Entitic volume] by Automated count", - "reference": "Observation/5814b42c-be27-4ceb-ba63-66e235e22b8f" - }, - { - "display": "MCH [Entitic mass] by Automated count", - "reference": "Observation/1c8d2ee3-2a7e-47f9-be16-abe4e9fa306b" - }, - { - "display": "MCHC [Mass/volume] by Automated count", - "reference": "Observation/6d24fd3b-f895-4f4c-a851-9b53ebd3cf49" - }, - { - "display": "Erythrocyte distribution width [Entitic volume] by Automated count", - "reference": "Observation/a4e4a6d9-dfb4-4a7c-bd6b-1599e3c16ec9" - }, - { - "display": "Platelets [#/volume] in Blood by Automated count", - "reference": "Observation/b0598af4-8ffe-43ba-84e5-b7fb49d3dcd7" - }, - { - "display": "Platelet distribution width [Entitic volume] in Blood by Automated count", - "reference": "Observation/7a9e943a-6638-47cb-931f-18c1b9d7ba3f" - }, - { - "display": "Platelet mean volume [Entitic volume] in Blood by Automated count", - "reference": "Observation/0adc582c-c620-4508-89e8-79ac134a6aa0" - } - ], - "status": "final", - "subject": { - "reference": "Patient/6f7acde5-db81-4361-82cf-886893a3280c" - } -} - -# the example is taken from hapi fhir server -# the example is modified, subject patient added -FHIR_SPECIMEN = { - "resourceType": "Specimen", - "id": "1168252", - "meta": { - "versionId": "1", - "lastUpdated": "2020-05-19T01:47:46.112+00:00" - }, - "identifier": [{ - "system": "urn:ietf:rfc:3986", - "value": "urn:uuid:bc269666-9972-11ea-a751-acde48001122" - }], - "accessionIdentifier": { - "system": "http://mghpathology.org/identifiers/specimens", - "value": "urn:id:TEST20-0002_A" - }, - "type": { - "coding": [{ - "system": "http://snomed.info/sct", - "code": "445405002", - "display": "Specimen obtained by surgical procedure" - }] - }, - "collection": { - "method": { - "coding": [{ - "system": "snowmed", - "code": "69466000", - "display": "Unknown procedure" - }] - }, - "bodySite": { - "coding": [{ - "system": "snowmed", - "code": "87100004", - "display": "Topography unknown" - }] - } - }, - "container": [{ - "identifier": [{ - "system": "urn:ietf:rfc:3986", - "value": "urn:uuid:bc269918-9972-11ea-a751-acde48001122" - }], - "type": { - "coding": [{ - "system": "http://snomed.info/sct", - "code": "434711009", - "display": "Specimen container" - }] - } - }], - "subject": { - "reference": "Patient/6f7acde5-db81-4361-82cf-886893a3280c" - } -} From 48272e60f1eb78bffbc6cca6cf202e133b11babc Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Tue, 26 May 2020 11:53:53 -0400 Subject: [PATCH 069/190] add fhir bundle schema --- .../restapi/fhir_ingest_utils.py | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/chord_metadata_service/restapi/fhir_ingest_utils.py b/chord_metadata_service/restapi/fhir_ingest_utils.py index 212ce26eb..db2ebf63d 100644 --- a/chord_metadata_service/restapi/fhir_ingest_utils.py +++ b/chord_metadata_service/restapi/fhir_ingest_utils.py @@ -12,7 +12,6 @@ from chord_metadata_service.chord.models import * from chord_metadata_service.phenopackets.models import * - FHIR_INGEST_SCHEMA = { "$id": "chord_metadata_service_fhir_ingest_schema", "$schema": "http://json-schema.org/draft-07/schema#", @@ -40,11 +39,43 @@ } +FHIR_BUNDLE_SCHEMA = { + "$id": "chord_metadata_service_fhir_bundle_schema", + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "FHIR Bundle schema", + "type": "object", + "properties": { + "resourceType": { + "type": "string", + "const": "Bundle", + "description": "Collection of resources." + }, + "entry": { + "type": "array", + "items": { + "type": "object", + "properties": { + "resource": {"type": "object"} + }, + "additionalProperties": True, + "required": ["resource"] + } + } + }, + "additionalProperties": True, + "required": ["resourceType", "entry"] +} + + def _parse_reference(ref): """ FHIR test data has reference object in a format "ResourceType/uuid" """ return ref.split('/')[-1] +def _check_fhir_schema(schema, obj): + return + + @api_view(["POST"]) @permission_classes([AllowAny]) def ingest_fhir(request): From ffe0cb871581cf95915fcbfbc01d7747b8f916e1 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Tue, 26 May 2020 18:38:37 -0400 Subject: [PATCH 070/190] add schema check --- .../restapi/fhir_ingest_utils.py | 89 ++++++++++++------- examples/conditions.json | 1 + 2 files changed, 56 insertions(+), 34 deletions(-) diff --git a/chord_metadata_service/restapi/fhir_ingest_utils.py b/chord_metadata_service/restapi/fhir_ingest_utils.py index db2ebf63d..9c7b3f9d5 100644 --- a/chord_metadata_service/restapi/fhir_ingest_utils.py +++ b/chord_metadata_service/restapi/fhir_ingest_utils.py @@ -72,50 +72,59 @@ def _parse_reference(ref): return ref.split('/')[-1] -def _check_fhir_schema(schema, obj): - return +def _check_schema(schema, obj, additional_info=None): + """ Validates schema and catches errors. """ + try: + jsonschema.validate(obj, schema) + except jsonschema.exceptions.ValidationError: + v = jsonschema.Draft7Validator(schema) + errors = [e for e in v.iter_errors(obj)] + error_messages = [] + for i, error in enumerate(errors, 1): + error_messages.append(f"{i} validation error {'.'.join(str(v) for v in error.path)}: {error.message}") + raise ValidationError(f"{additional_info+' ' if additional_info else None}errors: {error_messages}") @api_view(["POST"]) @permission_classes([AllowAny]) def ingest_fhir(request): - try: - jsonschema.validate(request.data, FHIR_INGEST_SCHEMA) - except jsonschema.exceptions.ValidationError: - v = jsonschema.Draft7Validator(FHIR_INGEST_SCHEMA) - errors = [e for e in v.iter_errors(request.data)] - for i, error in enumerate(errors, 1): - return Response(bad_request_error( - f"{i} Validation error in {'.'.join(str(v) for v in error.path)}: {error.message}" - )) + """ + View to ingest FHIR data. + Takes FHIR Bundles (collections of resources) of the following types: + Patient, Observation, Condition, Specimen. + """ + + # check schema of ingest body + _check_schema(FHIR_INGEST_SCHEMA, request.data, 'ingest body') + + # check if dataset exists if not Dataset.objects.filter(identifier=request.data["dataset_id"]).exists(): return Response(bad_request_error(f"Dataset with ID {request.data['dataset_id']} does not exist"), status=400) - # create new phenopacket for each individual - # fake metadata - meta_data_obj = MetaData.objects.create( - created_by=request.data["metadata"]["created_by"], - phenopacket_schema_version="1.0.0-RC3", - external_references=[] - ) - # patients-individuals with open(request.data["patients"], "r") as p_file: try: patients_data = json.load(p_file) - if isinstance(patients_data, dict): - # List of FHIR resources is of ResourceType "Bundle" - for item in patients_data["entry"]: - individual_data = patient_to_individual(item["resource"]) - individual, _ = Individual.objects.get_or_create(**individual_data) - phenopacket = Phenopacket.objects.create( - id=str(uuid.uuid4()), - subject=individual, - meta_data=meta_data_obj, - dataset=Dataset.objects.get(identifier=request.data["dataset_id"]) - ) - print(f'Phenopacket {phenopacket.id} created') + # check if Patients data follows FHIR Bundle schema + _check_schema(FHIR_BUNDLE_SCHEMA, patients_data, 'patients data') + for item in patients_data["entry"]: + individual_data = patient_to_individual(item["resource"]) + individual, _ = Individual.objects.get_or_create(**individual_data) + # create metadata for Phenopacket + meta_data_obj, _ = MetaData.objects.get_or_create( + created_by=request.data["metadata"]["created_by"], + phenopacket_schema_version="1.0.0-RC3", + external_references=[] + ) + # create new phenopacket for each individual + phenopacket = Phenopacket.objects.create( + id=str(uuid.uuid4()), + subject=individual, + meta_data=meta_data_obj, + dataset=Dataset.objects.get(identifier=request.data["dataset_id"]) + ) + print(f'Phenopacket {phenopacket.id} created') except json.decoder.JSONDecodeError as e: return Response(bad_request_error(f"Invalid JSON provided (message: {e})"), status=400) @@ -124,10 +133,13 @@ def ingest_fhir(request): with open(request.data["observations"], "r") as obs_file: try: observations_data = json.load(obs_file) + # check if Observations data follows FHIR Bundle schema + _check_schema(FHIR_BUNDLE_SCHEMA, observations_data, 'observations data') for item in observations_data["entry"]: phenotypic_feature_data = observation_to_phenotypic_feature(item["resource"]) + # Observation must have a subject if not item["resource"]["subject"]: - return Response(bad_request_error(f"Subject is required."), status=404) + return Response(bad_request_error(f"Observation's subject is required."), status=404) subject = _parse_reference(item["resource"]["subject"]["reference"]) phenotypic_feature, _ = PhenotypicFeature.objects.get_or_create( @@ -142,9 +154,12 @@ def ingest_fhir(request): with open(request.data["conditions"]) as c_file: try: conditions_data = json.load(c_file) + # check if Conditions data follows FHIR Bundle schema + _check_schema(FHIR_BUNDLE_SCHEMA, conditions_data, 'conditions data') for item in conditions_data["entry"]: disease_data = condition_to_disease(item["resource"]) - disease = Disease.objects.create(**disease_data) + disease= Disease.objects.create(**disease_data) + # Condition must have a subject if not item["resource"]["subject"]: return Response(bad_request_error(f"Subject is required."), status=404) subject = _parse_reference(item["resource"]["subject"]["reference"]) @@ -159,11 +174,17 @@ def ingest_fhir(request): with open(request.data["specimens"], "r") as s_file: try: specimens_data = json.load(s_file) + # check if Specimens data follows FHIR Bundle schema + _check_schema(FHIR_BUNDLE_SCHEMA, specimens_data, 'specimens data') for item in specimens_data["entry"]: biosample_data = specimen_to_biosample(item["resource"]) procedure, _ = Procedure.objects.get_or_create(**biosample_data["procedure"]) + # Specimen must have a subject + if not biosample_data["individual"]: + return Response(bad_request_error(f"Specimen's subject is required."), status=404) + individual_id = _parse_reference(biosample_data["individual"]) - Biosample.objects.create( + biosample, _ = Biosample.objects.get_or_create( id=biosample_data["id"], procedure=procedure, individual=Individual.objects.get(id=individual_id), diff --git a/examples/conditions.json b/examples/conditions.json index f7374e7b9..420c76323 100644 --- a/examples/conditions.json +++ b/examples/conditions.json @@ -1,4 +1,5 @@ { + "resourceType": "Bundle", "entry": [ { "fullUrl": "https://syntheticmass.mitre.org/v1/fhir/Condition/bab430ff-5b09-4e4a-8871-6e4fcb84fa17", From 80e93f761ab2f6a40335a0c0469aa4aff3a667fb Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Wed, 27 May 2020 13:46:25 -0400 Subject: [PATCH 071/190] move utils, schemas and views in right locations --- chord_metadata_service/metadata/urls.py | 3 +- chord_metadata_service/restapi/fhir_utils.py | 118 ++++++++++- chord_metadata_service/restapi/schemas.py | 60 ++++++ chord_metadata_service/restapi/urls.py | 3 - ...r_ingest_utils.py => views_ingest_fhir.py} | 188 ++---------------- 5 files changed, 197 insertions(+), 175 deletions(-) rename chord_metadata_service/restapi/{fhir_ingest_utils.py => views_ingest_fhir.py} (53%) diff --git a/chord_metadata_service/metadata/urls.py b/chord_metadata_service/metadata/urls.py index 11203bdf9..9676c91ad 100644 --- a/chord_metadata_service/metadata/urls.py +++ b/chord_metadata_service/metadata/urls.py @@ -15,7 +15,7 @@ """ from django.contrib import admin from django.urls import path, include -from chord_metadata_service.restapi import api_views, urls as restapi_urls +from chord_metadata_service.restapi import api_views, views_ingest_fhir, urls as restapi_urls from chord_metadata_service.chord import views_ingest, views_search from rest_framework.schemas import get_schema_view from rest_framework_swagger.views import get_swagger_view @@ -56,6 +56,7 @@ path('search', views_search.chord_search, name="search"), path('fhir-search', views_search.fhir_public_search, name="fhir-search"), path('private/fhir-search', views_search.fhir_private_search, name="fhir-private-search"), + path('private/ingest-fhir', views_ingest_fhir.ingest_fhir, name="ingest-fhir"), path('private/search', views_search.chord_private_search, name="private-search"), path('private/tables//search', views_search.chord_private_table_search, name="private-table-search"), ] + ([path('admin/', admin.site.urls)] if DEBUG else []) diff --git a/chord_metadata_service/restapi/fhir_utils.py b/chord_metadata_service/restapi/fhir_utils.py index 60dcf8d35..2989a083d 100644 --- a/chord_metadata_service/restapi/fhir_utils.py +++ b/chord_metadata_service/restapi/fhir_utils.py @@ -81,7 +81,7 @@ def check_disease_onset(disease): return False -##################### Class-based FHIR conversion functions ##################### +##################### Phenopackets to FHIR class conversion functions ##################### def fhir_patient(obj): @@ -384,3 +384,119 @@ def fhir_composition(obj): composition.section.append(section_content) return composition.as_json() + + +##################### FHIR to Phenopackets class conversion functions ##################### +# There is no guide to map FHIR to Phenopackets + +def patient_to_individual(obj): + """ FHIR Patient to Individual. """ + + patient = p.Patient(obj) + individual = { + "id": patient.id + } + if patient.identifier: + individual["alternate_ids"] = [alternate_id.value for alternate_id in patient.identifier] + gender_to_sex = { + "male": "MALE", + "female": "FEMALE", + "other": "OTHER_SEX", + "unknown": "UNKNOWN_SEX" + } + if patient.gender: + individual["sex"] = gender_to_sex.get(patient.gender, "unknown") + if patient.birthDate: + individual["date_of_birth"] = patient.birthDate.isostring + if patient.active: + individual["active"] = patient.active + if patient.deceasedBoolean: + individual["deceased"] = patient.deceasedBoolean + return individual + + +def observation_to_phenotypic_feature(obj): + """ FHIR Observation to Phenopackets PhenotypicFeature. """ + + observation = obs.Observation(obj) + codeable_concept = observation.code # CodeableConcept + phenotypic_feature = { + # id is an integer AutoField, store legacy id in description + # TODO change + "description": observation.id, + "pftype": { + "id": codeable_concept.coding[0].code, + "label": codeable_concept.coding[0].display + # TODO collect system info in metadata + } + } + if observation.specimen: # FK to Biosample + phenotypic_feature["biosample"] = observation.specimen.reference + return phenotypic_feature + + +def condition_to_disease(obj): + """ FHIR Condition to Phenopackets Disease. """ + + condition = cond.Condition(obj) + codeable_concept = condition.code # CodeableConcept + disease = { + # id is an integer AutoField, legacy id can be a string + # "id": condition.id, + "term": { + "id": codeable_concept.coding[0].code, + "label": codeable_concept.coding[0].display + # TODO collect system info in metadata + } + } + # condition.stage.type is only in FHIR 4.0.0 version + return disease + + +def diagnostic_report_to_interpretation(obj): + """ FHIR DiagnosticReport to Phenopackets Interpretation. """ + # it hardly maps at all + return + + +def procedure_to_procedure(obj): + """ FHIR Procedure to Phenopackets Procedure. + The main semantic difference: + - phenopackets procedure is a procedure performed to extract a biosample; + - fhir procedure is a procedure performed on or for a patient + (e.g. documentation of patient's medication) + """ + + return + + +def specimen_to_biosample(obj): + """ FHIR Specimen to Phenopackets Biosample. """ + + specimen = s.Specimen(obj) + biosample = { + "id": specimen.id + } + if specimen.subject: + biosample["individual"] = specimen.subject.reference + if specimen.type: + codeable_concept = specimen.type # CodeableConcept + biosample["sampled_tissue"] = { + "id": codeable_concept.coding[0].code, + "label": codeable_concept.coding[0].display + # TODO collect system info in metadata + } + if specimen.collection: + method_codeable_concept = specimen.collection.method + bodysite_codeable_concept = specimen.collection.bodySite + biosample["procedure"] = { + "code": { + "id": method_codeable_concept.coding[0].code, + "label": method_codeable_concept.coding[0].display + }, + "body_site": { + "id": bodysite_codeable_concept.coding[0].code, + "label": bodysite_codeable_concept.coding[0].display + } + } + return biosample diff --git a/chord_metadata_service/restapi/schemas.py b/chord_metadata_service/restapi/schemas.py index ee1e6f529..822cc543f 100644 --- a/chord_metadata_service/restapi/schemas.py +++ b/chord_metadata_service/restapi/schemas.py @@ -13,6 +13,8 @@ "AGE_RANGE", "AGE_OR_AGE_RANGE", "EXTRA_PROPERTIES_SCHEMA", + "FHIR_BUNDLE_SCHEMA", + "FHIR_INGEST_SCHEMA", ] @@ -114,3 +116,61 @@ ONTOLOGY_CLASS ] } + + +############################### FHIR INGEST SCHEMAS ############################### +# The schemas used to validate FHIR data for ingestion + +FHIR_INGEST_SCHEMA = { + "$id": "chord_metadata_service_fhir_ingest_schema", + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "FHIR Ingest schema", + "type": "object", + "properties": { + "dataset_id": {"type": "string"}, + "patients": {"type": "string", "description": "Path to a patients file location."}, + "observations": {"type": "string", "description": "Path to an observations file location."}, + "conditions": {"type": "string", "description": "Path to a conditions file location."}, + "specimens": {"type": "string", "description": "Path to a specimens file location."}, + "metadata": { + "type": "object", + "properties": { + "created_by": {"type": "string"} + }, + "required": ["created_by"] + } + }, + "required": [ + "dataset_id", + "patients", + "metadata" + ], +} + + +FHIR_BUNDLE_SCHEMA = { + "$id": "chord_metadata_service_fhir_bundle_schema", + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "FHIR Bundle schema", + "type": "object", + "properties": { + "resourceType": { + "type": "string", + "const": "Bundle", + "description": "Collection of resources." + }, + "entry": { + "type": "array", + "items": { + "type": "object", + "properties": { + "resource": {"type": "object"} + }, + "additionalProperties": True, + "required": ["resource"] + } + } + }, + "additionalProperties": True, + "required": ["resourceType", "entry"] +} diff --git a/chord_metadata_service/restapi/urls.py b/chord_metadata_service/restapi/urls.py index def48b2a3..6d0ab5bff 100644 --- a/chord_metadata_service/restapi/urls.py +++ b/chord_metadata_service/restapi/urls.py @@ -5,7 +5,6 @@ from chord_metadata_service.patients import api_views as individual_views from chord_metadata_service.phenopackets import api_views as phenopacket_views from chord_metadata_service.mcode import api_views as mcode_views -from .fhir_ingest_utils import ingest_fhir # from .settings import DEBUG @@ -55,6 +54,4 @@ name="experiment-schema"), path('mcode_schema', mcode_views.get_mcode_schema, name="mcode-schema"), - # ingest fhir - path('fhir/ingest', ingest_fhir, name="ingest-fhir"), ] diff --git a/chord_metadata_service/restapi/fhir_ingest_utils.py b/chord_metadata_service/restapi/views_ingest_fhir.py similarity index 53% rename from chord_metadata_service/restapi/fhir_ingest_utils.py rename to chord_metadata_service/restapi/views_ingest_fhir.py index 9c7b3f9d5..cbc52c5aa 100644 --- a/chord_metadata_service/restapi/fhir_ingest_utils.py +++ b/chord_metadata_service/restapi/views_ingest_fhir.py @@ -2,7 +2,13 @@ import uuid import jsonschema -from fhirclient.models import observation as obs, patient as p, condition as cond, specimen as s +from .schemas import FHIR_INGEST_SCHEMA, FHIR_BUNDLE_SCHEMA +from .fhir_utils import ( + patient_to_individual, + observation_to_phenotypic_feature, + condition_to_disease, + specimen_to_biosample +) from rest_framework.decorators import api_view, permission_classes from rest_framework.permissions import AllowAny @@ -12,58 +18,16 @@ from chord_metadata_service.chord.models import * from chord_metadata_service.phenopackets.models import * -FHIR_INGEST_SCHEMA = { - "$id": "chord_metadata_service_fhir_ingest_schema", - "$schema": "http://json-schema.org/draft-07/schema#", - "description": "FHIR Ingest schema", - "type": "object", - "properties": { - "dataset_id": {"type": "string"}, - "patients": {"type": "string", "description": "Path to a patients file location."}, - "observations": {"type": "string", "description": "Path to an observations file location."}, - "conditions": {"type": "string", "description": "Path to a conditions file location."}, - "specimens": {"type": "string", "description": "Path to a specimens file location."}, - "metadata": { - "type": "object", - "properties": { - "created_by": {"type": "string"} - }, - "required": ["created_by"] - } - }, - "required": [ - "dataset_id", - "patients", - "metadata" - ], -} - -FHIR_BUNDLE_SCHEMA = { - "$id": "chord_metadata_service_fhir_bundle_schema", - "$schema": "http://json-schema.org/draft-07/schema#", - "description": "FHIR Bundle schema", - "type": "object", - "properties": { - "resourceType": { - "type": "string", - "const": "Bundle", - "description": "Collection of resources." - }, - "entry": { - "type": "array", - "items": { - "type": "object", - "properties": { - "resource": {"type": "object"} - }, - "additionalProperties": True, - "required": ["resource"] - } - } - }, - "additionalProperties": True, - "required": ["resourceType", "entry"] +INGEST_BODY_EXAMPLE = { + "dataset_id": "62b5fc67-d925-4409-bb59-e1e9a1ef10ae", + "patients": "examples/patients.json", + "observations": "examples/observations.json", + "conditions": "examples/conditions.json", + "specimens": "examples/specimens.json", + "metadata": { + "created_by": "Ksenia Zaytseva" + } } @@ -82,7 +46,7 @@ def _check_schema(schema, obj, additional_info=None): error_messages = [] for i, error in enumerate(errors, 1): error_messages.append(f"{i} validation error {'.'.join(str(v) for v in error.path)}: {error.message}") - raise ValidationError(f"{additional_info+' ' if additional_info else None}errors: {error_messages}") + raise ValidationError(f"{additional_info + ' ' if additional_info else None}errors: {error_messages}") @api_view(["POST"]) @@ -158,7 +122,7 @@ def ingest_fhir(request): _check_schema(FHIR_BUNDLE_SCHEMA, conditions_data, 'conditions data') for item in conditions_data["entry"]: disease_data = condition_to_disease(item["resource"]) - disease= Disease.objects.create(**disease_data) + disease = Disease.objects.create(**disease_data) # Condition must have a subject if not item["resource"]["subject"]: return Response(bad_request_error(f"Subject is required."), status=404) @@ -195,119 +159,3 @@ def ingest_fhir(request): return Response(bad_request_error(f"Invalid JSON provided (message: {e})"), status=400) return Response(status=204) - - -################################# FHIR to Phenopackets ################################# -# There is no guide to map FHIR to Phenopackets - -def patient_to_individual(obj): - """ FHIR Patient to Individual. """ - - patient = p.Patient(obj) - individual = { - "id": patient.id - } - if patient.identifier: - individual["alternate_ids"] = [alternate_id.value for alternate_id in patient.identifier] - gender_to_sex = { - "male": "MALE", - "female": "FEMALE", - "other": "OTHER_SEX", - "unknown": "UNKNOWN_SEX" - } - if patient.gender: - individual["sex"] = gender_to_sex.get(patient.gender, "unknown") - if patient.birthDate: - individual["date_of_birth"] = patient.birthDate.isostring - if patient.active: - individual["active"] = patient.active - if patient.deceasedBoolean: - individual["deceased"] = patient.deceasedBoolean - return individual - - -def observation_to_phenotypic_feature(obj): - """ FHIR Observation to Phenopackets PhenotypicFeature. """ - - observation = obs.Observation(obj) - codeable_concept = observation.code # CodeableConcept - phenotypic_feature = { - # id is an integer AutoField, store legacy id in description - # TODO change - "description": observation.id, - "pftype": { - "id": codeable_concept.coding[0].code, - "label": codeable_concept.coding[0].display - # TODO collect system info in metadata - } - } - if observation.specimen: # FK to Biosample - phenotypic_feature["biosample"] = observation.specimen.reference - return phenotypic_feature - - -def condition_to_disease(obj): - """ FHIR Condition to Phenopackets Disease. """ - - condition = cond.Condition(obj) - codeable_concept = condition.code # CodeableConcept - disease = { - # id is an integer AutoField, legacy id can be a string - # "id": condition.id, - "term": { - "id": codeable_concept.coding[0].code, - "label": codeable_concept.coding[0].display - # TODO collect system info in metadata - } - } - # condition.stage.type is only in FHIR 4.0.0 version - return disease - - -def diagnostic_report_to_interpretation(obj): - """ FHIR DiagnosticReport to Phenopackets Interpretation. """ - # it hardly maps at all - return - - -def procedure_to_procedure(obj): - """ FHIR Procedure to Phenopackets Procedure. - The main semantic difference: - - phenopackets procedure is a procedure performed to extract a biosample; - - fhir procedure is a procedure performed on or for a patient - (e.g. documentation of patient's medication) - """ - - return - - -def specimen_to_biosample(obj): - """ FHIR Specimen to Phenopackets Biosample. """ - - specimen = s.Specimen(obj) - biosample = { - "id": specimen.id - } - if specimen.subject: - biosample["individual"] = specimen.subject.reference - if specimen.type: - codeable_concept = specimen.type # CodeableConcept - biosample["sampled_tissue"] = { - "id": codeable_concept.coding[0].code, - "label": codeable_concept.coding[0].display - # TODO collect system info in metadata - } - if specimen.collection: - method_codeable_concept = specimen.collection.method - bodysite_codeable_concept = specimen.collection.bodySite - biosample["procedure"] = { - "code": { - "id": method_codeable_concept.coding[0].code, - "label": method_codeable_concept.coding[0].display - }, - "body_site": { - "id": bodysite_codeable_concept.coding[0].code, - "label": bodysite_codeable_concept.coding[0].display - } - } - return biosample From 78eb923c44b6b4d634a39d984d7b60babcf8a47d Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Wed, 27 May 2020 21:32:57 -0400 Subject: [PATCH 072/190] add test for fhir ingest --- .../restapi/tests/constants.py | 17 +++++++++ .../restapi/tests/test_fhir_ingest.py | 38 +++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 chord_metadata_service/restapi/tests/constants.py create mode 100644 chord_metadata_service/restapi/tests/test_fhir_ingest.py diff --git a/chord_metadata_service/restapi/tests/constants.py b/chord_metadata_service/restapi/tests/constants.py new file mode 100644 index 000000000..418fd41fe --- /dev/null +++ b/chord_metadata_service/restapi/tests/constants.py @@ -0,0 +1,17 @@ +INVALID_INGEST_BODY = { + "dataset_id": "62b5fc67-d925-4409-bb59-e1e9a1ef10af", + "patients": "patients_file", + "metadata": { + "test": "required created_by is not present" + } +} + + +INVALID_FHIR_BUNDLE_1 = { + "resourceType": "NotBundle", + "entry": [ + { + "test": "required resource is not present" + } + ] +} diff --git a/chord_metadata_service/restapi/tests/test_fhir_ingest.py b/chord_metadata_service/restapi/tests/test_fhir_ingest.py new file mode 100644 index 000000000..e4d57a79a --- /dev/null +++ b/chord_metadata_service/restapi/tests/test_fhir_ingest.py @@ -0,0 +1,38 @@ +from rest_framework.test import APITestCase +from rest_framework.test import APIRequestFactory +from chord_metadata_service.chord.models import Project, Dataset +from chord_metadata_service.phenopackets.models import * +from chord_metadata_service.patients.models import Individual +from .constants import INVALID_INGEST_BODY, INVALID_FHIR_BUNDLE_1 +from ..views_ingest_fhir import ingest_fhir +from chord_metadata_service.chord.tests.constants import VALID_DATA_USE_1 +import copy + + +class TestFhirIngest(APITestCase): + + def setUp(self) -> None: + p = Project.objects.create(title="Project 1", description="Test") + self.d = Dataset.objects.create(title="Dataset 1", description="Test dataset", data_use=VALID_DATA_USE_1, + project=p) + + def test_ingest_body(self): + factory = APIRequestFactory() + request = factory.post('/private/ingest-fhir', INVALID_INGEST_BODY, format='json') + with self.assertRaises(ValidationError): + try: + ingest_fhir(request) + except ValidationError as e: + self.assertIn("created_by", e.message) + raise e + + def test_dataset_id(self): + factory = APIRequestFactory() + invalid_dataset_id_ingest = copy.deepcopy(INVALID_INGEST_BODY) + invalid_dataset_id_ingest["metadata"] = { + "created_by": "Name" + } + print(invalid_dataset_id_ingest) + request = factory.post('/private/ingest-fhir', invalid_dataset_id_ingest, format='json') + response = ingest_fhir(request) + self.assertEqual(response.status_code, 400) From 176b7d679c5a16c764c59ec906f0d49a9df6ab31 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 1 Jun 2020 10:13:08 -0400 Subject: [PATCH 073/190] Update markdown --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 16393b76a..7b6bcc4d6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,7 +25,7 @@ isodate==0.6.0 itypes==1.2.0 Jinja2==2.11.2 jsonschema==3.2.0 -Markdown==3.2.1 +Markdown==3.2.2 MarkupSafe==1.1.1 more-itertools==8.3.0 nose==1.3.7 From c0b394ba82410bf1b04f9086099ec40a7a9db56b Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 1 Jun 2020 10:14:45 -0400 Subject: [PATCH 074/190] Update resources app with model (moved from phenopackets) --- chord_metadata_service/chord/ingest.py | 14 +++++--- .../chord/tests/example_ingest.py | 6 ++-- .../migrations/0012_auto_20200525_2116.py | 26 ++++++++++++++ .../phenopackets/tests/constants.py | 26 -------------- .../phenopackets/tests/test_api.py | 16 --------- .../phenopackets/tests/test_models.py | 16 +-------- .../resources/migrations/0001_initial.py | 32 +++++++++++++++++ chord_metadata_service/resources/models.py | 22 +++++++++++- .../resources/tests/constants.py | 25 +++++++++++++ .../resources/tests/test_api.py | 23 ++++++++++++ .../resources/tests/test_models.py | 36 +++++++++++++++++++ chord_metadata_service/resources/utils.py | 7 ++++ chord_metadata_service/restapi/urls.py | 5 ++- 13 files changed, 188 insertions(+), 66 deletions(-) create mode 100644 chord_metadata_service/phenopackets/migrations/0012_auto_20200525_2116.py create mode 100644 chord_metadata_service/resources/migrations/0001_initial.py create mode 100644 chord_metadata_service/resources/tests/constants.py create mode 100644 chord_metadata_service/resources/tests/test_api.py create mode 100644 chord_metadata_service/resources/tests/test_models.py create mode 100644 chord_metadata_service/resources/utils.py diff --git a/chord_metadata_service/chord/ingest.py b/chord_metadata_service/chord/ingest.py index 8a2c8b2c8..99c4f4ef5 100644 --- a/chord_metadata_service/chord/ingest.py +++ b/chord_metadata_service/chord/ingest.py @@ -8,6 +8,7 @@ from chord_metadata_service.chord.models import Table from chord_metadata_service.experiments import models as em from chord_metadata_service.phenopackets import models as pm +from chord_metadata_service.resources import models as rm, utils as ru __all__ = [ @@ -197,12 +198,17 @@ def ingest_phenopacket(phenopacket_data, table_id) -> pm.Phenopacket: resources_db = [] for rs in meta_data.get("resources", []): - rs_obj, _ = pm.Resource.objects.get_or_create( - id=rs["id"], # TODO: This ID is a bit iffy, because they're researcher-provided + namespace_prefix = rs["namespace_prefix"].strip() + version = rs.get("version", "").strip() + assigned_resource_id = ru.make_resource_id(namespace_prefix, version) + + rs_obj, _ = rm.Resource.objects.get_or_create( + # If this doesn't match assigned_resource_id, it'll throw anyway + id=rs.get("id", assigned_resource_id), name=rs["name"], - namespace_prefix=rs["namespace_prefix"], + namespace_prefix=namespace_prefix, url=rs["url"], - version=rs["version"], + version=version, iri_prefix=rs["iri_prefix"] ) resources_db.append(rs_obj) diff --git a/chord_metadata_service/chord/tests/example_ingest.py b/chord_metadata_service/chord/tests/example_ingest.py index 16d56a4b2..6cbf523d4 100644 --- a/chord_metadata_service/chord/tests/example_ingest.py +++ b/chord_metadata_service/chord/tests/example_ingest.py @@ -55,7 +55,7 @@ "submitted_by": "Peter R", "resources": [ { - "id": "hp", + "id": "HP:2019-04-08", "name": "human phenotype ontology", "namespace_prefix": "HP", "url": "http://purl.obolibrary.org/obo/hp.owl", @@ -63,7 +63,7 @@ "iri_prefix": "http://purl.obolibrary.org/obo/HP_" }, { - "id": "uberon", + "id": "UBERON:2019-03-08", "name": "uber anatomy ontology", "namespace_prefix": "UBERON", "url": "http://purl.obolibrary.org/obo/uberon.owl", @@ -71,7 +71,7 @@ "iri_prefix": "http://purl.obolibrary.org/obo/UBERON_" }, { - "id": "ncit", + "id": "NCIT:18.05d", "name": "NCI Thesaurus OBO Edition", "namespace_prefix": "NCIT", "url": "http://purl.obolibrary.org/obo/ncit.owl", diff --git a/chord_metadata_service/phenopackets/migrations/0012_auto_20200525_2116.py b/chord_metadata_service/phenopackets/migrations/0012_auto_20200525_2116.py new file mode 100644 index 000000000..54a2da914 --- /dev/null +++ b/chord_metadata_service/phenopackets/migrations/0012_auto_20200525_2116.py @@ -0,0 +1,26 @@ +# Generated by Django 2.2.12 on 2020-05-25 21:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('phenopackets', '0011_auto_20200519_1538'), + ('resources', '0001_initial'), + ] + + operations = [ + migrations.DeleteModel( + name='Resource', + ), + migrations.RemoveField( + model_name='metadata', + name='resources', + ), + migrations.AddField( + model_name='metadata', + name='resources', + field=models.ManyToManyField(help_text='A list of resources or ontologies referenced in the phenopacket', to='resources.Resource'), + ), + ] diff --git a/chord_metadata_service/phenopackets/tests/constants.py b/chord_metadata_service/phenopackets/tests/constants.py index bd0810cd5..cbef37fcd 100644 --- a/chord_metadata_service/phenopackets/tests/constants.py +++ b/chord_metadata_service/phenopackets/tests/constants.py @@ -175,32 +175,6 @@ } } -VALID_RESOURCE_1 = { - "id": "so", - "name": "Sequence types and features", - "url": "http://purl.obolibrary.org/obo/so.owl", - "version": "2015-11-24", - "namespace_prefix": "SO", - "iri_prefix": "http://purl.obolibrary.org/obo/SO_" -} - -VALID_RESOURCE_2 = { - "id": "hgnc", - "name": "HUGO Gene Nomenclature Committee", - "url": "https://www.genenames.org", - "version": "2019-08-08", - "namespace_prefix": "HGNC", - "iri_prefix": "https://www.genenames.org/data/gene-symbol-report/#!/hgnc_id/" -} - -DUPLICATE_RESOURCE_3 = { - "id": "hgnc", - "name": "HUGO Gene Nomenclature Committee", - "url": "https://www.genenames.org", - "version": "2019-08-08", - "namespace_prefix": "HGNC" -} - def valid_phenopacket(subject, meta_data): return dict( diff --git a/chord_metadata_service/phenopackets/tests/test_api.py b/chord_metadata_service/phenopackets/tests/test_api.py index e17298021..e6dfa4ae4 100644 --- a/chord_metadata_service/phenopackets/tests/test_api.py +++ b/chord_metadata_service/phenopackets/tests/test_api.py @@ -189,22 +189,6 @@ def test_invalid_disease(self): self.assertEqual(Disease.objects.count(), 0) -class CreateResourceTest(APITestCase): - - def setUp(self): - self.resource = VALID_RESOURCE_2 - self.duplicate_resource = DUPLICATE_RESOURCE_3 - - def test_resource(self): - response = get_response('resource-list', self.resource) - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(Resource.objects.count(), 1) - - def test_serializer(self): - serializer = ResourceSerializer(data=self.resource) - self.assertEqual(serializer.is_valid(), True) - - class CreateMetaDataTest(APITestCase): def setUp(self): diff --git a/chord_metadata_service/phenopackets/tests/test_models.py b/chord_metadata_service/phenopackets/tests/test_models.py index 6f1fc36e5..96d55ee71 100644 --- a/chord_metadata_service/phenopackets/tests/test_models.py +++ b/chord_metadata_service/phenopackets/tests/test_models.py @@ -2,6 +2,7 @@ from django.test import TestCase from ..models import * from .constants import * +from chord_metadata_service.resources.tests.constants import VALID_RESOURCE_1, VALID_RESOURCE_2 class BiosampleTest(TestCase): @@ -218,21 +219,6 @@ def test_interpretation_str(self): self.assertEqual(str(self.interpretation), str(self.interpretation.id)) -class ResourceTest(TestCase): - def setUp(self): - self.resource_1 = Resource.objects.create(**VALID_RESOURCE_1) - self.resource_2 = Resource.objects.create(**VALID_RESOURCE_2) - - def test_resource(self): - self.assertEqual(Resource.objects.count(), 2) - with self.assertRaises(IntegrityError): - Resource.objects.create(**DUPLICATE_RESOURCE_3) - - def test_resource_str(self): - self.assertEqual(str(self.resource_1), "so") - self.assertEqual(str(self.resource_2), "hgnc") - - class MetaDataTest(TestCase): def setUp(self): self.resource_1 = Resource.objects.create(**VALID_RESOURCE_1) diff --git a/chord_metadata_service/resources/migrations/0001_initial.py b/chord_metadata_service/resources/migrations/0001_initial.py new file mode 100644 index 000000000..bd88f9d55 --- /dev/null +++ b/chord_metadata_service/resources/migrations/0001_initial.py @@ -0,0 +1,32 @@ +# Generated by Django 2.2.12 on 2020-05-25 21:16 + +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Resource', + fields=[ + ('id', models.CharField(help_text='For OBO ontologies, the value of this string MUST always be the official OBO ID, which is always equivalent to the ID prefix in lower case. For other resources use the prefix in identifiers.org.', max_length=200, primary_key=True, serialize=False)), + ('name', models.CharField(help_text='The full name of the resource or ontology referred to by the id element.', max_length=200)), + ('namespace_prefix', models.CharField(help_text='Prefix for objects from this resource. In the case of ontology resources, this should be the CURIE prefix.', max_length=200)), + ('url', models.URLField(help_text='Resource URL. In the case of ontologies, this should be an OBO or OWL file. Other resources should link to the official or top-level url.')), + ('version', models.CharField(help_text='The version of the resource or ontology used to make the annotation.', max_length=200)), + ('iri_prefix', models.URLField(help_text='The IRI prefix, when used with the namespace prefix and an object ID, should resolve the term or object from the resource in question.')), + ('extra_properties', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema.', null=True)), + ('created', models.DateTimeField(auto_now=True)), + ('updated', models.DateTimeField(auto_now_add=True)), + ], + options={ + 'unique_together': {('namespace_prefix', 'version')}, + }, + ), + ] diff --git a/chord_metadata_service/resources/models.py b/chord_metadata_service/resources/models.py index 30d14751d..28c70d724 100644 --- a/chord_metadata_service/resources/models.py +++ b/chord_metadata_service/resources/models.py @@ -1,4 +1,5 @@ from django.contrib.postgres.fields import JSONField +from django.core.exceptions import ValidationError from django.db import models from chord_metadata_service.restapi.description_utils import rec_help @@ -14,7 +15,10 @@ class Resource(models.Model): FHIR: CodeSystem """ - # resource_id e.g. "id": "uniprot" + class Meta: + unique_together = (("namespace_prefix", "version"),) + + # resource_id e.g. "id": "uniprot:2019_07" id = models.CharField(primary_key=True, max_length=200, help_text=rec_help(d.RESOURCE, "id")) name = models.CharField(max_length=200, help_text=rec_help(d.RESOURCE, "name")) namespace_prefix = models.CharField(max_length=200, help_text=rec_help(d.RESOURCE, "namespace_prefix")) @@ -22,8 +26,24 @@ class Resource(models.Model): version = models.CharField(max_length=200, help_text=rec_help(d.RESOURCE, "version")) iri_prefix = models.URLField(max_length=200, help_text=rec_help(d.RESOURCE, "iri_prefix")) extra_properties = JSONField(blank=True, null=True, help_text=rec_help(d.RESOURCE, "extra_properties")) + created = models.DateTimeField(auto_now=True) updated = models.DateTimeField(auto_now_add=True) + def clean(self): + # For phenopackets compliance, we need to have a string identifier. Django does not allow compound keys, we + # ideally want to identify resources by the pair (namespace_prefix, version). In this case, we hack this by + # enforcing that id == (namespace_prefix, version). In the case of an unspecified version, enforce + # id == namespace_prefix. + if (self.version and self.id != f"{self.namespace_prefix}:{self.version}") or \ + (not self.version and self.id != self.namespace_prefix): + raise ValidationError({ + "id": [ValidationError("Resource ID must match the format 'namespace_prefix:version'")], + }) + + def save(self, *args, **kwargs): + self.clean() + return super().save(*args, **kwargs ) + def __str__(self): return str(self.id) diff --git a/chord_metadata_service/resources/tests/constants.py b/chord_metadata_service/resources/tests/constants.py new file mode 100644 index 000000000..0fb7f2dba --- /dev/null +++ b/chord_metadata_service/resources/tests/constants.py @@ -0,0 +1,25 @@ +VALID_RESOURCE_1 = { + "id": "SO:2015-11-24", + "name": "Sequence types and features", + "url": "http://purl.obolibrary.org/obo/so.owl", + "version": "2015-11-24", + "namespace_prefix": "SO", + "iri_prefix": "http://purl.obolibrary.org/obo/SO_" +} + +VALID_RESOURCE_2 = { + "id": "HGNC:2019-08-08", + "name": "HUGO Gene Nomenclature Committee", + "url": "https://www.genenames.org", + "version": "2019-08-08", + "namespace_prefix": "HGNC", + "iri_prefix": "https://www.genenames.org/data/gene-symbol-report/#!/hgnc_id/" +} + +DUPLICATE_RESOURCE_3 = { + "id": "HGNC:2019-08-08", + "name": "HUGO Gene Nomenclature Committee", + "url": "https://www.genenames.org", + "version": "2019-08-08", + "namespace_prefix": "HGNC" +} diff --git a/chord_metadata_service/resources/tests/test_api.py b/chord_metadata_service/resources/tests/test_api.py new file mode 100644 index 000000000..b6ca1c679 --- /dev/null +++ b/chord_metadata_service/resources/tests/test_api.py @@ -0,0 +1,23 @@ +from rest_framework import status +from rest_framework.test import APITestCase +from chord_metadata_service.restapi.tests.utils import get_response + +from ..models import Resource +from ..serializers import ResourceSerializer +from .constants import VALID_RESOURCE_2, DUPLICATE_RESOURCE_3 + + +class CreateResourceTest(APITestCase): + + def setUp(self): + self.resource = VALID_RESOURCE_2 + self.duplicate_resource = DUPLICATE_RESOURCE_3 + + def test_resource(self): + response = get_response('resource-list', self.resource) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(Resource.objects.count(), 1) + + def test_serializer(self): + serializer = ResourceSerializer(data=self.resource) + self.assertEqual(serializer.is_valid(), True) diff --git a/chord_metadata_service/resources/tests/test_models.py b/chord_metadata_service/resources/tests/test_models.py new file mode 100644 index 000000000..b64ada380 --- /dev/null +++ b/chord_metadata_service/resources/tests/test_models.py @@ -0,0 +1,36 @@ +from django.core.exceptions import ValidationError +from django.db.utils import IntegrityError +from django.test import TestCase + +from ..models import Resource +from .constants import VALID_RESOURCE_1, VALID_RESOURCE_2, DUPLICATE_RESOURCE_3 + + +class ResourceTest(TestCase): + """ + Test class for Resource model. + """ + + def setUp(self): + self.resource_1 = Resource.objects.create(**VALID_RESOURCE_1) + self.resource_2 = Resource.objects.create(**VALID_RESOURCE_2) + + def test_resource(self): + self.assertEqual(Resource.objects.count(), 2) + with self.assertRaises(IntegrityError): + Resource.objects.create(**DUPLICATE_RESOURCE_3) + + def test_resource_str(self): + self.assertEqual(str(self.resource_1), f"{self.resource_1.namespace_prefix}:{self.resource_1.version}") + self.assertEqual(str(self.resource_2), f"{self.resource_2.namespace_prefix}:{self.resource_2.version}") + + def test_invalid_id(self): + with self.assertRaises(ValidationError): + Resource.objects.create( + id="SO", + name="Sequence types and features", + namespace_prefix="SO", + url="https://raw.githubusercontent.com/The-Sequence-Ontology/SO-Ontologies/v3.1/so.owl", + version="3.1", + iri_prefix="http://purl.obolibrary.org/obo/SO_", + ) diff --git a/chord_metadata_service/resources/utils.py b/chord_metadata_service/resources/utils.py new file mode 100644 index 000000000..648a41cb2 --- /dev/null +++ b/chord_metadata_service/resources/utils.py @@ -0,0 +1,7 @@ +__all__ = ["make_resource_id"] + + +def make_resource_id(namespace_prefix: str, version: str = "") -> str: + namespace_prefix = namespace_prefix.strip() + version = version.strip() + return namespace_prefix if not version else f"{namespace_prefix}:{version}" diff --git a/chord_metadata_service/restapi/urls.py b/chord_metadata_service/restapi/urls.py index 215884a30..32b56ad93 100644 --- a/chord_metadata_service/restapi/urls.py +++ b/chord_metadata_service/restapi/urls.py @@ -5,6 +5,7 @@ from chord_metadata_service.patients import api_views as individual_views from chord_metadata_service.phenopackets import api_views as phenopacket_views from chord_metadata_service.mcode import api_views as mcode_views +from chord_metadata_service.resources import api_views as resources_views router = routers.DefaultRouter(trailing_slash=False) @@ -28,7 +29,6 @@ router.register(r'genes', phenopacket_views.GeneViewSet) router.register(r'variants', phenopacket_views.VariantViewSet) router.register(r'diseases', phenopacket_views.DiseaseViewSet) -router.register(r'resources', phenopacket_views.ResourceViewSet) router.register(r'metadata', phenopacket_views.MetaDataViewSet) router.register(r'biosamples', phenopacket_views.BiosampleViewSet) router.register(r'phenopackets', phenopacket_views.PhenopacketViewSet) @@ -47,6 +47,9 @@ router.register(r'medicationstatements', mcode_views.MedicationStatementViewSet) router.register(r'mcodepackets', mcode_views.MCodePacketViewSet) +# Resources app urls +router.register(r'resources', resources_views.ResourceViewSet) + urlpatterns = [ path('', include(router.urls)), # apps schemas From f622c761853eb7d083d23c5d5b56fa08db650a29 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 1 Jun 2020 10:14:58 -0400 Subject: [PATCH 075/190] Update experiment descriptions --- .../experiments/descriptions.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/chord_metadata_service/experiments/descriptions.py b/chord_metadata_service/experiments/descriptions.py index 3af447185..b0656c2ef 100644 --- a/chord_metadata_service/experiments/descriptions.py +++ b/chord_metadata_service/experiments/descriptions.py @@ -1,4 +1,4 @@ -from chord_metadata_service.restapi.description_utils import EXTRA_PROPERTIES +from chord_metadata_service.restapi.description_utils import EXTRA_PROPERTIES, ontology_class EXPERIMENT = { @@ -8,11 +8,20 @@ "reference_registry_id": "The IHEC EpiRR ID for this dataset, only for IHEC Reference Epigenome datasets. " "Otherwise leave empty.", - "qc_flags": "Any quanlity control observations can be noted here. This field can be omitted if empty", + "qc_flags": { + "description": "Any quality control observations can be noted here. This field can be omitted if empty", + "items": "A quality control observation.", + }, "experiment_type": "(Controlled Vocabulary) The assay target (e.g. ‘DNA Methylation’, ‘mRNA-Seq’, ‘smRNA-Seq’, " "'Histone H3K4me1').", - "experiment_ontology": "(Ontology: OBI) links to experiment ontology information.", - "molecule_ontology": "(Ontology: SO) links to molecule ontology information.", + "experiment_ontology": { + "description": "Links to experiment ontology information (e.g. via the OBI ontology.)", + "items": ontology_class("describing the experiment"), + }, + "molecule_ontology": { + "description": "Links to molecule ontology information (e.g. via the SO ontology.)", + "items": ontology_class("describing a molecular property"), + }, "molecule": "(Controlled Vocabulary) The type of molecule that was extracted from the biological material." "Include one of the following: total RNA, polyA RNA, cytoplasmic RNA, nuclear RNA, small RNA, " "genomic DNA, protein, or other.", From 3fedf29dd9c4336a413b902dd555ac5abec896eb Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 1 Jun 2020 10:15:41 -0400 Subject: [PATCH 076/190] Fix/improve some imports --- .../phenopackets/tests/test_api.py | 12 +++++++++++- .../restapi/tests/test_descriptions.py | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/chord_metadata_service/phenopackets/tests/test_api.py b/chord_metadata_service/phenopackets/tests/test_api.py index e6dfa4ae4..8de6b4cfd 100644 --- a/chord_metadata_service/phenopackets/tests/test_api.py +++ b/chord_metadata_service/phenopackets/tests/test_api.py @@ -2,7 +2,17 @@ from rest_framework.test import APITestCase from .constants import * from ..models import * -from ..serializers import * +from ..serializers import ( + BiosampleSerializer, + DiagnosisSerializer, + DiseaseSerializer, + GeneSerializer, + GenomicInterpretationSerializer, + MetaDataSerializer, + PhenopacketSerializer, + PhenotypicFeatureSerializer, + VariantSerializer, +) from chord_metadata_service.restapi.tests.utils import get_response diff --git a/chord_metadata_service/restapi/tests/test_descriptions.py b/chord_metadata_service/restapi/tests/test_descriptions.py index e31cb1884..a0a05d3a9 100644 --- a/chord_metadata_service/restapi/tests/test_descriptions.py +++ b/chord_metadata_service/restapi/tests/test_descriptions.py @@ -1,5 +1,5 @@ from django.test import TestCase -from . import description_utils as du +from .. import description_utils as du TEST_SCHEMA_1 = {"type": "string"} From 50c9403201dd28e3450bbaf53d1d248d17910d5b Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 1 Jun 2020 10:44:17 -0400 Subject: [PATCH 077/190] Add experiments migration corresponding with help text changes --- .../migrations/0008_auto_20200601_1438.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 chord_metadata_service/experiments/migrations/0008_auto_20200601_1438.py diff --git a/chord_metadata_service/experiments/migrations/0008_auto_20200601_1438.py b/chord_metadata_service/experiments/migrations/0008_auto_20200601_1438.py new file mode 100644 index 000000000..771eab656 --- /dev/null +++ b/chord_metadata_service/experiments/migrations/0008_auto_20200601_1438.py @@ -0,0 +1,31 @@ +# Generated by Django 2.2.12 on 2020-06-01 14:38 + +import chord_metadata_service.restapi.validators +import django.contrib.postgres.fields +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('experiments', '0007_auto_20200519_1538'), + ] + + operations = [ + migrations.AlterField( + model_name='experiment', + name='experiment_ontology', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, help_text='Links to experiment ontology information (e.g. via the OBI ontology.)', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)]), + ), + migrations.AlterField( + model_name='experiment', + name='molecule_ontology', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, help_text='Links to molecule ontology information (e.g. via the SO ontology.)', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)]), + ), + migrations.AlterField( + model_name='experiment', + name='qc_flags', + field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(help_text='Any quality control observations can be noted here. This field can be omitted if empty', max_length=100), blank=True, default=list, size=None), + ), + ] From 82a45c3dff875c102f2b6f7111f0fa1dd05f8b9f Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 1 Jun 2020 12:54:00 -0400 Subject: [PATCH 078/190] Move resource ingest into a reusable function --- chord_metadata_service/chord/ingest.py | 38 ++++++++++++++------------ 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/chord_metadata_service/chord/ingest.py b/chord_metadata_service/chord/ingest.py index 99c4f4ef5..b1a6f0a95 100644 --- a/chord_metadata_service/chord/ingest.py +++ b/chord_metadata_service/chord/ingest.py @@ -14,6 +14,7 @@ __all__ = [ "METADATA_WORKFLOWS", "WORKFLOWS_PATH", + "ingest_resource", "DATA_TYPE_INGEST_FUNCTION_MAP", ] @@ -89,6 +90,24 @@ def _query_and_check_nulls(obj: dict, key: str, transform: Callable = lambda x: return {f"{key}__isnull": True} if value is None else {key: transform(value)} +def ingest_resource(resource: dict) -> rm.Resource: + namespace_prefix = resource["namespace_prefix"].strip() + version = resource.get("version", "").strip() + assigned_resource_id = ru.make_resource_id(namespace_prefix, version) + + rs_obj, _ = rm.Resource.objects.get_or_create( + # If this doesn't match assigned_resource_id, it'll throw anyway + id=resource.get("id", assigned_resource_id), + name=resource["name"], + namespace_prefix=namespace_prefix, + url=resource["url"], + version=version, + iri_prefix=resource["iri_prefix"] + ) + + return rs_obj + + def ingest_experiment(experiment_data, table_id) -> em.Experiment: """Ingests a single experiment.""" @@ -196,22 +215,7 @@ def ingest_phenopacket(phenopacket_data, table_id) -> pm.Phenopacket: ) diseases_db.append(d_obj.id) - resources_db = [] - for rs in meta_data.get("resources", []): - namespace_prefix = rs["namespace_prefix"].strip() - version = rs.get("version", "").strip() - assigned_resource_id = ru.make_resource_id(namespace_prefix, version) - - rs_obj, _ = rm.Resource.objects.get_or_create( - # If this doesn't match assigned_resource_id, it'll throw anyway - id=rs.get("id", assigned_resource_id), - name=rs["name"], - namespace_prefix=namespace_prefix, - url=rs["url"], - version=version, - iri_prefix=rs["iri_prefix"] - ) - resources_db.append(rs_obj) + resources_db = [ingest_resource(rs) for rs in meta_data.get("resources", [])] meta_data_obj = pm.MetaData( created_by=meta_data["created_by"], @@ -221,7 +225,7 @@ def ingest_phenopacket(phenopacket_data, table_id) -> pm.Phenopacket: ) meta_data_obj.save() - meta_data_obj.resources.set(resources_db) # TODO: primary key ??? + meta_data_obj.resources.set(resources_db) new_phenopacket = pm.Phenopacket( id=new_phenopacket_id, From d03d4afcdce8cc86db2b0de91655343121d77b4b Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 1 Jun 2020 12:58:30 -0400 Subject: [PATCH 079/190] Add additional_resources field to dataset Add computed resources field Add validation for unique ontology prefices --- .../0017_dataset_additional_resources.py | 19 +++++++++++ chord_metadata_service/chord/models.py | 33 +++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 chord_metadata_service/chord/migrations/0017_dataset_additional_resources.py diff --git a/chord_metadata_service/chord/migrations/0017_dataset_additional_resources.py b/chord_metadata_service/chord/migrations/0017_dataset_additional_resources.py new file mode 100644 index 000000000..bd388cfeb --- /dev/null +++ b/chord_metadata_service/chord/migrations/0017_dataset_additional_resources.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.12 on 2020-06-01 14:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('resources', '0001_initial'), + ('chord', '0016_auto_20200519_2100'), + ] + + operations = [ + migrations.AddField( + model_name='dataset', + name='additional_resources', + field=models.ManyToManyField(blank=True, help_text="Any resource objects linked to this dataset that aren't specified by a phenopacket in the dataset.", to='resources.Resource'), + ), + ] diff --git a/chord_metadata_service/chord/models.py b/chord_metadata_service/chord/models.py index 1f318cb9a..149520eb7 100644 --- a/chord_metadata_service/chord/models.py +++ b/chord_metadata_service/chord/models.py @@ -1,8 +1,12 @@ +import collections import uuid +from django.core.exceptions import ValidationError from django.contrib.postgres.fields import JSONField from django.db import models from django.utils import timezone +from chord_metadata_service.phenopackets.models import Phenopacket +from chord_metadata_service.resources.models import Resource from .data_types import DATA_TYPE_EXPERIMENT, DATA_TYPE_PHENOPACKET @@ -55,6 +59,24 @@ class Dataset(models.Model): data_use = JSONField() linked_field_sets = JSONField(blank=True, default=list, help_text="Data type fields which are linked together.") + additional_resources = models.ManyToManyField(Resource, blank=True, help_text="Any resource objects linked to this " + "dataset that aren't specified by a " + "phenopacket in the dataset.") + + @property + def resources(self): + # Union of phenopacket resources and any additional resources for other table types + return Resource.objects.filter(id__in={ + *(r.id for r in self.additional_resources.objects.all()), + *( + r.id + for p in Phenopacket.objects.filter( + table_id__in={t.id for t in self.table_ownership} + ).prefetch_related("meta_data", "meta_data__resources") + for r in p.meta_data.resources.objects.all() + ), + }) + @property def n_of_tables(self): return TableOwnership.objects.filter(dataset=self).count() @@ -124,6 +146,17 @@ def n_of_tables(self): created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) + def clean(self): + # Check that all namespace prefices are unique within a dataset + c = collections.Counter(r.namespace_prefix for r in self.resources) + mc = next(c.most_common(1), (None, 0)) + if mc[1] > 1: + raise ValidationError(f"Dataset {self.identifier} cannot have ambiguous resource namespace prefix {mc[0]}") + + def save(self, *args, **kwargs): + self.clean() + super().save(*args, **kwargs) + def __str__(self): return f"{self.title} (ID: {self.identifier})" From aef22480f48b37d708f489c12876dd30f66a8cf5 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 1 Jun 2020 12:59:30 -0400 Subject: [PATCH 080/190] Change ingestion for experiments to allow resources to be specified Experiments now specified in a field as well --- chord_metadata_service/chord/views_ingest.py | 23 ++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/chord_metadata_service/chord/views_ingest.py b/chord_metadata_service/chord/views_ingest.py index c2a1c345a..96fee1b16 100644 --- a/chord_metadata_service/chord/views_ingest.py +++ b/chord_metadata_service/chord/views_ingest.py @@ -13,8 +13,9 @@ from chord_lib.responses import errors from chord_lib.workflows import get_workflow, get_workflow_resource, workflow_exists -from .ingest import METADATA_WORKFLOWS, WORKFLOWS_PATH, DATA_TYPE_INGEST_FUNCTION_MAP -from .models import Table +from .data_types import DATA_TYPE_EXPERIMENT +from .ingest import METADATA_WORKFLOWS, WORKFLOWS_PATH, DATA_TYPE_INGEST_FUNCTION_MAP, ingest_resource +from .models import Table, TableOwnership class WDLRenderer(BaseRenderer): @@ -75,6 +76,8 @@ def ingest(request): table_id = str(uuid.UUID(table_id)) # Normalize dataset ID to UUID's str format. + dataset = TableOwnership.objects.get(table_id=table_id).dataset + workflow_id = request.data["workflow_id"].strip() workflow_outputs = request.data["workflow_outputs"] @@ -88,11 +91,23 @@ def ingest(request): with open(workflow_outputs["json_document"], "r") as jf: try: + dt = workflow["data_type"] json_data = json.load(jf) - ingest_fn = DATA_TYPE_INGEST_FUNCTION_MAP[workflow["data_type"]] - if isinstance(json_data, list): + ingest_fn = DATA_TYPE_INGEST_FUNCTION_MAP[dt] + + # TODO: Better mechanism for workflow-specific ingestion handling + + if dt == DATA_TYPE_EXPERIMENT: + for rs in json_data.get("resources", []): + dataset.additional_resources.add(ingest_resource(rs)) + + for exp in json_data.get("experiments", []): + ingest_fn(exp, table_id) + + elif isinstance(json_data, list): for obj in json_data: ingest_fn(obj, table_id) + else: ingest_fn(json_data, table_id) From 00108f1a6738d1ef80c41e2617a6baa951e327c6 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 1 Jun 2020 13:04:48 -0400 Subject: [PATCH 081/190] Fix errors --- chord_metadata_service/chord/models.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/chord_metadata_service/chord/models.py b/chord_metadata_service/chord/models.py index 149520eb7..e6b6e3587 100644 --- a/chord_metadata_service/chord/models.py +++ b/chord_metadata_service/chord/models.py @@ -67,13 +67,13 @@ class Dataset(models.Model): def resources(self): # Union of phenopacket resources and any additional resources for other table types return Resource.objects.filter(id__in={ - *(r.id for r in self.additional_resources.objects.all()), + *(r.id for r in self.additional_resources.all()), *( r.id for p in Phenopacket.objects.filter( - table_id__in={t.id for t in self.table_ownership} + table_id__in={t.id for t in self.table_ownership.all()} ).prefetch_related("meta_data", "meta_data__resources") - for r in p.meta_data.resources.objects.all() + for r in p.meta_data.resources.all() ), }) @@ -149,7 +149,7 @@ def n_of_tables(self): def clean(self): # Check that all namespace prefices are unique within a dataset c = collections.Counter(r.namespace_prefix for r in self.resources) - mc = next(c.most_common(1), (None, 0)) + mc = (*c.most_common(1), (None, 0))[0] if mc[1] > 1: raise ValidationError(f"Dataset {self.identifier} cannot have ambiguous resource namespace prefix {mc[0]}") From 8b6cfc255de7c5401889082c55084f69c380dd28 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 1 Jun 2020 13:10:50 -0400 Subject: [PATCH 082/190] Remove unused table ownership fields --- .../migrations/0018_auto_20200601_1708.py | 21 +++++++++++++++++++ chord_metadata_service/chord/models.py | 5 +---- .../chord/tests/constants.py | 1 - .../chord/tests/test_ingest.py | 3 +-- .../chord/tests/test_models.py | 4 ---- 5 files changed, 23 insertions(+), 11 deletions(-) create mode 100644 chord_metadata_service/chord/migrations/0018_auto_20200601_1708.py diff --git a/chord_metadata_service/chord/migrations/0018_auto_20200601_1708.py b/chord_metadata_service/chord/migrations/0018_auto_20200601_1708.py new file mode 100644 index 000000000..a356b04ff --- /dev/null +++ b/chord_metadata_service/chord/migrations/0018_auto_20200601_1708.py @@ -0,0 +1,21 @@ +# Generated by Django 2.2.12 on 2020-06-01 17:08 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('chord', '0017_dataset_additional_resources'), + ] + + operations = [ + migrations.RemoveField( + model_name='tableownership', + name='data_type', + ), + migrations.RemoveField( + model_name='tableownership', + name='sample', + ), + ] diff --git a/chord_metadata_service/chord/models.py b/chord_metadata_service/chord/models.py index e6b6e3587..ad0af3161 100644 --- a/chord_metadata_service/chord/models.py +++ b/chord_metadata_service/chord/models.py @@ -170,15 +170,12 @@ class TableOwnership(models.Model): table_id = models.CharField(primary_key=True, max_length=200) service_id = models.CharField(max_length=200) service_artifact = models.CharField(max_length=200, default="") - data_type = models.CharField(max_length=200) # TODO: Is this needed? TODO: Remove # Delete table ownership upon project/dataset deletion dataset = models.ForeignKey(Dataset, on_delete=models.CASCADE, related_name='table_ownership') - # If not specified, compound table which may link to many samples TODO: ??? - sample = models.ForeignKey("phenopackets.Biosample", on_delete=models.CASCADE, blank=True, null=True) def __str__(self): - return f"{self.dataset if not self.sample else self.sample} -> {self.table_id}" + return f"{self.dataset} -> {self.table_id}" class Table(models.Model): diff --git a/chord_metadata_service/chord/tests/constants.py b/chord_metadata_service/chord/tests/constants.py index 8aad20eab..30c9fd6d6 100644 --- a/chord_metadata_service/chord/tests/constants.py +++ b/chord_metadata_service/chord/tests/constants.py @@ -86,7 +86,6 @@ def valid_table_1(dataset_id, model_compatible=False): "table_id": table_id, "service_id": service_id, "service_artifact": "metadata", - "data_type": DATA_TYPE_PHENOPACKET, # TODO: Remove ("dataset_id" if model_compatible else "dataset"): dataset_id, }, { diff --git a/chord_metadata_service/chord/tests/test_ingest.py b/chord_metadata_service/chord/tests/test_ingest.py index aac745c83..072d6afb5 100644 --- a/chord_metadata_service/chord/tests/test_ingest.py +++ b/chord_metadata_service/chord/tests/test_ingest.py @@ -19,9 +19,8 @@ def setUp(self) -> None: self.d = Dataset.objects.create(title="Dataset 1", description="Some dataset", data_use=VALID_DATA_USE_1, project=p) # TODO: Real service ID - # TODO: Remove data_type here to = TableOwnership.objects.create(table_id=uuid.uuid4(), service_id=uuid.uuid4(), service_artifact="metadata", - data_type=DATA_TYPE_PHENOPACKET, dataset=self.d) + dataset=self.d) self.t = Table.objects.create(ownership_record=to, name="Table 1", data_type=DATA_TYPE_PHENOPACKET) def test_create_pf(self): diff --git a/chord_metadata_service/chord/tests/test_models.py b/chord_metadata_service/chord/tests/test_models.py index 5a6001ab9..59e3afde9 100644 --- a/chord_metadata_service/chord/tests/test_models.py +++ b/chord_metadata_service/chord/tests/test_models.py @@ -53,8 +53,6 @@ def setUp(self) -> None: table_id=TABLE_ID, service_id=SERVICE_ID, service_artifact="variant", - data_type="variant", - dataset=d ) @@ -63,7 +61,6 @@ def test_table_ownership(self): t = TableOwnership.objects.get(table_id=TABLE_ID, service_id=SERVICE_ID) self.assertEqual(t.service_artifact, "variant") - self.assertEqual(t.data_type, "variant") self.assertEqual(t.dataset, d) self.assertIn(t, d.table_ownership.all()) @@ -79,7 +76,6 @@ def setUp(self) -> None: table_id=TABLE_ID, service_id=SERVICE_ID, service_artifact="variant", - data_type="variant", dataset=self.d ) Table.objects.create(ownership_record=to, name="Table 1", data_type=DATA_TYPE_PHENOPACKET) From b3b7158b4e907e80eb007bbd95ccff082001bde9 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 1 Jun 2020 17:41:11 -0400 Subject: [PATCH 083/190] Fix invalid creation query for table ownership --- chord_metadata_service/chord/views_search.py | 1 - 1 file changed, 1 deletion(-) diff --git a/chord_metadata_service/chord/views_search.py b/chord_metadata_service/chord/views_search.py index 79ee1e778..2b1cb8365 100644 --- a/chord_metadata_service/chord/views_search.py +++ b/chord_metadata_service/chord/views_search.py @@ -100,7 +100,6 @@ def table_list(request): table_id=table_id, service_id=CHORD_SERVICE_ID, service_artifact=CHORD_SERVICE_ARTIFACT, - data_type=data_type, dataset=Dataset.objects.get(identifier=dataset), ) From a031dcc80f19308246f85a058a3b26c4a4ee2a6f Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 1 Jun 2020 17:42:23 -0400 Subject: [PATCH 084/190] Bump version to 0.7.0 --- chord_metadata_service/package.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chord_metadata_service/package.cfg b/chord_metadata_service/package.cfg index b60d66a92..0fce38394 100644 --- a/chord_metadata_service/package.cfg +++ b/chord_metadata_service/package.cfg @@ -1,4 +1,4 @@ [package] name = chord_metadata_service -version = 0.6.0 +version = 0.7.0 authors = Ksenia Zaytseva, David Lougheed, Simon Chénard From c5af0290fa699142f492b46523d99d975c81a4fb Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Tue, 2 Jun 2020 09:55:11 -0400 Subject: [PATCH 085/190] Handle validation errors better Use transaction for an ingestion routine to prevent invalid database states. --- chord_metadata_service/chord/views_ingest.py | 31 +++++++++++++------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/chord_metadata_service/chord/views_ingest.py b/chord_metadata_service/chord/views_ingest.py index 96fee1b16..44993c1f4 100644 --- a/chord_metadata_service/chord/views_ingest.py +++ b/chord_metadata_service/chord/views_ingest.py @@ -4,6 +4,8 @@ import os import uuid +from django.core.exceptions import ValidationError +from django.db import transaction from rest_framework.decorators import api_view, permission_classes, renderer_classes from rest_framework.permissions import AllowAny from rest_framework.renderers import BaseRenderer @@ -97,24 +99,33 @@ def ingest(request): # TODO: Better mechanism for workflow-specific ingestion handling - if dt == DATA_TYPE_EXPERIMENT: - for rs in json_data.get("resources", []): - dataset.additional_resources.add(ingest_resource(rs)) + with transaction.atomic(): + # Wrap ingestion in a transaction, so if it fails we don't end up in a partial state in the database. - for exp in json_data.get("experiments", []): - ingest_fn(exp, table_id) + if dt == DATA_TYPE_EXPERIMENT: + for rs in json_data.get("resources", []): + dataset.additional_resources.add(ingest_resource(rs)) - elif isinstance(json_data, list): - for obj in json_data: - ingest_fn(obj, table_id) + for exp in json_data.get("experiments", []): + ingest_fn(exp, table_id) - else: - ingest_fn(json_data, table_id) + elif isinstance(json_data, list): + for obj in json_data: + ingest_fn(obj, table_id) + + else: + ingest_fn(json_data, table_id) except json.decoder.JSONDecodeError as e: return Response(errors.bad_request_error(f"Invalid JSON provided for ingest document (message: {e})"), status=400) + except ValidationError as e: + return Response(errors.bad_request_error( + "Encountered validation errors during ingestion", + *(e.error_list if hasattr(e, "error_list") else e.error_dict.items()), + )) + # TODO: Schema validation # TODO: Rollback in case of failures return Response(status=204) From 2053b35dd50ad27c188ff16fb88c37979593c3f6 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Tue, 2 Jun 2020 10:31:32 -0400 Subject: [PATCH 086/190] Fix invalid prop access in computed resources --- chord_metadata_service/chord/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chord_metadata_service/chord/models.py b/chord_metadata_service/chord/models.py index ad0af3161..392838b78 100644 --- a/chord_metadata_service/chord/models.py +++ b/chord_metadata_service/chord/models.py @@ -71,7 +71,7 @@ def resources(self): *( r.id for p in Phenopacket.objects.filter( - table_id__in={t.id for t in self.table_ownership.all()} + table_id__in={t.table_id for t in self.table_ownership.all()} ).prefetch_related("meta_data", "meta_data__resources") for r in p.meta_data.resources.all() ), From d134c8cd38ed8ea44c4f15ef4abcf721f1beb0b8 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Tue, 2 Jun 2020 15:54:34 -0400 Subject: [PATCH 087/190] add fhir system to ontology class id in fhir conversion add biosamples to phenopacket in fhir ingest --- chord_metadata_service/restapi/fhir_utils.py | 17 ++++++++++++----- .../restapi/tests/test_fhir_ingest.py | 1 - .../restapi/views_ingest_fhir.py | 3 +++ 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/chord_metadata_service/restapi/fhir_utils.py b/chord_metadata_service/restapi/fhir_utils.py index 2989a083d..4623d9958 100644 --- a/chord_metadata_service/restapi/fhir_utils.py +++ b/chord_metadata_service/restapi/fhir_utils.py @@ -425,7 +425,7 @@ def observation_to_phenotypic_feature(obj): # TODO change "description": observation.id, "pftype": { - "id": codeable_concept.coding[0].code, + "id": f"{codeable_concept.coding[0].system}:{codeable_concept.coding[0].code}", "label": codeable_concept.coding[0].display # TODO collect system info in metadata } @@ -444,7 +444,7 @@ def condition_to_disease(obj): # id is an integer AutoField, legacy id can be a string # "id": condition.id, "term": { - "id": codeable_concept.coding[0].code, + "id": f"{codeable_concept.coding[0].system}:{codeable_concept.coding[0].code}", "label": codeable_concept.coding[0].display # TODO collect system info in metadata } @@ -482,7 +482,7 @@ def specimen_to_biosample(obj): if specimen.type: codeable_concept = specimen.type # CodeableConcept biosample["sampled_tissue"] = { - "id": codeable_concept.coding[0].code, + "id": f"{codeable_concept.coding[0].system}:{codeable_concept.coding[0].code}", "label": codeable_concept.coding[0].display # TODO collect system info in metadata } @@ -491,12 +491,19 @@ def specimen_to_biosample(obj): bodysite_codeable_concept = specimen.collection.bodySite biosample["procedure"] = { "code": { - "id": method_codeable_concept.coding[0].code, + "id": f"{method_codeable_concept.coding[0].system}:{method_codeable_concept.coding[0].code}", "label": method_codeable_concept.coding[0].display }, "body_site": { - "id": bodysite_codeable_concept.coding[0].code, + "id": f"{bodysite_codeable_concept.coding[0].system}:{bodysite_codeable_concept.coding[0].code}", "label": bodysite_codeable_concept.coding[0].display } } + else: + biosample["procedure"] = { + "code": { + "id": "SNOMED:42630001", + "label": "Procedure code not assigned", + } + } return biosample diff --git a/chord_metadata_service/restapi/tests/test_fhir_ingest.py b/chord_metadata_service/restapi/tests/test_fhir_ingest.py index e4d57a79a..274636ffc 100644 --- a/chord_metadata_service/restapi/tests/test_fhir_ingest.py +++ b/chord_metadata_service/restapi/tests/test_fhir_ingest.py @@ -32,7 +32,6 @@ def test_dataset_id(self): invalid_dataset_id_ingest["metadata"] = { "created_by": "Name" } - print(invalid_dataset_id_ingest) request = factory.post('/private/ingest-fhir', invalid_dataset_id_ingest, format='json') response = ingest_fhir(request) self.assertEqual(response.status_code, 400) diff --git a/chord_metadata_service/restapi/views_ingest_fhir.py b/chord_metadata_service/restapi/views_ingest_fhir.py index cbc52c5aa..fe07b216c 100644 --- a/chord_metadata_service/restapi/views_ingest_fhir.py +++ b/chord_metadata_service/restapi/views_ingest_fhir.py @@ -154,6 +154,9 @@ def ingest_fhir(request): individual=Individual.objects.get(id=individual_id), sampled_tissue=biosample_data["sampled_tissue"] ) + phenopacket = Phenopacket.objects.get(subject=Individual.objects.get(id=individual_id)) + phenopacket.biosamples.add(biosample) + except json.decoder.JSONDecodeError as e: return Response(bad_request_error(f"Invalid JSON provided (message: {e})"), status=400) From f7a19160d729c83f1529f57b2394b707783afc33 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Wed, 3 Jun 2020 09:40:35 -0400 Subject: [PATCH 088/190] Bump redis requirements to 3.5.3 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7b6bcc4d6..df6ddc1b3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -40,7 +40,7 @@ pytz==2020.1 PyYAML==5.3.1 rdflib==4.2.2 rdflib-jsonld==0.4.0 -redis==3.5.2 +redis==3.5.3 requests==2.23.0 rfc3987==1.3.8 simplejson==3.17.0 From 0a91418656f7c80a51e341c909a00d9508ec08c3 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Wed, 3 Jun 2020 09:58:09 -0400 Subject: [PATCH 089/190] Update django to 2.2.13 --- requirements.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index df6ddc1b3..1f8659b56 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ colorama==0.4.3 coreapi==2.3.3 coreschema==0.0.4 coverage==5.1 -Django==2.2.12 +Django==2.2.13 django-filter==2.2.0 django-nose==1.4.6 django-rest-swagger==2.2.0 diff --git a/setup.py b/setup.py index d52c7765a..e3f363dee 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ python_requires=">=3.6", install_requires=[ "chord_lib[django]==0.9.0", - "Django>=2.2.12,<3.0", + "Django>=2.2.13,<3.0", "django-filter>=2.2,<3.0", "django-nose>=1.4,<2.0", "djangorestframework>=3.11,<3.12", From 1e106f86ef8f4a11209d1644959cd3c21f4ea81c Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Wed, 3 Jun 2020 13:57:08 -0400 Subject: [PATCH 090/190] Derive host from CHORD_URL instead of using env var --- chord_metadata_service/metadata/settings.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/chord_metadata_service/metadata/settings.py b/chord_metadata_service/metadata/settings.py index f07ca1aff..bd2997478 100644 --- a/chord_metadata_service/metadata/settings.py +++ b/chord_metadata_service/metadata/settings.py @@ -14,6 +14,8 @@ import sys import logging +from urllib.parse import urlparse + from .. import __version__ # Build paths inside the project like this: os.path.join(BASE_DIR, ...) @@ -29,12 +31,6 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = os.environ.get("CHORD_DEBUG", "true").lower() == "true" -ALLOWED_HOSTS = [os.environ.get("CHORD_HOST", "localhost")] -if DEBUG: - ALLOWED_HOSTS = list(set(ALLOWED_HOSTS + ["localhost", "127.0.0.1", "[::1]"])) - -APPEND_SLASH = False - # CHORD-specific settings @@ -52,6 +48,16 @@ AUTH_OVERRIDE = not CHORD_PERMISSIONS +# Allowed hosts - TODO: Derive from CHORD_URL + +CHORD_HOST = urlparse(CHORD_URL or "").netloc +ALLOWED_HOSTS = [CHORD_HOST or "localhost"] +if DEBUG: + ALLOWED_HOSTS = list(set(ALLOWED_HOSTS + ["localhost", "127.0.0.1", "[::1]"])) + +APPEND_SLASH = False + + # Application definition INSTALLED_APPS = [ From 3b07747ad8d395f393020b80217af4d2c6605ccd Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Thu, 4 Jun 2020 13:14:57 -0400 Subject: [PATCH 091/190] remove empty functions --- chord_metadata_service/restapi/fhir_utils.py | 33 ++++++-------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/chord_metadata_service/restapi/fhir_utils.py b/chord_metadata_service/restapi/fhir_utils.py index 4623d9958..0a32256b2 100644 --- a/chord_metadata_service/restapi/fhir_utils.py +++ b/chord_metadata_service/restapi/fhir_utils.py @@ -389,6 +389,15 @@ def fhir_composition(obj): ##################### FHIR to Phenopackets class conversion functions ##################### # There is no guide to map FHIR to Phenopackets +# SNOMED term to use as placeholder when collection method is not present in Specimen +procedure_not_assigned = { + "code": { + "id": "SNOMED:42630001", + "label": "Procedure code not assigned", + } + } + + def patient_to_individual(obj): """ FHIR Patient to Individual. """ @@ -453,23 +462,6 @@ def condition_to_disease(obj): return disease -def diagnostic_report_to_interpretation(obj): - """ FHIR DiagnosticReport to Phenopackets Interpretation. """ - # it hardly maps at all - return - - -def procedure_to_procedure(obj): - """ FHIR Procedure to Phenopackets Procedure. - The main semantic difference: - - phenopackets procedure is a procedure performed to extract a biosample; - - fhir procedure is a procedure performed on or for a patient - (e.g. documentation of patient's medication) - """ - - return - - def specimen_to_biosample(obj): """ FHIR Specimen to Phenopackets Biosample. """ @@ -500,10 +492,5 @@ def specimen_to_biosample(obj): } } else: - biosample["procedure"] = { - "code": { - "id": "SNOMED:42630001", - "label": "Procedure code not assigned", - } - } + biosample["procedure"] = procedure_not_assigned return biosample From 1ef4cf129c3b7d10e74568529bfdc64c40851a93 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Thu, 4 Jun 2020 23:12:32 -0400 Subject: [PATCH 092/190] change Dataset to Table in phenopacket fhir-ingest adjust tests --- chord_metadata_service/restapi/fhir_utils.py | 2 +- chord_metadata_service/restapi/schemas.py | 4 ++-- chord_metadata_service/restapi/tests/constants.py | 2 +- .../restapi/tests/test_fhir_ingest.py | 7 ++++++- chord_metadata_service/restapi/views_ingest_fhir.py | 11 ++++++----- 5 files changed, 16 insertions(+), 10 deletions(-) diff --git a/chord_metadata_service/restapi/fhir_utils.py b/chord_metadata_service/restapi/fhir_utils.py index 0a32256b2..cb64ca871 100644 --- a/chord_metadata_service/restapi/fhir_utils.py +++ b/chord_metadata_service/restapi/fhir_utils.py @@ -414,7 +414,7 @@ def patient_to_individual(obj): "unknown": "UNKNOWN_SEX" } if patient.gender: - individual["sex"] = gender_to_sex.get(patient.gender, "unknown") + individual["sex"] = gender_to_sex[patient.gender] if patient.birthDate: individual["date_of_birth"] = patient.birthDate.isostring if patient.active: diff --git a/chord_metadata_service/restapi/schemas.py b/chord_metadata_service/restapi/schemas.py index 3bb8bf86b..a56e10cf0 100644 --- a/chord_metadata_service/restapi/schemas.py +++ b/chord_metadata_service/restapi/schemas.py @@ -126,7 +126,7 @@ "description": "FHIR Ingest schema", "type": "object", "properties": { - "dataset_id": {"type": "string"}, + "table_id": {"type": "string"}, "patients": {"type": "string", "description": "Path to a patients file location."}, "observations": {"type": "string", "description": "Path to an observations file location."}, "conditions": {"type": "string", "description": "Path to a conditions file location."}, @@ -140,7 +140,7 @@ } }, "required": [ - "dataset_id", + "table_id", "patients", "metadata" ], diff --git a/chord_metadata_service/restapi/tests/constants.py b/chord_metadata_service/restapi/tests/constants.py index 418fd41fe..386458f33 100644 --- a/chord_metadata_service/restapi/tests/constants.py +++ b/chord_metadata_service/restapi/tests/constants.py @@ -1,5 +1,5 @@ INVALID_INGEST_BODY = { - "dataset_id": "62b5fc67-d925-4409-bb59-e1e9a1ef10af", + "table_id": "62b5fc67-d925-4409-bb59-e1e9a1ef10af", "patients": "patients_file", "metadata": { "test": "required created_by is not present" diff --git a/chord_metadata_service/restapi/tests/test_fhir_ingest.py b/chord_metadata_service/restapi/tests/test_fhir_ingest.py index 274636ffc..04bee3b75 100644 --- a/chord_metadata_service/restapi/tests/test_fhir_ingest.py +++ b/chord_metadata_service/restapi/tests/test_fhir_ingest.py @@ -1,12 +1,14 @@ from rest_framework.test import APITestCase from rest_framework.test import APIRequestFactory -from chord_metadata_service.chord.models import Project, Dataset +from chord_metadata_service.chord.models import * from chord_metadata_service.phenopackets.models import * from chord_metadata_service.patients.models import Individual from .constants import INVALID_INGEST_BODY, INVALID_FHIR_BUNDLE_1 from ..views_ingest_fhir import ingest_fhir from chord_metadata_service.chord.tests.constants import VALID_DATA_USE_1 +from chord_metadata_service.chord.data_types import DATA_TYPE_PHENOPACKET import copy +import uuid class TestFhirIngest(APITestCase): @@ -15,6 +17,9 @@ def setUp(self) -> None: p = Project.objects.create(title="Project 1", description="Test") self.d = Dataset.objects.create(title="Dataset 1", description="Test dataset", data_use=VALID_DATA_USE_1, project=p) + to = TableOwnership.objects.create(table_id=uuid.uuid4(), service_id=uuid.uuid4(), service_artifact="metadata", + dataset=self.d) + self.t = Table.objects.create(ownership_record=to, name="Table 1", data_type=DATA_TYPE_PHENOPACKET) def test_ingest_body(self): factory = APIRequestFactory() diff --git a/chord_metadata_service/restapi/views_ingest_fhir.py b/chord_metadata_service/restapi/views_ingest_fhir.py index fe07b216c..1c4be7264 100644 --- a/chord_metadata_service/restapi/views_ingest_fhir.py +++ b/chord_metadata_service/restapi/views_ingest_fhir.py @@ -61,10 +61,11 @@ def ingest_fhir(request): # check schema of ingest body _check_schema(FHIR_INGEST_SCHEMA, request.data, 'ingest body') - # check if dataset exists - if not Dataset.objects.filter(identifier=request.data["dataset_id"]).exists(): - return Response(bad_request_error(f"Dataset with ID {request.data['dataset_id']} does not exist"), - status=400) + # check if table exists + table_id = request.data["table_id"] + + if not Table.objects.filter(ownership_record_id=table_id).exists(): + return Response(bad_request_error(f"Table with ID {table_id} does not exist"), status=400) # patients-individuals with open(request.data["patients"], "r") as p_file: @@ -86,7 +87,7 @@ def ingest_fhir(request): id=str(uuid.uuid4()), subject=individual, meta_data=meta_data_obj, - dataset=Dataset.objects.get(identifier=request.data["dataset_id"]) + table=Table.objects.get(ownership_record_id=table_id) ) print(f'Phenopacket {phenopacket.id} created') except json.decoder.JSONDecodeError as e: From 648713b4305e9260c5eb7b73294853c5a1307dcc Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Fri, 5 Jun 2020 09:59:46 -0400 Subject: [PATCH 093/190] Update codecov --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1f8659b56..9678d5377 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ Babel==2.8.0 certifi==2020.4.5.1 chardet==3.0.4 chord-lib==0.9.0 -codecov==2.1.3 +codecov==2.1.4 colorama==0.4.3 coreapi==2.3.3 coreschema==0.0.4 From 3b3e166b03a94f9fb9620b87a61e780ae90e4b6e Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Fri, 5 Jun 2020 10:01:59 -0400 Subject: [PATCH 094/190] Auto-generate experiment ID if needed --- chord_metadata_service/chord/ingest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chord_metadata_service/chord/ingest.py b/chord_metadata_service/chord/ingest.py index b1a6f0a95..bea6c69e4 100644 --- a/chord_metadata_service/chord/ingest.py +++ b/chord_metadata_service/chord/ingest.py @@ -111,7 +111,7 @@ def ingest_resource(resource: dict) -> rm.Resource: def ingest_experiment(experiment_data, table_id) -> em.Experiment: """Ingests a single experiment.""" - new_experiment_id = experiment_data["id"] # TODO: Is this provided? + new_experiment_id = experiment_data.get("id", str(uuid.uuid4())) reference_registry_id = experiment_data.get("reference_registry_id") qc_flags = experiment_data.get("qc_flags", []) From 7c3b95a1a4694e0b3c78240f31e594aab31ce891 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Fri, 5 Jun 2020 18:04:23 -0400 Subject: [PATCH 095/190] add fhir to workflow --- chord_metadata_service/chord/data_types.py | 1 + chord_metadata_service/chord/ingest.py | 25 ++++++++++++++++++- .../chord/workflows/fhir_json.wdl | 17 +++++++++++++ .../restapi/views_ingest_fhir.py | 2 +- 4 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 chord_metadata_service/chord/workflows/fhir_json.wdl diff --git a/chord_metadata_service/chord/data_types.py b/chord_metadata_service/chord/data_types.py index 3c639f0a1..af97ed733 100644 --- a/chord_metadata_service/chord/data_types.py +++ b/chord_metadata_service/chord/data_types.py @@ -9,6 +9,7 @@ DATA_TYPE_EXPERIMENT = "experiment" DATA_TYPE_PHENOPACKET = "phenopacket" +DATA_TYPE_FHIR = "fhir" DATA_TYPES = { DATA_TYPE_EXPERIMENT: { diff --git a/chord_metadata_service/chord/ingest.py b/chord_metadata_service/chord/ingest.py index b1a6f0a95..151342615 100644 --- a/chord_metadata_service/chord/ingest.py +++ b/chord_metadata_service/chord/ingest.py @@ -4,11 +4,12 @@ from dateutil.parser import isoparse from typing import Callable -from chord_metadata_service.chord.data_types import DATA_TYPE_EXPERIMENT, DATA_TYPE_PHENOPACKET +from chord_metadata_service.chord.data_types import DATA_TYPE_EXPERIMENT, DATA_TYPE_PHENOPACKET, DATA_TYPE_FHIR from chord_metadata_service.chord.models import Table from chord_metadata_service.experiments import models as em from chord_metadata_service.phenopackets import models as pm from chord_metadata_service.resources import models as rm, utils as ru +from chord_metadata_service.restapi.views_ingest_fhir import ingest_fhir __all__ = [ @@ -62,6 +63,27 @@ "value": "{json_document}" } ] + }, + "fhir_json": { + "name": "FHIR resources json", + "description": "This ingestion workflow will validate and import a FHIR schema-compatible " + "JSON document.", + "data_type": "experiment", + "file": "experiments_json.wdl", + "inputs": [ + { + "id": "json_document", + "type": "file", + "extensions": [".json"] + } + ], + "outputs": [ + { + "id": "json_document", + "type": "file", + "value": "{json_document}" + } + ] } }, "analysis": {} @@ -247,4 +269,5 @@ def ingest_phenopacket(phenopacket_data, table_id) -> pm.Phenopacket: DATA_TYPE_INGEST_FUNCTION_MAP = { DATA_TYPE_EXPERIMENT: ingest_experiment, DATA_TYPE_PHENOPACKET: ingest_phenopacket, + DATA_TYPE_FHIR: ingest_fhir } diff --git a/chord_metadata_service/chord/workflows/fhir_json.wdl b/chord_metadata_service/chord/workflows/fhir_json.wdl new file mode 100644 index 000000000..43683b3a7 --- /dev/null +++ b/chord_metadata_service/chord/workflows/fhir_json.wdl @@ -0,0 +1,17 @@ +workflow fhir_json { + File json_document + + call identity_task { + input: json_document_in = json_document + } +} + +task identity_task { + File json_document_in + command { + true + } + output { + File json_document = "${json_document_in}" + } +} \ No newline at end of file diff --git a/chord_metadata_service/restapi/views_ingest_fhir.py b/chord_metadata_service/restapi/views_ingest_fhir.py index 1c4be7264..88c2772a9 100644 --- a/chord_metadata_service/restapi/views_ingest_fhir.py +++ b/chord_metadata_service/restapi/views_ingest_fhir.py @@ -20,7 +20,7 @@ INGEST_BODY_EXAMPLE = { - "dataset_id": "62b5fc67-d925-4409-bb59-e1e9a1ef10ae", + "table_id": "62b5fc67-d925-4409-bb59-e1e9a1ef10ae", "patients": "examples/patients.json", "observations": "examples/observations.json", "conditions": "examples/conditions.json", From 79bc53f252fcba301f44f3149fa1b887c6b36e22 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Mon, 8 Jun 2020 12:31:00 -0400 Subject: [PATCH 096/190] change data type to 'phenopacket' --- chord_metadata_service/chord/ingest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chord_metadata_service/chord/ingest.py b/chord_metadata_service/chord/ingest.py index 151342615..958665ac6 100644 --- a/chord_metadata_service/chord/ingest.py +++ b/chord_metadata_service/chord/ingest.py @@ -68,8 +68,8 @@ "name": "FHIR resources json", "description": "This ingestion workflow will validate and import a FHIR schema-compatible " "JSON document.", - "data_type": "experiment", - "file": "experiments_json.wdl", + "data_type": "phenopacket", + "file": "fhir_json.wdl", "inputs": [ { "id": "json_document", From b0e613521eb0434bf110688ba4fac71451147954 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 8 Jun 2020 14:36:49 -0400 Subject: [PATCH 097/190] Tweak chord ingest handling to prep for fhir --- chord_metadata_service/chord/data_types.py | 1 - chord_metadata_service/chord/ingest.py | 69 +++++++++++++++----- chord_metadata_service/chord/views_ingest.py | 65 ++++++------------ requirements.txt | 2 +- 4 files changed, 75 insertions(+), 62 deletions(-) diff --git a/chord_metadata_service/chord/data_types.py b/chord_metadata_service/chord/data_types.py index af97ed733..3c639f0a1 100644 --- a/chord_metadata_service/chord/data_types.py +++ b/chord_metadata_service/chord/data_types.py @@ -9,7 +9,6 @@ DATA_TYPE_EXPERIMENT = "experiment" DATA_TYPE_PHENOPACKET = "phenopacket" -DATA_TYPE_FHIR = "fhir" DATA_TYPES = { DATA_TYPE_EXPERIMENT: { diff --git a/chord_metadata_service/chord/ingest.py b/chord_metadata_service/chord/ingest.py index 958665ac6..151a45f81 100644 --- a/chord_metadata_service/chord/ingest.py +++ b/chord_metadata_service/chord/ingest.py @@ -1,11 +1,12 @@ +import json import os import uuid from dateutil.parser import isoparse from typing import Callable -from chord_metadata_service.chord.data_types import DATA_TYPE_EXPERIMENT, DATA_TYPE_PHENOPACKET, DATA_TYPE_FHIR -from chord_metadata_service.chord.models import Table +from chord_metadata_service.chord.data_types import DATA_TYPE_EXPERIMENT, DATA_TYPE_PHENOPACKET +from chord_metadata_service.chord.models import Table, TableOwnership from chord_metadata_service.experiments import models as em from chord_metadata_service.phenopackets import models as pm from chord_metadata_service.resources import models as rm, utils as ru @@ -16,17 +17,22 @@ "METADATA_WORKFLOWS", "WORKFLOWS_PATH", "ingest_resource", - "DATA_TYPE_INGEST_FUNCTION_MAP", + "WORKFLOW_INGEST_FUNCTION_MAP", ] +WORKFLOW_PHENOPACKETS_JSON = "phenopackets_json" +WORKFLOW_EXPERIMENTS_JSON = "experiments_json" +WORKFLOW_FHIR_JSON = "fhir_json" + + METADATA_WORKFLOWS = { "ingestion": { - "phenopackets_json": { + WORKFLOW_PHENOPACKETS_JSON: { "name": "Bento Phenopackets-Compatible JSON", "description": "This ingestion workflow will validate and import a Phenopackets schema-compatible " "JSON document.", - "data_type": "phenopacket", + "data_type": DATA_TYPE_PHENOPACKET, "file": "phenopackets_json.wdl", "inputs": [ { @@ -43,11 +49,11 @@ } ] }, - "experiments_json": { + WORKFLOW_EXPERIMENTS_JSON: { "name": "Bento Experiments JSON", "description": "This ingestion workflow will validate and import a Bento Experiments schema-compatible " "JSON document.", - "data_type": "experiment", + "data_type": DATA_TYPE_EXPERIMENT, "file": "experiments_json.wdl", "inputs": [ { @@ -64,11 +70,12 @@ } ] }, - "fhir_json": { - "name": "FHIR resources json", + WORKFLOW_FHIR_JSON: { + "name": "FHIR Resources JSON", "description": "This ingestion workflow will validate and import a FHIR schema-compatible " - "JSON document.", - "data_type": "phenopacket", + "JSON document, and convert it to the Bento metadata service's internal Phenopackets-based " + "data model.", + "data_type": DATA_TYPE_PHENOPACKET, "file": "fhir_json.wdl", "inputs": [ { @@ -266,8 +273,40 @@ def ingest_phenopacket(phenopacket_data, table_id) -> pm.Phenopacket: return new_phenopacket -DATA_TYPE_INGEST_FUNCTION_MAP = { - DATA_TYPE_EXPERIMENT: ingest_experiment, - DATA_TYPE_PHENOPACKET: ingest_phenopacket, - DATA_TYPE_FHIR: ingest_fhir +def _map_if_list(fn, data, *args): + if isinstance(data, list): + return [fn(d, *args) for d in data] + + return fn(data, *args) + + +def ingest_experiments_workflow(workflow_outputs, table_id): + with open(workflow_outputs["json_document"], "r") as jf: + json_data = json.load(jf) + + dataset = TableOwnership.objects.get(table_id=table_id).dataset + + for rs in json_data.get("resources", []): + dataset.additional_resources.add(ingest_resource(rs)) + + for exp in json_data.get("experiments", []): + ingest_experiment(exp, table_id) + + +def ingest_phenopacket_workflow(workflow_outputs, table_id): + with open(workflow_outputs["json_document"], "r") as jf: + json_data = json.load(jf) + _map_if_list(ingest_phenopacket, json_data, table_id) + + +def ingest_fhir_workflow(workflow_outputs, table_id): + with open(workflow_outputs["json_document"], "r") as jf: + json_data = json.load(jf) + _map_if_list(ingest_fhir, json_data, table_id) + + +WORKFLOW_INGEST_FUNCTION_MAP = { + WORKFLOW_EXPERIMENTS_JSON: ingest_experiments_workflow, + WORKFLOW_PHENOPACKETS_JSON: ingest_phenopacket_workflow, + WORKFLOW_FHIR_JSON: ingest_fhir_workflow, } diff --git a/chord_metadata_service/chord/views_ingest.py b/chord_metadata_service/chord/views_ingest.py index 44993c1f4..ce6880b88 100644 --- a/chord_metadata_service/chord/views_ingest.py +++ b/chord_metadata_service/chord/views_ingest.py @@ -15,9 +15,8 @@ from chord_lib.responses import errors from chord_lib.workflows import get_workflow, get_workflow_resource, workflow_exists -from .data_types import DATA_TYPE_EXPERIMENT -from .ingest import METADATA_WORKFLOWS, WORKFLOWS_PATH, DATA_TYPE_INGEST_FUNCTION_MAP, ingest_resource -from .models import Table, TableOwnership +from .ingest import METADATA_WORKFLOWS, WORKFLOWS_PATH, WORKFLOW_INGEST_FUNCTION_MAP +from .models import Table class WDLRenderer(BaseRenderer): @@ -78,54 +77,30 @@ def ingest(request): table_id = str(uuid.UUID(table_id)) # Normalize dataset ID to UUID's str format. - dataset = TableOwnership.objects.get(table_id=table_id).dataset - workflow_id = request.data["workflow_id"].strip() workflow_outputs = request.data["workflow_outputs"] if not workflow_exists(workflow_id, METADATA_WORKFLOWS): # Check that the workflow exists return Response(errors.bad_request_error(f"Workflow with ID {workflow_id} does not exist"), status=400) - workflow = get_workflow(workflow_id, METADATA_WORKFLOWS) - if "json_document" not in workflow_outputs: return Response(errors.bad_request_error("Missing workflow output 'json_document'"), status=400) - with open(workflow_outputs["json_document"], "r") as jf: - try: - dt = workflow["data_type"] - json_data = json.load(jf) - ingest_fn = DATA_TYPE_INGEST_FUNCTION_MAP[dt] - - # TODO: Better mechanism for workflow-specific ingestion handling - - with transaction.atomic(): - # Wrap ingestion in a transaction, so if it fails we don't end up in a partial state in the database. - - if dt == DATA_TYPE_EXPERIMENT: - for rs in json_data.get("resources", []): - dataset.additional_resources.add(ingest_resource(rs)) - - for exp in json_data.get("experiments", []): - ingest_fn(exp, table_id) - - elif isinstance(json_data, list): - for obj in json_data: - ingest_fn(obj, table_id) - - else: - ingest_fn(json_data, table_id) - - except json.decoder.JSONDecodeError as e: - return Response(errors.bad_request_error(f"Invalid JSON provided for ingest document (message: {e})"), - status=400) - - except ValidationError as e: - return Response(errors.bad_request_error( - "Encountered validation errors during ingestion", - *(e.error_list if hasattr(e, "error_list") else e.error_dict.items()), - )) - - # TODO: Schema validation - # TODO: Rollback in case of failures - return Response(status=204) + try: + with transaction.atomic(): + # Wrap ingestion in a transaction, so if it fails we don't end up in a partial state in the database. + WORKFLOW_INGEST_FUNCTION_MAP[workflow_id](workflow_outputs, table_id) + + except json.decoder.JSONDecodeError as e: + return Response(errors.bad_request_error(f"Invalid JSON provided for ingest document (message: {e})"), + status=400) + + except ValidationError as e: + return Response(errors.bad_request_error( + "Encountered validation errors during ingestion", + *(e.error_list if hasattr(e, "error_list") else e.error_dict.items()), + )) + + # TODO: Schema validation + # TODO: Rollback in case of failures + return Response(status=204) diff --git a/requirements.txt b/requirements.txt index 1f8659b56..9678d5377 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ Babel==2.8.0 certifi==2020.4.5.1 chardet==3.0.4 chord-lib==0.9.0 -codecov==2.1.3 +codecov==2.1.4 colorama==0.4.3 coreapi==2.3.3 coreschema==0.0.4 From 5f143067e3e45645bb74df1c871cbe118320270b Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Mon, 8 Jun 2020 16:52:31 -0400 Subject: [PATCH 098/190] import WORKFLOW_INGEST_FUNCTION_MAP --- chord_metadata_service/chord/tests/test_ingest.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/chord_metadata_service/chord/tests/test_ingest.py b/chord_metadata_service/chord/tests/test_ingest.py index 072d6afb5..1759e1f36 100644 --- a/chord_metadata_service/chord/tests/test_ingest.py +++ b/chord_metadata_service/chord/tests/test_ingest.py @@ -6,7 +6,7 @@ from chord_metadata_service.chord.data_types import DATA_TYPE_PHENOPACKET from chord_metadata_service.chord.models import Project, Dataset, TableOwnership, Table # noinspection PyProtectedMember -from chord_metadata_service.chord.ingest import create_phenotypic_feature, DATA_TYPE_INGEST_FUNCTION_MAP +from chord_metadata_service.chord.ingest import create_phenotypic_feature, WORKFLOW_INGEST_FUNCTION_MAP from chord_metadata_service.phenopackets.models import PhenotypicFeature, Phenopacket from .constants import VALID_DATA_USE_1 @@ -40,7 +40,7 @@ def test_create_pf(self): self.assertEqual(p1.pk, p2.pk) def test_ingesting_phenopackets_json(self): - p = DATA_TYPE_INGEST_FUNCTION_MAP[DATA_TYPE_PHENOPACKET](EXAMPLE_INGEST, self.t.identifier) + p = WORKFLOW_INGEST_FUNCTION_MAP[DATA_TYPE_PHENOPACKET](EXAMPLE_INGEST, self.t.identifier) self.assertEqual(p.id, Phenopacket.objects.get(id=p.id).id) self.assertEqual(p.subject.id, EXAMPLE_INGEST["subject"]["id"]) @@ -68,6 +68,6 @@ def test_ingesting_phenopackets_json(self): # TODO: More # Test ingesting again - p2 = DATA_TYPE_INGEST_FUNCTION_MAP[DATA_TYPE_PHENOPACKET](EXAMPLE_INGEST, self.t.identifier) + p2 = WORKFLOW_INGEST_FUNCTION_MAP[DATA_TYPE_PHENOPACKET](EXAMPLE_INGEST, self.t.identifier) self.assertNotEqual(p.id, p2.id) # TODO: More From aa8fbc6e47027804701cacab97887ab50738d26e Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 8 Jun 2020 17:28:39 -0400 Subject: [PATCH 099/190] Fix misc issues with ingestion tests --- chord_metadata_service/chord/ingest.py | 7 +- .../chord/tests/example_ingest.py | 243 +----------------- .../chord/tests/example_phenopacket.json | 233 +++++++++++++++++ .../chord/tests/test_ingest.py | 28 +- 4 files changed, 262 insertions(+), 249 deletions(-) create mode 100644 chord_metadata_service/chord/tests/example_phenopacket.json diff --git a/chord_metadata_service/chord/ingest.py b/chord_metadata_service/chord/ingest.py index 151a45f81..646e0c3e7 100644 --- a/chord_metadata_service/chord/ingest.py +++ b/chord_metadata_service/chord/ingest.py @@ -289,20 +289,19 @@ def ingest_experiments_workflow(workflow_outputs, table_id): for rs in json_data.get("resources", []): dataset.additional_resources.add(ingest_resource(rs)) - for exp in json_data.get("experiments", []): - ingest_experiment(exp, table_id) + return [ingest_experiment(exp, table_id) for exp in json_data.get("experiments", [])] def ingest_phenopacket_workflow(workflow_outputs, table_id): with open(workflow_outputs["json_document"], "r") as jf: json_data = json.load(jf) - _map_if_list(ingest_phenopacket, json_data, table_id) + return _map_if_list(ingest_phenopacket, json_data, table_id) def ingest_fhir_workflow(workflow_outputs, table_id): with open(workflow_outputs["json_document"], "r") as jf: json_data = json.load(jf) - _map_if_list(ingest_fhir, json_data, table_id) + return _map_if_list(ingest_fhir, json_data, table_id) WORKFLOW_INGEST_FUNCTION_MAP = { diff --git a/chord_metadata_service/chord/tests/example_ingest.py b/chord_metadata_service/chord/tests/example_ingest.py index 6cbf523d4..b9170c1ce 100644 --- a/chord_metadata_service/chord/tests/example_ingest.py +++ b/chord_metadata_service/chord/tests/example_ingest.py @@ -1,234 +1,11 @@ -EXAMPLE_INGEST = { - "subject": { - "id": "patient1", - "date_of_birth": "1964-03-15T00:00:00Z", - "sex": "MALE", - "karyotypic_sex": "UNKNOWN_KARYOTYPE" - }, - "phenotypic_features": [ - { - "description": "", - "type": { - "id": "HP:0000790", - "label": "Hematuria" - }, - "negated": False, - "modifier": [], - "evidence": [] - }, - { - "description": "", - "type": { - "id": "HP:0100518", - "label": "Dysuria" - }, - "negated": False, - "severity": { - "id": "HP:0012828", - "label": "Severe" - }, - "modifier": [], - "evidence": [] - } - ], - "diseases": [ - { - "term": { - "id": "NCIT:C39853", - "label": "Infiltrating Urothelial Carcinoma" - }, - "disease_stage": [ - { - "id": "NCIT:C48766", - "label": "pT2b Stage Finding" - }, - { - "id": "NCIT:C48750", - "label": "pN2 Stage Finding" - } - ] - } - ], - "meta_data": { - "created": "2019-04-03T15:31:40.765Z", - "created_by": "Peter R", - "submitted_by": "Peter R", - "resources": [ - { - "id": "HP:2019-04-08", - "name": "human phenotype ontology", - "namespace_prefix": "HP", - "url": "http://purl.obolibrary.org/obo/hp.owl", - "version": "2019-04-08", - "iri_prefix": "http://purl.obolibrary.org/obo/HP_" - }, - { - "id": "UBERON:2019-03-08", - "name": "uber anatomy ontology", - "namespace_prefix": "UBERON", - "url": "http://purl.obolibrary.org/obo/uberon.owl", - "version": "2019-03-08", - "iri_prefix": "http://purl.obolibrary.org/obo/UBERON_" - }, - { - "id": "NCIT:18.05d", - "name": "NCI Thesaurus OBO Edition", - "namespace_prefix": "NCIT", - "url": "http://purl.obolibrary.org/obo/ncit.owl", - "version": "18.05d", - "iri_prefix": "http://purl.obolibrary.org/obo/NCIT_" - } - ], - "updated": [], - "external_references": [ - { - "id": "PMID:29221636", - "description": "Urothelial neoplasms in pediatric and young adult patients: A large single-center " - "series" - } - ] - }, - "biosamples": [ - { - "id": "sample1", - "individual_id": "patient1", - "description": "", - "sampled_tissue": { - "id": "UBERON_0001256", - "label": "wall of urinary bladder" - }, - "phenotypic_features": [], - "individual_age_at_collection": { - "age": "P52Y2M" - }, - "histological_diagnosis": { - "id": "NCIT:C39853", - "label": "Infiltrating Urothelial Carcinoma" - }, - "tumor_progression": { - "id": "NCIT:C84509", - "label": "Primary Malignant Neoplasm" - }, - "diagnostic_markers": [], - "procedure": { - "code": { - "id": "NCIT:C5189", - "label": "Radical Cystoprostatectomy" - } - }, - "is_control_sample": False - }, - { - "id": "sample2", - "individual_id": "patient1", - "description": "", - "sampled_tissue": { - "id": "UBERON:0002367", - "label": "prostate gland" - }, - "phenotypic_features": [], - "individual_age_at_collection": { - "age": "P52Y2M" - }, - "histological_diagnosis": { - "id": "NCIT:C5596", - "label": "Prostate Acinar Adenocarcinoma" - }, - "tumor_progression": { - "id": "NCIT:C95606", - "label": "Second Primary Malignant Neoplasm" - }, - "tumor_grade": { - "id": "NCIT:C28091", - "label": "Gleason Score 7" - }, - "disease_stage": [], - "diagnostic_markers": [], - "procedure": { - "code": { - "id": "NCIT:C15189", - "label": "Biopsy" - } - }, - "is_control_sample": False - }, - { - "id": "sample3", - "individual_id": "patient1", - "description": "", - "sampled_tissue": { - "id": "UBERON:0001223", - "label": "left ureter" - }, - "phenotypic_features": [], - "individual_age_at_collection": { - "age": "P52Y2M" - }, - "histological_diagnosis": { - "id": "NCIT:C38757", - "label": "Negative Finding" - }, - "disease_stage": [], - "diagnostic_markers": [], - "procedure": { - "code": { - "id": "NCIT:C15189", - "label": "Biopsy" - } - }, - "is_control_sample": False - }, - { - "id": "sample4", - "individual_id": "patient1", - "description": "", - "sampled_tissue": { - "id": "UBERON:0001222", - "label": "right ureter" - }, - "phenotypic_features": [], - "individual_age_at_collection": { - "age": "P52Y2M" - }, - "histological_diagnosis": { - "id": "NCIT:C38757", - "label": "Negative Finding" - }, - "disease_stage": [], - "diagnostic_markers": [], - "procedure": { - "code": { - "id": "NCIT:C15189", - "label": "Biopsy" - } - }, - "is_control_sample": False - }, - { - "id": "sample5", - "individual_id": "patient1", - "description": "", - "sampled_tissue": { - "id": "UBERON:0015876", - "label": "pelvic lymph node" - }, - "phenotypic_features": [], - "individual_age_at_collection": { - "age": "P52Y2M" - }, - "tumor_progression": { - "id": "NCIT:C3261", - "label": "Metastatic Neoplasm" - }, - "disease_stage": [], - "diagnostic_markers": [], - "procedure": { - "code": { - "id": "NCIT:C15189", - "label": "Biopsy" - } - }, - "is_control_sample": False - } - ] +import json +import os + +__all__ = ["EXAMPLE_INGEST_PHENOPACKET", "EXAMPLE_INGEST_OUTPUTS"] + +with open(os.path.join(os.path.dirname(__file__), "example_phenopacket.json"), "r") as pf: + EXAMPLE_INGEST_PHENOPACKET = json.load(pf) + +EXAMPLE_INGEST_OUTPUTS = { + "json_document": os.path.join(os.path.dirname(__file__), "example_phenopacket.json"), } diff --git a/chord_metadata_service/chord/tests/example_phenopacket.json b/chord_metadata_service/chord/tests/example_phenopacket.json new file mode 100644 index 000000000..10d1d8f87 --- /dev/null +++ b/chord_metadata_service/chord/tests/example_phenopacket.json @@ -0,0 +1,233 @@ +{ + "subject": { + "id": "patient1", + "date_of_birth": "1964-03-15T00:00:00Z", + "sex": "MALE", + "karyotypic_sex": "UNKNOWN_KARYOTYPE" + }, + "phenotypic_features": [ + { + "description": "", + "type": { + "id": "HP:0000790", + "label": "Hematuria" + }, + "negated": false, + "modifier": [], + "evidence": [] + }, + { + "description": "", + "type": { + "id": "HP:0100518", + "label": "Dysuria" + }, + "negated": false, + "severity": { + "id": "HP:0012828", + "label": "Severe" + }, + "modifier": [], + "evidence": [] + } + ], + "diseases": [ + { + "term": { + "id": "NCIT:C39853", + "label": "Infiltrating Urothelial Carcinoma" + }, + "disease_stage": [ + { + "id": "NCIT:C48766", + "label": "pT2b Stage Finding" + }, + { + "id": "NCIT:C48750", + "label": "pN2 Stage Finding" + } + ] + } + ], + "meta_data": { + "created": "2019-04-03T15:31:40.765Z", + "created_by": "Peter R", + "submitted_by": "Peter R", + "resources": [ + { + "id": "HP:2019-04-08", + "name": "human phenotype ontology", + "namespace_prefix": "HP", + "url": "http://purl.obolibrary.org/obo/hp.owl", + "version": "2019-04-08", + "iri_prefix": "http://purl.obolibrary.org/obo/HP_" + }, + { + "id": "UBERON:2019-03-08", + "name": "uber anatomy ontology", + "namespace_prefix": "UBERON", + "url": "http://purl.obolibrary.org/obo/uberon.owl", + "version": "2019-03-08", + "iri_prefix": "http://purl.obolibrary.org/obo/UBERON_" + }, + { + "id": "NCIT:18.05d", + "name": "NCI Thesaurus OBO Edition", + "namespace_prefix": "NCIT", + "url": "http://purl.obolibrary.org/obo/ncit.owl", + "version": "18.05d", + "iri_prefix": "http://purl.obolibrary.org/obo/NCIT_" + } + ], + "updated": [], + "external_references": [ + { + "id": "PMID:29221636", + "description": "Urothelial neoplasms in pediatric and young adult patients: A large single-center series" + } + ] + }, + "biosamples": [ + { + "id": "sample1", + "individual_id": "patient1", + "description": "", + "sampled_tissue": { + "id": "UBERON_0001256", + "label": "wall of urinary bladder" + }, + "phenotypic_features": [], + "individual_age_at_collection": { + "age": "P52Y2M" + }, + "histological_diagnosis": { + "id": "NCIT:C39853", + "label": "Infiltrating Urothelial Carcinoma" + }, + "tumor_progression": { + "id": "NCIT:C84509", + "label": "Primary Malignant Neoplasm" + }, + "diagnostic_markers": [], + "procedure": { + "code": { + "id": "NCIT:C5189", + "label": "Radical Cystoprostatectomy" + } + }, + "is_control_sample": false + }, + { + "id": "sample2", + "individual_id": "patient1", + "description": "", + "sampled_tissue": { + "id": "UBERON:0002367", + "label": "prostate gland" + }, + "phenotypic_features": [], + "individual_age_at_collection": { + "age": "P52Y2M" + }, + "histological_diagnosis": { + "id": "NCIT:C5596", + "label": "Prostate Acinar Adenocarcinoma" + }, + "tumor_progression": { + "id": "NCIT:C95606", + "label": "Second Primary Malignant Neoplasm" + }, + "tumor_grade": { + "id": "NCIT:C28091", + "label": "Gleason Score 7" + }, + "disease_stage": [], + "diagnostic_markers": [], + "procedure": { + "code": { + "id": "NCIT:C15189", + "label": "Biopsy" + } + }, + "is_control_sample": false + }, + { + "id": "sample3", + "individual_id": "patient1", + "description": "", + "sampled_tissue": { + "id": "UBERON:0001223", + "label": "left ureter" + }, + "phenotypic_features": [], + "individual_age_at_collection": { + "age": "P52Y2M" + }, + "histological_diagnosis": { + "id": "NCIT:C38757", + "label": "Negative Finding" + }, + "disease_stage": [], + "diagnostic_markers": [], + "procedure": { + "code": { + "id": "NCIT:C15189", + "label": "Biopsy" + } + }, + "is_control_sample": false + }, + { + "id": "sample4", + "individual_id": "patient1", + "description": "", + "sampled_tissue": { + "id": "UBERON:0001222", + "label": "right ureter" + }, + "phenotypic_features": [], + "individual_age_at_collection": { + "age": "P52Y2M" + }, + "histological_diagnosis": { + "id": "NCIT:C38757", + "label": "Negative Finding" + }, + "disease_stage": [], + "diagnostic_markers": [], + "procedure": { + "code": { + "id": "NCIT:C15189", + "label": "Biopsy" + } + }, + "is_control_sample": false + }, + { + "id": "sample5", + "individual_id": "patient1", + "description": "", + "sampled_tissue": { + "id": "UBERON:0015876", + "label": "pelvic lymph node" + }, + "phenotypic_features": [], + "individual_age_at_collection": { + "age": "P52Y2M" + }, + "tumor_progression": { + "id": "NCIT:C3261", + "label": "Metastatic Neoplasm" + }, + "disease_stage": [], + "diagnostic_markers": [], + "procedure": { + "code": { + "id": "NCIT:C15189", + "label": "Biopsy" + } + }, + "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 1759e1f36..d86fb34fd 100644 --- a/chord_metadata_service/chord/tests/test_ingest.py +++ b/chord_metadata_service/chord/tests/test_ingest.py @@ -6,11 +6,15 @@ from chord_metadata_service.chord.data_types import DATA_TYPE_PHENOPACKET from chord_metadata_service.chord.models import Project, Dataset, TableOwnership, Table # noinspection PyProtectedMember -from chord_metadata_service.chord.ingest import create_phenotypic_feature, WORKFLOW_INGEST_FUNCTION_MAP +from chord_metadata_service.chord.ingest import ( + WORKFLOW_PHENOPACKETS_JSON, + create_phenotypic_feature, + WORKFLOW_INGEST_FUNCTION_MAP, +) from chord_metadata_service.phenopackets.models import PhenotypicFeature, Phenopacket from .constants import VALID_DATA_USE_1 -from .example_ingest import EXAMPLE_INGEST +from .example_ingest import EXAMPLE_INGEST_PHENOPACKET, EXAMPLE_INGEST_OUTPUTS class IngestTest(TestCase): @@ -40,21 +44,21 @@ def test_create_pf(self): self.assertEqual(p1.pk, p2.pk) def test_ingesting_phenopackets_json(self): - p = WORKFLOW_INGEST_FUNCTION_MAP[DATA_TYPE_PHENOPACKET](EXAMPLE_INGEST, self.t.identifier) + p = WORKFLOW_INGEST_FUNCTION_MAP[WORKFLOW_PHENOPACKETS_JSON](EXAMPLE_INGEST_OUTPUTS, self.t.identifier) self.assertEqual(p.id, Phenopacket.objects.get(id=p.id).id) - self.assertEqual(p.subject.id, EXAMPLE_INGEST["subject"]["id"]) - self.assertEqual(p.subject.date_of_birth, isoparse(EXAMPLE_INGEST["subject"]["date_of_birth"])) - self.assertEqual(p.subject.sex, EXAMPLE_INGEST["subject"]["sex"]) - self.assertEqual(p.subject.karyotypic_sex, EXAMPLE_INGEST["subject"]["karyotypic_sex"]) + self.assertEqual(p.subject.id, EXAMPLE_INGEST_PHENOPACKET["subject"]["id"]) + self.assertEqual(p.subject.date_of_birth, isoparse(EXAMPLE_INGEST_PHENOPACKET["subject"]["date_of_birth"])) + self.assertEqual(p.subject.sex, EXAMPLE_INGEST_PHENOPACKET["subject"]["sex"]) + self.assertEqual(p.subject.karyotypic_sex, EXAMPLE_INGEST_PHENOPACKET["subject"]["karyotypic_sex"]) pfs = list(p.phenotypic_features.all().order_by("pftype__id")) self.assertEqual(len(pfs), 2) - self.assertEqual(pfs[0].description, EXAMPLE_INGEST["phenotypic_features"][0]["description"]) - self.assertEqual(pfs[0].pftype["id"], EXAMPLE_INGEST["phenotypic_features"][0]["type"]["id"]) - self.assertEqual(pfs[0].pftype["label"], EXAMPLE_INGEST["phenotypic_features"][0]["type"]["label"]) - self.assertEqual(pfs[0].negated, EXAMPLE_INGEST["phenotypic_features"][0]["negated"]) + self.assertEqual(pfs[0].description, EXAMPLE_INGEST_PHENOPACKET["phenotypic_features"][0]["description"]) + self.assertEqual(pfs[0].pftype["id"], EXAMPLE_INGEST_PHENOPACKET["phenotypic_features"][0]["type"]["id"]) + self.assertEqual(pfs[0].pftype["label"], EXAMPLE_INGEST_PHENOPACKET["phenotypic_features"][0]["type"]["label"]) + self.assertEqual(pfs[0].negated, EXAMPLE_INGEST_PHENOPACKET["phenotypic_features"][0]["negated"]) # TODO: Test more properties diseases = list(p.diseases.all().order_by("term__id")) @@ -68,6 +72,6 @@ def test_ingesting_phenopackets_json(self): # TODO: More # Test ingesting again - p2 = WORKFLOW_INGEST_FUNCTION_MAP[DATA_TYPE_PHENOPACKET](EXAMPLE_INGEST, self.t.identifier) + p2 = WORKFLOW_INGEST_FUNCTION_MAP[WORKFLOW_PHENOPACKETS_JSON](EXAMPLE_INGEST_OUTPUTS, self.t.identifier) self.assertNotEqual(p.id, p2.id) # TODO: More From b6b8b6e64243099c604635326f9b7a7fb69181a1 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Tue, 9 Jun 2020 00:41:31 -0400 Subject: [PATCH 100/190] split ingest functions from view add fhir ingest workflow --- chord_metadata_service/chord/ingest.py | 77 ++++++++++++++-- chord_metadata_service/restapi/schemas.py | 10 +- .../restapi/tests/test_fhir_ingest.py | 38 ++++---- .../restapi/views_ingest_fhir.py | 92 ++++++++++++++++++- 4 files changed, 177 insertions(+), 40 deletions(-) diff --git a/chord_metadata_service/chord/ingest.py b/chord_metadata_service/chord/ingest.py index 646e0c3e7..d3adfb620 100644 --- a/chord_metadata_service/chord/ingest.py +++ b/chord_metadata_service/chord/ingest.py @@ -10,7 +10,12 @@ from chord_metadata_service.experiments import models as em from chord_metadata_service.phenopackets import models as pm from chord_metadata_service.resources import models as rm, utils as ru -from chord_metadata_service.restapi.views_ingest_fhir import ingest_fhir +from chord_metadata_service.restapi.views_ingest_fhir import ( + ingest_patients, + ingest_observations, + ingest_conditions, + ingest_specimens +) __all__ = [ @@ -20,12 +25,10 @@ "WORKFLOW_INGEST_FUNCTION_MAP", ] - WORKFLOW_PHENOPACKETS_JSON = "phenopackets_json" WORKFLOW_EXPERIMENTS_JSON = "experiments_json" WORKFLOW_FHIR_JSON = "fhir_json" - METADATA_WORKFLOWS = { "ingestion": { WORKFLOW_PHENOPACKETS_JSON: { @@ -82,14 +85,54 @@ "id": "json_document", "type": "file", "extensions": [".json"] - } + }, + { + "id": "observations", + "type": "file", + "extensions": [".json"] + }, + { + "id": "conditions", + "type": "file", + "extensions": [".json"] + }, + { + "id": "specimens", + "type": "file", + "extensions": [".json"] + }, + { + "id": "created_by", + "type": "string" + }, + ], "outputs": [ { "id": "json_document", "type": "file", "value": "{json_document}" - } + }, + { + "id": "observations", + "type": "file", + "value": "{json_document}" + }, + { + "id": "conditions", + "type": "file", + "value": "{json_document}" + }, + { + "id": "specimens", + "type": "file", + "value": "{json_document}" + }, + { + "id": "created_by", + "type": "string" + }, + ] } }, @@ -140,7 +183,7 @@ def ingest_resource(resource: dict) -> rm.Resource: def ingest_experiment(experiment_data, table_id) -> em.Experiment: """Ingests a single experiment.""" - new_experiment_id = experiment_data["id"] # TODO: Is this provided? + new_experiment_id = experiment_data["id"] # TODO: Is this provided? reference_registry_id = experiment_data.get("reference_registry_id") qc_flags = experiment_data.get("qc_flags", []) @@ -299,9 +342,25 @@ def ingest_phenopacket_workflow(workflow_outputs, table_id): def ingest_fhir_workflow(workflow_outputs, table_id): - with open(workflow_outputs["json_document"], "r") as jf: - json_data = json.load(jf) - return _map_if_list(ingest_fhir, json_data, table_id) + with open(workflow_outputs["json_document"], "r") as pf: + patients_data = json.load(pf) + ingest_patients(patients_data, table_id, + workflow_outputs["created_by"] if "created_by" in workflow_outputs else "Imported from file.") + + if "observations" in workflow_outputs: + with open(workflow_outputs["observations"], "r") as of: + observations_data = json.load(of) + ingest_observations(observations_data) + + if "conditions" in workflow_outputs: + with open(workflow_outputs["conditions"], "r") as cf: + conditions_data = json.load(cf) + ingest_conditions(conditions_data) + + if "specimens" in workflow_outputs: + with open(workflow_outputs["specimens"], "r") as sf: + specimens_data = json.load(sf) + ingest_specimens(specimens_data) WORKFLOW_INGEST_FUNCTION_MAP = { diff --git a/chord_metadata_service/restapi/schemas.py b/chord_metadata_service/restapi/schemas.py index a56e10cf0..21073934f 100644 --- a/chord_metadata_service/restapi/schemas.py +++ b/chord_metadata_service/restapi/schemas.py @@ -131,18 +131,12 @@ "observations": {"type": "string", "description": "Path to an observations file location."}, "conditions": {"type": "string", "description": "Path to a conditions file location."}, "specimens": {"type": "string", "description": "Path to a specimens file location."}, - "metadata": { - "type": "object", - "properties": { - "created_by": {"type": "string"} - }, - "required": ["created_by"] - } + "created_by": {"type": "string"} }, "required": [ "table_id", "patients", - "metadata" + "created_by" ], } diff --git a/chord_metadata_service/restapi/tests/test_fhir_ingest.py b/chord_metadata_service/restapi/tests/test_fhir_ingest.py index 04bee3b75..d6ee3511e 100644 --- a/chord_metadata_service/restapi/tests/test_fhir_ingest.py +++ b/chord_metadata_service/restapi/tests/test_fhir_ingest.py @@ -21,22 +21,22 @@ def setUp(self) -> None: dataset=self.d) self.t = Table.objects.create(ownership_record=to, name="Table 1", data_type=DATA_TYPE_PHENOPACKET) - def test_ingest_body(self): - factory = APIRequestFactory() - request = factory.post('/private/ingest-fhir', INVALID_INGEST_BODY, format='json') - with self.assertRaises(ValidationError): - try: - ingest_fhir(request) - except ValidationError as e: - self.assertIn("created_by", e.message) - raise e - - def test_dataset_id(self): - factory = APIRequestFactory() - invalid_dataset_id_ingest = copy.deepcopy(INVALID_INGEST_BODY) - invalid_dataset_id_ingest["metadata"] = { - "created_by": "Name" - } - request = factory.post('/private/ingest-fhir', invalid_dataset_id_ingest, format='json') - response = ingest_fhir(request) - self.assertEqual(response.status_code, 400) + # def test_ingest_body(self): + # factory = APIRequestFactory() + # request = factory.post('/private/ingest-fhir', INVALID_INGEST_BODY, format='json') + # with self.assertRaises(ValidationError): + # try: + # ingest_fhir(request) + # except ValidationError as e: + # self.assertIn("created_by", e.message) + # raise e + # + # def test_dataset_id(self): + # factory = APIRequestFactory() + # invalid_dataset_id_ingest = copy.deepcopy(INVALID_INGEST_BODY) + # invalid_dataset_id_ingest["metadata"] = { + # "created_by": "Name" + # } + # request = factory.post('/private/ingest-fhir', invalid_dataset_id_ingest, format='json') + # response = ingest_fhir(request) + # self.assertEqual(response.status_code, 400) diff --git a/chord_metadata_service/restapi/views_ingest_fhir.py b/chord_metadata_service/restapi/views_ingest_fhir.py index 88c2772a9..958149e30 100644 --- a/chord_metadata_service/restapi/views_ingest_fhir.py +++ b/chord_metadata_service/restapi/views_ingest_fhir.py @@ -13,6 +13,7 @@ from rest_framework.decorators import api_view, permission_classes from rest_framework.permissions import AllowAny from rest_framework.response import Response +from django.core.exceptions import ValidationError from chord_lib.responses.errors import * from chord_metadata_service.chord.models import * @@ -25,9 +26,7 @@ "observations": "examples/observations.json", "conditions": "examples/conditions.json", "specimens": "examples/specimens.json", - "metadata": { - "created_by": "Ksenia Zaytseva" - } + "created_by": "Ksenia Zaytseva" } @@ -49,6 +48,91 @@ def _check_schema(schema, obj, additional_info=None): raise ValidationError(f"{additional_info + ' ' if additional_info else None}errors: {error_messages}") +def ingest_patients(patients_data, table_id, created_by): + _check_schema(FHIR_BUNDLE_SCHEMA, patients_data, 'patients data') + for item in patients_data["entry"]: + individual_data = patient_to_individual(item["resource"]) + individual, _ = Individual.objects.get_or_create(**individual_data) + # create metadata for Phenopacket + meta_data_obj, _ = MetaData.objects.get_or_create( + created_by=created_by, + phenopacket_schema_version="1.0.0-RC3", + external_references=[] + ) + # create new phenopacket for each individual + phenopacket = Phenopacket.objects.create( + id=str(uuid.uuid4()), + subject=individual, + meta_data=meta_data_obj, + table=Table.objects.get(ownership_record_id=table_id) + ) + print(f'Phenopacket {phenopacket.id} created') + return + + +def ingest_observations(observations_data): + # check if Observations data follows FHIR Bundle schema + _check_schema(FHIR_BUNDLE_SCHEMA, observations_data, 'observations data') + for item in observations_data["entry"]: + phenotypic_feature_data = observation_to_phenotypic_feature(item["resource"]) + # Observation must have a subject + try: + item["resource"]["subject"] + except KeyError: + raise KeyError(f"Observation {item['resource']['id']} doesn't have a subject.") + + subject = _parse_reference(item["resource"]["subject"]["reference"]) + phenotypic_feature, _ = PhenotypicFeature.objects.get_or_create( + phenopacket=Phenopacket.objects.get(subject=Individual.objects.get(id=subject)), + **phenotypic_feature_data + ) + print(f'PhenotypicFeature {phenotypic_feature.id} created') + return + + +def ingest_conditions(conditions_data): + # check if Conditions data follows FHIR Bundle schema + _check_schema(FHIR_BUNDLE_SCHEMA, conditions_data, 'conditions data') + for item in conditions_data["entry"]: + disease_data = condition_to_disease(item["resource"]) + disease = Disease.objects.create(**disease_data) + # Condition must have a subject + try: + item["resource"]["subject"] + except KeyError: + raise KeyError(f"Condition {item['resource']['id']} doesn't have a subject.") + + subject = _parse_reference(item["resource"]["subject"]["reference"]) + phenopacket = Phenopacket.objects.get(subject=Individual.objects.get(id=subject)) + phenopacket.diseases.add(disease) + print(f'Disease {disease.id} created') + return + + +def ingest_specimens(specimens_data): + _check_schema(FHIR_BUNDLE_SCHEMA, specimens_data, 'specimens data') + for item in specimens_data["entry"]: + biosample_data = specimen_to_biosample(item["resource"]) + procedure, _ = Procedure.objects.get_or_create(**biosample_data["procedure"]) + # Specimen must have a subject + try: + biosample_data["individual"] + except KeyError: + raise KeyError(f"Specimen {item['resource']['id']} doesn't have a subject.") + + individual_id = _parse_reference(biosample_data["individual"]) + biosample, _ = Biosample.objects.get_or_create( + id=biosample_data["id"], + procedure=procedure, + individual=Individual.objects.get(id=individual_id), + sampled_tissue=biosample_data["sampled_tissue"] + ) + phenopacket = Phenopacket.objects.get(subject=Individual.objects.get(id=individual_id)) + phenopacket.biosamples.add(biosample) + print(f'Biosample {biosample.id} created') + return + + @api_view(["POST"]) @permission_classes([AllowAny]) def ingest_fhir(request): @@ -78,7 +162,7 @@ def ingest_fhir(request): individual, _ = Individual.objects.get_or_create(**individual_data) # create metadata for Phenopacket meta_data_obj, _ = MetaData.objects.get_or_create( - created_by=request.data["metadata"]["created_by"], + created_by=request.data["created_by"], phenopacket_schema_version="1.0.0-RC3", external_references=[] ) From 734d2e75052070115bdccce447ab0c1e0b04b8aa Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Tue, 9 Jun 2020 09:27:13 -0400 Subject: [PATCH 101/190] clean old code, add logger --- chord_metadata_service/chord/ingest.py | 2 +- chord_metadata_service/metadata/urls.py | 3 +- chord_metadata_service/restapi/fhir_ingest.py | 126 +++++++++ .../restapi/tests/test_fhir_ingest.py | 1 - .../restapi/views_ingest_fhir.py | 249 ------------------ examples/patients.json | 2 +- 6 files changed, 129 insertions(+), 254 deletions(-) create mode 100644 chord_metadata_service/restapi/fhir_ingest.py delete mode 100644 chord_metadata_service/restapi/views_ingest_fhir.py diff --git a/chord_metadata_service/chord/ingest.py b/chord_metadata_service/chord/ingest.py index d3adfb620..d71452958 100644 --- a/chord_metadata_service/chord/ingest.py +++ b/chord_metadata_service/chord/ingest.py @@ -10,7 +10,7 @@ from chord_metadata_service.experiments import models as em from chord_metadata_service.phenopackets import models as pm from chord_metadata_service.resources import models as rm, utils as ru -from chord_metadata_service.restapi.views_ingest_fhir import ( +from chord_metadata_service.restapi.fhir_ingest import ( ingest_patients, ingest_observations, ingest_conditions, diff --git a/chord_metadata_service/metadata/urls.py b/chord_metadata_service/metadata/urls.py index bda070f80..7f67b6a8a 100644 --- a/chord_metadata_service/metadata/urls.py +++ b/chord_metadata_service/metadata/urls.py @@ -15,7 +15,7 @@ """ from django.contrib import admin from django.urls import path, include -from chord_metadata_service.restapi import api_views, views_ingest_fhir, urls as restapi_urls +from chord_metadata_service.restapi import api_views, urls as restapi_urls from chord_metadata_service.chord import urls as chord_urls from rest_framework.schemas import get_schema_view from rest_framework_swagger.views import get_swagger_view @@ -36,7 +36,6 @@ path('api/', include(restapi_urls)), path('api/schema', schema_view, name='openapi-schema'), path('service-info', api_views.service_info, name="service-info"), - path('private/ingest-fhir', views_ingest_fhir.ingest_fhir, name="ingest-fhir"), *chord_urls.urlpatterns, # TODO: Use include? can we double up? *([path('admin/', admin.site.urls)] if DEBUG else []), ] diff --git a/chord_metadata_service/restapi/fhir_ingest.py b/chord_metadata_service/restapi/fhir_ingest.py new file mode 100644 index 000000000..2e8410f5f --- /dev/null +++ b/chord_metadata_service/restapi/fhir_ingest.py @@ -0,0 +1,126 @@ +import uuid +import jsonschema +import logging + +from .schemas import FHIR_BUNDLE_SCHEMA +from .fhir_utils import ( + patient_to_individual, + observation_to_phenotypic_feature, + condition_to_disease, + specimen_to_biosample +) +from chord_metadata_service.chord.models import * +from chord_metadata_service.phenopackets.models import * + + +logger = logging.getLogger("fhir_ingest") +logger.setLevel(logging.INFO) + + +def _parse_reference(ref): + """ FHIR test data has reference object in a format "ResourceType/uuid" """ + return ref.split('/')[-1] + + +def _check_schema(schema, obj, additional_info=None): + """ Validates schema and catches errors. """ + try: + jsonschema.validate(obj, schema) + except jsonschema.exceptions.ValidationError: + v = jsonschema.Draft7Validator(schema) + errors = [e for e in v.iter_errors(obj)] + error_messages = [] + for i, error in enumerate(errors, 1): + error_messages.append(f"{i} validation error {'.'.join(str(v) for v in error.path)}: {error.message}") + raise ValidationError(f"{additional_info + ' ' if additional_info else None}errors: {error_messages}") + + +def ingest_patients(patients_data, table_id, created_by): + """ Takes FHIR Bundle containing Patient resources. """ + # check if Patients data follows FHIR Bundle schema + _check_schema(FHIR_BUNDLE_SCHEMA, patients_data, 'patients data') + for item in patients_data["entry"]: + individual_data = patient_to_individual(item["resource"]) + individual, _ = Individual.objects.get_or_create(**individual_data) + # create metadata for Phenopacket + meta_data_obj, _ = MetaData.objects.get_or_create( + created_by=created_by, + phenopacket_schema_version="1.0.0-RC3", + external_references=[] + ) + # create new phenopacket for each individual + phenopacket = Phenopacket.objects.create( + id=str(uuid.uuid4()), + subject=individual, + meta_data=meta_data_obj, + table=Table.objects.get(ownership_record_id=table_id) + ) + logger.info(f'Phenopacket {phenopacket.id} created') + return + + +def ingest_observations(observations_data): + """ Takes FHIR Bundle containing Observation resources. """ + # check if Observations data follows FHIR Bundle schema + _check_schema(FHIR_BUNDLE_SCHEMA, observations_data, 'observations data') + for item in observations_data["entry"]: + phenotypic_feature_data = observation_to_phenotypic_feature(item["resource"]) + # Observation must have a subject + try: + item["resource"]["subject"] + except KeyError: + raise KeyError(f"Observation {item['resource']['id']} doesn't have a subject.") + + subject = _parse_reference(item["resource"]["subject"]["reference"]) + phenotypic_feature, _ = PhenotypicFeature.objects.get_or_create( + phenopacket=Phenopacket.objects.get(subject=Individual.objects.get(id=subject)), + **phenotypic_feature_data + ) + logger.info(f'PhenotypicFeature {phenotypic_feature.id} created') + return + + +def ingest_conditions(conditions_data): + """ Takes FHIR Bundle containing Condition resources. """ + # check if Conditions data follows FHIR Bundle schema + _check_schema(FHIR_BUNDLE_SCHEMA, conditions_data, 'conditions data') + for item in conditions_data["entry"]: + disease_data = condition_to_disease(item["resource"]) + disease = Disease.objects.create(**disease_data) + # Condition must have a subject + try: + item["resource"]["subject"] + except KeyError: + raise KeyError(f"Condition {item['resource']['id']} doesn't have a subject.") + + subject = _parse_reference(item["resource"]["subject"]["reference"]) + phenopacket = Phenopacket.objects.get(subject=Individual.objects.get(id=subject)) + phenopacket.diseases.add(disease) + logger.info(f'Disease {disease.id} created') + return + + +def ingest_specimens(specimens_data): + """ Takes FHIR Bundle containing Specimen resources. """ + # check if Specimens data follows FHIR Bundle schema + _check_schema(FHIR_BUNDLE_SCHEMA, specimens_data, 'specimens data') + for item in specimens_data["entry"]: + biosample_data = specimen_to_biosample(item["resource"]) + procedure, _ = Procedure.objects.get_or_create(**biosample_data["procedure"]) + # Specimen must have a subject + try: + biosample_data["individual"] + except KeyError: + raise KeyError(f"Specimen {item['resource']['id']} doesn't have a subject.") + + individual_id = _parse_reference(biosample_data["individual"]) + biosample, _ = Biosample.objects.get_or_create( + id=biosample_data["id"], + procedure=procedure, + individual=Individual.objects.get(id=individual_id), + sampled_tissue=biosample_data["sampled_tissue"] + ) + phenopacket = Phenopacket.objects.get(subject=Individual.objects.get(id=individual_id)) + phenopacket.biosamples.add(biosample) + logger.info(f'Biosample {biosample.id} created') + return diff --git a/chord_metadata_service/restapi/tests/test_fhir_ingest.py b/chord_metadata_service/restapi/tests/test_fhir_ingest.py index d6ee3511e..355a95270 100644 --- a/chord_metadata_service/restapi/tests/test_fhir_ingest.py +++ b/chord_metadata_service/restapi/tests/test_fhir_ingest.py @@ -4,7 +4,6 @@ from chord_metadata_service.phenopackets.models import * from chord_metadata_service.patients.models import Individual from .constants import INVALID_INGEST_BODY, INVALID_FHIR_BUNDLE_1 -from ..views_ingest_fhir import ingest_fhir from chord_metadata_service.chord.tests.constants import VALID_DATA_USE_1 from chord_metadata_service.chord.data_types import DATA_TYPE_PHENOPACKET import copy diff --git a/chord_metadata_service/restapi/views_ingest_fhir.py b/chord_metadata_service/restapi/views_ingest_fhir.py deleted file mode 100644 index 958149e30..000000000 --- a/chord_metadata_service/restapi/views_ingest_fhir.py +++ /dev/null @@ -1,249 +0,0 @@ -import json -import uuid -import jsonschema - -from .schemas import FHIR_INGEST_SCHEMA, FHIR_BUNDLE_SCHEMA -from .fhir_utils import ( - patient_to_individual, - observation_to_phenotypic_feature, - condition_to_disease, - specimen_to_biosample -) - -from rest_framework.decorators import api_view, permission_classes -from rest_framework.permissions import AllowAny -from rest_framework.response import Response -from django.core.exceptions import ValidationError -from chord_lib.responses.errors import * - -from chord_metadata_service.chord.models import * -from chord_metadata_service.phenopackets.models import * - - -INGEST_BODY_EXAMPLE = { - "table_id": "62b5fc67-d925-4409-bb59-e1e9a1ef10ae", - "patients": "examples/patients.json", - "observations": "examples/observations.json", - "conditions": "examples/conditions.json", - "specimens": "examples/specimens.json", - "created_by": "Ksenia Zaytseva" -} - - -def _parse_reference(ref): - """ FHIR test data has reference object in a format "ResourceType/uuid" """ - return ref.split('/')[-1] - - -def _check_schema(schema, obj, additional_info=None): - """ Validates schema and catches errors. """ - try: - jsonschema.validate(obj, schema) - except jsonschema.exceptions.ValidationError: - v = jsonschema.Draft7Validator(schema) - errors = [e for e in v.iter_errors(obj)] - error_messages = [] - for i, error in enumerate(errors, 1): - error_messages.append(f"{i} validation error {'.'.join(str(v) for v in error.path)}: {error.message}") - raise ValidationError(f"{additional_info + ' ' if additional_info else None}errors: {error_messages}") - - -def ingest_patients(patients_data, table_id, created_by): - _check_schema(FHIR_BUNDLE_SCHEMA, patients_data, 'patients data') - for item in patients_data["entry"]: - individual_data = patient_to_individual(item["resource"]) - individual, _ = Individual.objects.get_or_create(**individual_data) - # create metadata for Phenopacket - meta_data_obj, _ = MetaData.objects.get_or_create( - created_by=created_by, - phenopacket_schema_version="1.0.0-RC3", - external_references=[] - ) - # create new phenopacket for each individual - phenopacket = Phenopacket.objects.create( - id=str(uuid.uuid4()), - subject=individual, - meta_data=meta_data_obj, - table=Table.objects.get(ownership_record_id=table_id) - ) - print(f'Phenopacket {phenopacket.id} created') - return - - -def ingest_observations(observations_data): - # check if Observations data follows FHIR Bundle schema - _check_schema(FHIR_BUNDLE_SCHEMA, observations_data, 'observations data') - for item in observations_data["entry"]: - phenotypic_feature_data = observation_to_phenotypic_feature(item["resource"]) - # Observation must have a subject - try: - item["resource"]["subject"] - except KeyError: - raise KeyError(f"Observation {item['resource']['id']} doesn't have a subject.") - - subject = _parse_reference(item["resource"]["subject"]["reference"]) - phenotypic_feature, _ = PhenotypicFeature.objects.get_or_create( - phenopacket=Phenopacket.objects.get(subject=Individual.objects.get(id=subject)), - **phenotypic_feature_data - ) - print(f'PhenotypicFeature {phenotypic_feature.id} created') - return - - -def ingest_conditions(conditions_data): - # check if Conditions data follows FHIR Bundle schema - _check_schema(FHIR_BUNDLE_SCHEMA, conditions_data, 'conditions data') - for item in conditions_data["entry"]: - disease_data = condition_to_disease(item["resource"]) - disease = Disease.objects.create(**disease_data) - # Condition must have a subject - try: - item["resource"]["subject"] - except KeyError: - raise KeyError(f"Condition {item['resource']['id']} doesn't have a subject.") - - subject = _parse_reference(item["resource"]["subject"]["reference"]) - phenopacket = Phenopacket.objects.get(subject=Individual.objects.get(id=subject)) - phenopacket.diseases.add(disease) - print(f'Disease {disease.id} created') - return - - -def ingest_specimens(specimens_data): - _check_schema(FHIR_BUNDLE_SCHEMA, specimens_data, 'specimens data') - for item in specimens_data["entry"]: - biosample_data = specimen_to_biosample(item["resource"]) - procedure, _ = Procedure.objects.get_or_create(**biosample_data["procedure"]) - # Specimen must have a subject - try: - biosample_data["individual"] - except KeyError: - raise KeyError(f"Specimen {item['resource']['id']} doesn't have a subject.") - - individual_id = _parse_reference(biosample_data["individual"]) - biosample, _ = Biosample.objects.get_or_create( - id=biosample_data["id"], - procedure=procedure, - individual=Individual.objects.get(id=individual_id), - sampled_tissue=biosample_data["sampled_tissue"] - ) - phenopacket = Phenopacket.objects.get(subject=Individual.objects.get(id=individual_id)) - phenopacket.biosamples.add(biosample) - print(f'Biosample {biosample.id} created') - return - - -@api_view(["POST"]) -@permission_classes([AllowAny]) -def ingest_fhir(request): - """ - View to ingest FHIR data. - Takes FHIR Bundles (collections of resources) of the following types: - Patient, Observation, Condition, Specimen. - """ - - # check schema of ingest body - _check_schema(FHIR_INGEST_SCHEMA, request.data, 'ingest body') - - # check if table exists - table_id = request.data["table_id"] - - if not Table.objects.filter(ownership_record_id=table_id).exists(): - return Response(bad_request_error(f"Table with ID {table_id} does not exist"), status=400) - - # patients-individuals - with open(request.data["patients"], "r") as p_file: - try: - patients_data = json.load(p_file) - # check if Patients data follows FHIR Bundle schema - _check_schema(FHIR_BUNDLE_SCHEMA, patients_data, 'patients data') - for item in patients_data["entry"]: - individual_data = patient_to_individual(item["resource"]) - individual, _ = Individual.objects.get_or_create(**individual_data) - # create metadata for Phenopacket - meta_data_obj, _ = MetaData.objects.get_or_create( - created_by=request.data["created_by"], - phenopacket_schema_version="1.0.0-RC3", - external_references=[] - ) - # create new phenopacket for each individual - phenopacket = Phenopacket.objects.create( - id=str(uuid.uuid4()), - subject=individual, - meta_data=meta_data_obj, - table=Table.objects.get(ownership_record_id=table_id) - ) - print(f'Phenopacket {phenopacket.id} created') - except json.decoder.JSONDecodeError as e: - return Response(bad_request_error(f"Invalid JSON provided (message: {e})"), status=400) - - # observations-phenotypicFeatures - if "observations" in request.data: - with open(request.data["observations"], "r") as obs_file: - try: - observations_data = json.load(obs_file) - # check if Observations data follows FHIR Bundle schema - _check_schema(FHIR_BUNDLE_SCHEMA, observations_data, 'observations data') - for item in observations_data["entry"]: - phenotypic_feature_data = observation_to_phenotypic_feature(item["resource"]) - # Observation must have a subject - if not item["resource"]["subject"]: - return Response(bad_request_error(f"Observation's subject is required."), status=404) - - subject = _parse_reference(item["resource"]["subject"]["reference"]) - phenotypic_feature, _ = PhenotypicFeature.objects.get_or_create( - phenopacket=Phenopacket.objects.get(subject=Individual.objects.get(id=subject)), - **phenotypic_feature_data - ) - except json.decoder.JSONDecodeError as e: - return Response(bad_request_error(f"Invalid JSON provided (message: {e})"), status=400) - - # conditions-diseases - if "conditions" in request.data: - with open(request.data["conditions"]) as c_file: - try: - conditions_data = json.load(c_file) - # check if Conditions data follows FHIR Bundle schema - _check_schema(FHIR_BUNDLE_SCHEMA, conditions_data, 'conditions data') - for item in conditions_data["entry"]: - disease_data = condition_to_disease(item["resource"]) - disease = Disease.objects.create(**disease_data) - # Condition must have a subject - if not item["resource"]["subject"]: - return Response(bad_request_error(f"Subject is required."), status=404) - subject = _parse_reference(item["resource"]["subject"]["reference"]) - phenopacket = Phenopacket.objects.get(subject=Individual.objects.get(id=subject)) - phenopacket.diseases.add(disease) - - except json.decoder.JSONDecodeError as e: - return Response(bad_request_error(f"Invalid JSON provided (message: {e})"), status=400) - - # specimens-biosamples - if "specimens" in request.data: - with open(request.data["specimens"], "r") as s_file: - try: - specimens_data = json.load(s_file) - # check if Specimens data follows FHIR Bundle schema - _check_schema(FHIR_BUNDLE_SCHEMA, specimens_data, 'specimens data') - for item in specimens_data["entry"]: - biosample_data = specimen_to_biosample(item["resource"]) - procedure, _ = Procedure.objects.get_or_create(**biosample_data["procedure"]) - # Specimen must have a subject - if not biosample_data["individual"]: - return Response(bad_request_error(f"Specimen's subject is required."), status=404) - - individual_id = _parse_reference(biosample_data["individual"]) - biosample, _ = Biosample.objects.get_or_create( - id=biosample_data["id"], - procedure=procedure, - individual=Individual.objects.get(id=individual_id), - sampled_tissue=biosample_data["sampled_tissue"] - ) - phenopacket = Phenopacket.objects.get(subject=Individual.objects.get(id=individual_id)) - phenopacket.biosamples.add(biosample) - - - except json.decoder.JSONDecodeError as e: - return Response(bad_request_error(f"Invalid JSON provided (message: {e})"), status=400) - - return Response(status=204) diff --git a/examples/patients.json b/examples/patients.json index c9093fe21..f3c471693 100644 --- a/examples/patients.json +++ b/examples/patients.json @@ -388,6 +388,7 @@ "country": "US" } ], + "gender": "unknown", "resourceType": "Patient", "communication": [ { @@ -407,7 +408,6 @@ "status": "generated", "div": "
Generated by Synthea.Version identifier: v2.2.0-56-g113d8a2d\n . Person seed: -5708762760784740351 Population seed: 5
" }, - "gender": "male", "multipleBirthBoolean": false, "birthDate": "1943-06-08", "maritalStatus": { From 55d26a1316c64bfadf55cdfdc176838a33fc25a8 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Tue, 9 Jun 2020 10:08:58 -0400 Subject: [PATCH 102/190] change tests, remove ingest body schema --- chord_metadata_service/restapi/schemas.py | 22 +--------- .../restapi/tests/constants.py | 32 ++++++++++---- .../restapi/tests/test_fhir_ingest.py | 42 +++++++++---------- 3 files changed, 43 insertions(+), 53 deletions(-) diff --git a/chord_metadata_service/restapi/schemas.py b/chord_metadata_service/restapi/schemas.py index 21073934f..27360bfde 100644 --- a/chord_metadata_service/restapi/schemas.py +++ b/chord_metadata_service/restapi/schemas.py @@ -118,27 +118,7 @@ ############################### FHIR INGEST SCHEMAS ############################### -# The schemas used to validate FHIR data for ingestion - -FHIR_INGEST_SCHEMA = { - "$id": "chord_metadata_service_fhir_ingest_schema", - "$schema": "http://json-schema.org/draft-07/schema#", - "description": "FHIR Ingest schema", - "type": "object", - "properties": { - "table_id": {"type": "string"}, - "patients": {"type": "string", "description": "Path to a patients file location."}, - "observations": {"type": "string", "description": "Path to an observations file location."}, - "conditions": {"type": "string", "description": "Path to a conditions file location."}, - "specimens": {"type": "string", "description": "Path to a specimens file location."}, - "created_by": {"type": "string"} - }, - "required": [ - "table_id", - "patients", - "created_by" - ], -} +# The schema used to validate FHIR data for ingestion FHIR_BUNDLE_SCHEMA = { diff --git a/chord_metadata_service/restapi/tests/constants.py b/chord_metadata_service/restapi/tests/constants.py index 386458f33..ac3651f97 100644 --- a/chord_metadata_service/restapi/tests/constants.py +++ b/chord_metadata_service/restapi/tests/constants.py @@ -1,12 +1,3 @@ -INVALID_INGEST_BODY = { - "table_id": "62b5fc67-d925-4409-bb59-e1e9a1ef10af", - "patients": "patients_file", - "metadata": { - "test": "required created_by is not present" - } -} - - INVALID_FHIR_BUNDLE_1 = { "resourceType": "NotBundle", "entry": [ @@ -15,3 +6,26 @@ } ] } + +INVALID_SUBJECT_NOT_PRESENT = { + "resourceType": "Bundle", + "entry": [ + { + "resource": { + "id": "1c8d2ee3-2a7e-47f9-be16-abe4e9fa306b", + "resourceType": "Observation", + "status": "final", + "code": { + "coding": [ + { + "code": "718-7", + "display": "Hemoglobin [Mass/volume] in Blood", + "system": "http://loinc.org" + } + ], + "text": "Hemoglobin [Mass/volume] in Blood" + } + } + } + ] +} diff --git a/chord_metadata_service/restapi/tests/test_fhir_ingest.py b/chord_metadata_service/restapi/tests/test_fhir_ingest.py index 355a95270..95f66cc29 100644 --- a/chord_metadata_service/restapi/tests/test_fhir_ingest.py +++ b/chord_metadata_service/restapi/tests/test_fhir_ingest.py @@ -1,12 +1,10 @@ from rest_framework.test import APITestCase -from rest_framework.test import APIRequestFactory from chord_metadata_service.chord.models import * from chord_metadata_service.phenopackets.models import * -from chord_metadata_service.patients.models import Individual -from .constants import INVALID_INGEST_BODY, INVALID_FHIR_BUNDLE_1 +from .constants import INVALID_FHIR_BUNDLE_1, INVALID_SUBJECT_NOT_PRESENT from chord_metadata_service.chord.tests.constants import VALID_DATA_USE_1 from chord_metadata_service.chord.data_types import DATA_TYPE_PHENOPACKET -import copy +from chord_metadata_service.restapi.fhir_ingest import ingest_patients, ingest_observations import uuid @@ -20,22 +18,20 @@ def setUp(self) -> None: dataset=self.d) self.t = Table.objects.create(ownership_record=to, name="Table 1", data_type=DATA_TYPE_PHENOPACKET) - # def test_ingest_body(self): - # factory = APIRequestFactory() - # request = factory.post('/private/ingest-fhir', INVALID_INGEST_BODY, format='json') - # with self.assertRaises(ValidationError): - # try: - # ingest_fhir(request) - # except ValidationError as e: - # self.assertIn("created_by", e.message) - # raise e - # - # def test_dataset_id(self): - # factory = APIRequestFactory() - # invalid_dataset_id_ingest = copy.deepcopy(INVALID_INGEST_BODY) - # invalid_dataset_id_ingest["metadata"] = { - # "created_by": "Name" - # } - # request = factory.post('/private/ingest-fhir', invalid_dataset_id_ingest, format='json') - # response = ingest_fhir(request) - # self.assertEqual(response.status_code, 400) + + def test_fhir_bundle_schema(self): + + with self.assertRaises(ValidationError): + try: + ingest_patients(INVALID_FHIR_BUNDLE_1, self.t, "Test") + except ValidationError as e: + self.assertIn("resourceType", e.message) + raise e + + def test_required_subject(self): + + with self.assertRaises(KeyError): + try: + ingest_observations(INVALID_SUBJECT_NOT_PRESENT) + except KeyError as e: + raise e From ed9cee10be1c5ff3989f09dfd410654935a7747e Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Tue, 9 Jun 2020 10:12:45 -0400 Subject: [PATCH 103/190] remove fhir ingest schema from all --- chord_metadata_service/restapi/schemas.py | 1 - 1 file changed, 1 deletion(-) diff --git a/chord_metadata_service/restapi/schemas.py b/chord_metadata_service/restapi/schemas.py index 27360bfde..dd0034dad 100644 --- a/chord_metadata_service/restapi/schemas.py +++ b/chord_metadata_service/restapi/schemas.py @@ -14,7 +14,6 @@ "AGE_OR_AGE_RANGE", "EXTRA_PROPERTIES_SCHEMA", "FHIR_BUNDLE_SCHEMA", - "FHIR_INGEST_SCHEMA", ] From 7e2a156dec597639267e33abc6d9fd9b28e18654 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Tue, 9 Jun 2020 10:24:36 -0400 Subject: [PATCH 104/190] change 'json_document' to 'patients' --- chord_metadata_service/chord/ingest.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/chord_metadata_service/chord/ingest.py b/chord_metadata_service/chord/ingest.py index d71452958..9350e2f6e 100644 --- a/chord_metadata_service/chord/ingest.py +++ b/chord_metadata_service/chord/ingest.py @@ -82,7 +82,7 @@ "file": "fhir_json.wdl", "inputs": [ { - "id": "json_document", + "id": "patients", "type": "file", "extensions": [".json"] }, @@ -109,7 +109,7 @@ ], "outputs": [ { - "id": "json_document", + "id": "patients", "type": "file", "value": "{json_document}" }, @@ -342,7 +342,7 @@ def ingest_phenopacket_workflow(workflow_outputs, table_id): def ingest_fhir_workflow(workflow_outputs, table_id): - with open(workflow_outputs["json_document"], "r") as pf: + with open(workflow_outputs["patients"], "r") as pf: patients_data = json.load(pf) ingest_patients(patients_data, table_id, workflow_outputs["created_by"] if "created_by" in workflow_outputs else "Imported from file.") From 0b8a8b804f035bd07e0bf5271dfd9ed251f682c2 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Tue, 9 Jun 2020 10:39:38 -0400 Subject: [PATCH 105/190] Remove old check in ingestion Update fhir_json wdl --- chord_metadata_service/chord/ingest.py | 9 +++---- chord_metadata_service/chord/views_ingest.py | 8 +++--- .../chord/workflows/fhir_json.wdl | 26 ++++++++++++++++--- 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/chord_metadata_service/chord/ingest.py b/chord_metadata_service/chord/ingest.py index 9350e2f6e..e8a0fac87 100644 --- a/chord_metadata_service/chord/ingest.py +++ b/chord_metadata_service/chord/ingest.py @@ -317,10 +317,8 @@ def ingest_phenopacket(phenopacket_data, table_id) -> pm.Phenopacket: def _map_if_list(fn, data, *args): - if isinstance(data, list): - return [fn(d, *args) for d in data] - - return fn(data, *args) + # TODO: Any sequence? + return [fn(d, *args) for d in data] if isinstance(data, list) else fn(data, *args) def ingest_experiments_workflow(workflow_outputs, table_id): @@ -344,8 +342,7 @@ def ingest_phenopacket_workflow(workflow_outputs, table_id): def ingest_fhir_workflow(workflow_outputs, table_id): with open(workflow_outputs["patients"], "r") as pf: patients_data = json.load(pf) - ingest_patients(patients_data, table_id, - workflow_outputs["created_by"] if "created_by" in workflow_outputs else "Imported from file.") + ingest_patients(patients_data, table_id, workflow_outputs.get("created_by") or "Imported from file.") if "observations" in workflow_outputs: with open(workflow_outputs["observations"], "r") as of: diff --git a/chord_metadata_service/chord/views_ingest.py b/chord_metadata_service/chord/views_ingest.py index ce6880b88..48c8ed696 100644 --- a/chord_metadata_service/chord/views_ingest.py +++ b/chord_metadata_service/chord/views_ingest.py @@ -83,14 +83,16 @@ def ingest(request): if not workflow_exists(workflow_id, METADATA_WORKFLOWS): # Check that the workflow exists return Response(errors.bad_request_error(f"Workflow with ID {workflow_id} does not exist"), status=400) - if "json_document" not in workflow_outputs: - return Response(errors.bad_request_error("Missing workflow output 'json_document'"), status=400) - try: with transaction.atomic(): # Wrap ingestion in a transaction, so if it fails we don't end up in a partial state in the database. WORKFLOW_INGEST_FUNCTION_MAP[workflow_id](workflow_outputs, table_id) + except KeyError: + # Tried to access a non-existant workflow output + # TODO: More precise error (which key?) + return Response(errors.bad_request_error("Missing workflow output"), status=400) + except json.decoder.JSONDecodeError as e: return Response(errors.bad_request_error(f"Invalid JSON provided for ingest document (message: {e})"), status=400) diff --git a/chord_metadata_service/chord/workflows/fhir_json.wdl b/chord_metadata_service/chord/workflows/fhir_json.wdl index 43683b3a7..e99ddd3a5 100644 --- a/chord_metadata_service/chord/workflows/fhir_json.wdl +++ b/chord_metadata_service/chord/workflows/fhir_json.wdl @@ -1,17 +1,35 @@ workflow fhir_json { - File json_document + File patients + File? observations + File? conditions + File? specimens + String? created_by call identity_task { - input: json_document_in = json_document + input: + patients_in = patients, + observations_in = observations, + conditions_in = conditions, + specimens_in = specimens, + created_by_in = created_by } } task identity_task { - File json_document_in + File patients_in + File? observations_in + File? conditions_in + File? specimens_in + String? created_by_in + command { true } output { - File json_document = "${json_document_in}" + File patients = "${patients_in}" + File? observations = "${observations_in}" + File? conditions = "${conditions_in}" + File? specimens = "${specimens_in}" + String? created_by = "${created_by_in}" } } \ No newline at end of file From 4c32489575a76911c8363ac630c5129bcc2d29cd Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Tue, 9 Jun 2020 11:58:31 -0400 Subject: [PATCH 106/190] fix path error in travis --- chord_metadata_service/chord/tests/example_ingest.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/chord_metadata_service/chord/tests/example_ingest.py b/chord_metadata_service/chord/tests/example_ingest.py index b9170c1ce..f7fb63d7f 100644 --- a/chord_metadata_service/chord/tests/example_ingest.py +++ b/chord_metadata_service/chord/tests/example_ingest.py @@ -1,9 +1,13 @@ import json import os +import sys + __all__ = ["EXAMPLE_INGEST_PHENOPACKET", "EXAMPLE_INGEST_OUTPUTS"] -with open(os.path.join(os.path.dirname(__file__), "example_phenopacket.json"), "r") as pf: +current_dir = os.path.join(os.path.dirname(sys.path[0]), os.path.dirname(__file__)) + +with open(os.path.join(current_dir, "example_phenopacket.json"), "r") as pf: EXAMPLE_INGEST_PHENOPACKET = json.load(pf) EXAMPLE_INGEST_OUTPUTS = { From 6c6f3c30fb7473d4e54a0cded3562ebb7d52d152 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Tue, 9 Jun 2020 16:59:08 -0400 Subject: [PATCH 107/190] change path back --- chord_metadata_service/chord/tests/example_ingest.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/chord_metadata_service/chord/tests/example_ingest.py b/chord_metadata_service/chord/tests/example_ingest.py index f7fb63d7f..4c234ac47 100644 --- a/chord_metadata_service/chord/tests/example_ingest.py +++ b/chord_metadata_service/chord/tests/example_ingest.py @@ -1,13 +1,10 @@ import json import os -import sys __all__ = ["EXAMPLE_INGEST_PHENOPACKET", "EXAMPLE_INGEST_OUTPUTS"] -current_dir = os.path.join(os.path.dirname(sys.path[0]), os.path.dirname(__file__)) - -with open(os.path.join(current_dir, "example_phenopacket.json"), "r") as pf: +with open(os.path.join(os.path.dirname(__file__), "example_phenopacket.json"), "r") as pf: EXAMPLE_INGEST_PHENOPACKET = json.load(pf) EXAMPLE_INGEST_OUTPUTS = { From 31b7b8808025eea4a5e4f2f7f7b97de68e1a6d45 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Tue, 9 Jun 2020 17:24:20 -0400 Subject: [PATCH 108/190] include json file in manifest --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index 5e497cd7e..461cc729d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,4 @@ include chord_metadata_service/chord/workflows/phenopackets_json.wdl +include chord_metadata_service/chord/tests/example_phenopacket.json include chord_metadata_service/dats/* include chord_metadata_service/package.cfg From ce87b064f04cbea3384b6b9cb9927726ea8f81b1 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Wed, 10 Jun 2020 11:03:24 -0400 Subject: [PATCH 109/190] save fhir original record in extra_properties --- chord_metadata_service/restapi/fhir_utils.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/chord_metadata_service/restapi/fhir_utils.py b/chord_metadata_service/restapi/fhir_utils.py index cb64ca871..3268150ef 100644 --- a/chord_metadata_service/restapi/fhir_utils.py +++ b/chord_metadata_service/restapi/fhir_utils.py @@ -391,11 +391,11 @@ def fhir_composition(obj): # SNOMED term to use as placeholder when collection method is not present in Specimen procedure_not_assigned = { - "code": { - "id": "SNOMED:42630001", - "label": "Procedure code not assigned", - } - } + "code": { + "id": "SNOMED:42630001", + "label": "Procedure code not assigned", + } +} def patient_to_individual(obj): @@ -421,6 +421,7 @@ def patient_to_individual(obj): individual["active"] = patient.active if patient.deceasedBoolean: individual["deceased"] = patient.deceasedBoolean + individual["extra_properties"] = patient.as_json() return individual @@ -441,6 +442,7 @@ def observation_to_phenotypic_feature(obj): } if observation.specimen: # FK to Biosample phenotypic_feature["biosample"] = observation.specimen.reference + phenotypic_feature["extra_properties"] = observation.as_json() return phenotypic_feature @@ -450,13 +452,14 @@ def condition_to_disease(obj): condition = cond.Condition(obj) codeable_concept = condition.code # CodeableConcept disease = { - # id is an integer AutoField, legacy id can be a string - # "id": condition.id, "term": { + # id is an integer AutoField, legacy id can be a string + # "id": condition.id, "id": f"{codeable_concept.coding[0].system}:{codeable_concept.coding[0].code}", "label": codeable_concept.coding[0].display # TODO collect system info in metadata - } + }, + "extra_properties": condition.as_json() } # condition.stage.type is only in FHIR 4.0.0 version return disease @@ -493,4 +496,5 @@ def specimen_to_biosample(obj): } else: biosample["procedure"] = procedure_not_assigned + biosample["extra_properties"] = specimen.as_json() return biosample From a86db20f97b1e6311dd0ef1588221e74c151cbc8 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Wed, 10 Jun 2020 13:37:10 -0400 Subject: [PATCH 110/190] add mcode profiles version STU 1 Release --- .../mcode/mappings/mcode_profiles.py | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 chord_metadata_service/mcode/mappings/mcode_profiles.py diff --git a/chord_metadata_service/mcode/mappings/mcode_profiles.py b/chord_metadata_service/mcode/mappings/mcode_profiles.py new file mode 100644 index 000000000..e083c832d --- /dev/null +++ b/chord_metadata_service/mcode/mappings/mcode_profiles.py @@ -0,0 +1,53 @@ +# Individual +# TODO there are two version of mcode profile URLs, both are not resolvable, monitor when stable URLs are being release +MCODE_PATIENT = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-cancer-patient" +MCODE_COMORBID_CONDITION = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-comorbid-condition" +MCODE_ECOG_PERFORMANCE_STATUS = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-ecog-performance-status" +MCODE_KARNOFSKY = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-karnofsky-performance-status" + +# GeneticVariantTested +MCODE_GENETIC_VARIANT_TESTED = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-cancer-genetic-variant" + +# GeneticVariantFound +MCODE_GENETIC_VARIANT_FOUND = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-cancer-genetic-variant" + +# GenomicsReport +MCODE_GENOMICS_REPORT = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-cancer-genomics-report" + +# LabsVital +# the following are present in Ballout 1 version but not in 1.0.0 version +MCODE_BODY_HEIGHT = "" +MCODE_BODY_WEIGHT = "" +MCODE_CBC_WITH_AUTO_DIFFERENTIAL_PANEL = "" +MCODE_COMPREHENSIVE_METABOLIC_2000 = "" +MCODE_BLOOD_PRESSURE = "" +MCODE_TUMOR_MARKER_TEST = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-tumor-marker" + +# CancerCondition +MCODE_PRIMARY_CANCER_CONDITION = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-primary-cancer-condition" +MCODE_SECONDARY_CANCER_CONDITION = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-secondary-cancer-condition" + +# TNMStaging +# CLINICAL +MCODE_CLINICAL_STAGE_GROUP = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-tnm-clinical-stage-group" +MCODE_CLINICAL_PRIMARY_TUMOR_CATEGORY = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-tnm-clinical-primary-tumor-category" +MCODE_CLINICAL_REGIONAL_NODES_CATEGORY = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-tnm-clinical-regional-nodes-category" +MCODE_CLINICAL_DISTANT_METASTASES_CATEGORY = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-tnm-clinical-distant-metastases-category" + +# PATHOLOGIC +MCODE_PATHOLOGIC_STAGE_GROUP = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-tnm-pathological-stage-group" +MCODE_PATHOLOGIC_PRIMARY_TUMOR_CATEGORY = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-tnm-pathological-primary-tumor-category" +MCODE_PATHOLOGIC_REGIONAL_NODES_CATEGORY = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-tnm-pathological-regional-nodes-category" +MCODE_PATHOLOGIC_DISTANT_METASTASES_CATEGORY = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-tnm-pathological-distant-metastases-category" + +# CancerRelatedProcedure +# CancerRelatedRadiationProcedure +MCODE_CANCER_RELATED_RADIATION_PROCEDURE = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-cancer-related-radiation-procedure" +# CancerRelatedSurgicalProcedure +MCODE_CANCER_RELATED_SURGICAL_PROCEDURE = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-cancer-related-surgical-procedure" + +# MedicationStatement +MCODE_MEDICATION_STATEMENT = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-cancer-related-medication-statement" + +# add it to mCodepacket +MCODE_CANCER_DISEASE_STATUS = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-cancer-disease-status" From 56a35b213217528773d86a0aa47fac8c45373d48 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Wed, 10 Jun 2020 14:26:24 -0400 Subject: [PATCH 111/190] Fix some issues with fhir workflow --- chord_metadata_service/chord/ingest.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/chord_metadata_service/chord/ingest.py b/chord_metadata_service/chord/ingest.py index d6a5ffd36..2288a8ce5 100644 --- a/chord_metadata_service/chord/ingest.py +++ b/chord_metadata_service/chord/ingest.py @@ -111,26 +111,27 @@ { "id": "patients", "type": "file", - "value": "{json_document}" + "value": "{patients}" }, { "id": "observations", "type": "file", - "value": "{json_document}" + "value": "{observations}" }, { "id": "conditions", "type": "file", - "value": "{json_document}" + "value": "{conditions}" }, { "id": "specimens", "type": "file", - "value": "{json_document}" + "value": "{specimens}" }, { "id": "created_by", - "type": "string" + "type": "string", + "value": "{created_by}" }, ] From 50c9b9eae7b50d9fbc996a9c3a4bbfa19676a211 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Wed, 10 Jun 2020 17:19:50 -0400 Subject: [PATCH 112/190] change mcode model according to a latest released version 1.0.0 STU1 --- chord_metadata_service/mcode/admin.py | 10 - chord_metadata_service/mcode/api_views.py | 11 - chord_metadata_service/mcode/descriptions.py | 51 +---- .../mcode/mappings/mcode_profiles.py | 7 +- chord_metadata_service/mcode/models.py | 98 +-------- chord_metadata_service/mcode/schemas.py | 76 +------ chord_metadata_service/mcode/serializers.py | 20 -- .../mcode/tests/constants.py | 195 +++++++++--------- .../mcode/tests/test_models.py | 65 +----- chord_metadata_service/restapi/urls.py | 2 - examples/mcode_example.json | 39 +--- 11 files changed, 137 insertions(+), 437 deletions(-) diff --git a/chord_metadata_service/mcode/admin.py b/chord_metadata_service/mcode/admin.py index d103f5229..6eacc2ab2 100644 --- a/chord_metadata_service/mcode/admin.py +++ b/chord_metadata_service/mcode/admin.py @@ -2,16 +2,6 @@ from .models import * -@admin.register(GeneticVariantTested) -class GeneticVariantTestedAdmin(admin.ModelAdmin): - pass - - -@admin.register(GeneticVariantFound) -class GeneticVariantFoundAdmin(admin.ModelAdmin): - pass - - @admin.register(GenomicsReport) class GenomicsReportAdmin(admin.ModelAdmin): pass diff --git a/chord_metadata_service/mcode/api_views.py b/chord_metadata_service/mcode/api_views.py index 6c0f38968..1acbdde72 100644 --- a/chord_metadata_service/mcode/api_views.py +++ b/chord_metadata_service/mcode/api_views.py @@ -15,17 +15,6 @@ class McodeModelViewSet(viewsets.ModelViewSet): pagination_class = LargeResultsSetPagination renderer_classes = (*api_settings.DEFAULT_RENDERER_CLASSES, PhenopacketsRenderer) - -class GeneticVariantTestedViewSet(McodeModelViewSet): - queryset = GeneticVariantTested.objects.all() - serializer_class = GeneticVariantTestedSerializer - - -class GeneticVariantFoundViewSet(McodeModelViewSet): - queryset = GeneticVariantFound.objects.all() - serializer_class = GeneticVariantFoundSerializer - - class GenomicsReportViewSet(McodeModelViewSet): queryset = GenomicsReport.objects.all() serializer_class = GenomicsReportSerializer diff --git a/chord_metadata_service/mcode/descriptions.py b/chord_metadata_service/mcode/descriptions.py index b154964bf..cc494a761 100644 --- a/chord_metadata_service/mcode/descriptions.py +++ b/chord_metadata_service/mcode/descriptions.py @@ -7,54 +7,15 @@ from chord_metadata_service.restapi.description_utils import EXTRA_PROPERTIES -GENETIC_VARIANT_TESTED = { - "description": "A description of an alteration in the most common DNA nucleotide sequence.", - "properties": { - "id": "An arbitrary identifier for the genetic variant tested.", - "gene_studied": "A gene targeted for mutation analysis, identified in HUGO Gene Nomenclature Committee " - "(HGNC) notation.", - "method": "An ontology or controlled vocabulary term to identify the method used to perform the genetic test. " - "Accepted value set: NCIT.", - "variant_tested_identifier": "The variation ID assigned by HGVS, for example, 360448 is the identifier for " - "NM_005228.4(EGFR):c.-237A>G (single nucleotide variant in EGFR).", - "variant_tested_hgvs_name": "Symbolic representation of the variant used in HGVS, for example, " - "NM_005228.4(EGFR):c.-237A>G for HVGS variation ID 360448.", - "variant_tested_description": "Description of the variant.", - "data_value": "An ontology or controlled vocabulary term to identify positive or negative value for" - "the mutation. Accepted value set: SNOMED CT.", - **EXTRA_PROPERTIES - } -} - -GENETIC_VARIANT_FOUND = { - "description": "Description of single discrete variant tested.", - "properties": { - "id": "An arbitrary identifier for the genetic variant found.", - "method": "An ontology or controlled vocabulary term to identify the method used to perform the genetic test. " - "Accepted value set: NCIT.", - "variant_found_identifier": "The variation ID assigned by HGVS, for example, 360448 is the identifier for " - "NM_005228.4(EGFR):c.-237A>G (single nucleotide variant in EGFR). " - "Accepted value set: ClinVar.", - "variant_found_hgvs_name": "Symbolic representation of the variant used in HGVS, for example, " - "NM_005228.4(EGFR):c.-237A>G for HVGS variation ID 360448.", - "variant_found_description": "Description of the variant.", - "genomic_source_class": "An ontology or controlled vocabulary term to identify the genomic class of the " - "specimen being analyzed.", - **EXTRA_PROPERTIES - } -} GENOMICS_REPORT = { "description": "Genetic Analysis Summary.", "properties": { "id": "An arbitrary identifier for the genetics report.", - "test_name": "An ontology or controlled vocabulary term to identify the laboratory test. " + "code": "An ontology or controlled vocabulary term to identify the laboratory test. " "Accepted value sets: LOINC, GTR.", "performing_organization_name": "The name of the organization producing the genomics report.", - "specimen_type": "An ontology or controlled vocabulary term to identify the type of material the specimen " - "contains or consists of. Accepted value set: HL7 Version 2 and Specimen Type.", - "genetic_variant_tested": "A test for a specific mutation on a particular gene.", - "genetic_variant_found": "Records an alteration in the most common DNA nucleotide sequence.", + "issued": "The date/time this report was issued.", **EXTRA_PROPERTIES } } @@ -83,16 +44,20 @@ "properties": { "id": "An arbitrary identifier for the cancer condition.", "condition_type": "Cancer condition type: primary or secondary.", - "body_location_code": "Code for the body location, optionally pre-coordinating laterality or direction. " + "body_site": "Code for the body location, optionally pre-coordinating laterality or direction. " "Accepted ontologies: SNOMED CT, ICD-O-3 and others.", + "laterality": "Body side of the body location, if needed to distinguish from a similar location " + "on the other side of the body.", "clinical_status": "A flag indicating whether the condition is active or inactive, recurring, in remission, " "or resolved (as of the last update of the Condition). Accepted code system: " "http://terminology.hl7.org/CodeSystem/condition-clinical", - "condition_code": "A code describing the type of primary or secondary malignant neoplastic disease.", + "code": "A code describing the type of primary or secondary malignant neoplastic disease.", "date_of_diagnosis": "The date the disease was first clinically recognized with sufficient certainty, " "regardless of whether it was fully characterized at that time.", "histology_morphology_behavior": "A description of the morphologic and behavioral characteristics of " "the cancer. Accepted ontologies: SNOMED CT, ICD-O-3 and others.", + "verification_status": "A flag indicating whether the condition is unconfirmed, provisional, differential, " + "confirmed, refuted, or entered-in-error.", **EXTRA_PROPERTIES } } diff --git a/chord_metadata_service/mcode/mappings/mcode_profiles.py b/chord_metadata_service/mcode/mappings/mcode_profiles.py index e083c832d..734742c30 100644 --- a/chord_metadata_service/mcode/mappings/mcode_profiles.py +++ b/chord_metadata_service/mcode/mappings/mcode_profiles.py @@ -5,11 +5,8 @@ MCODE_ECOG_PERFORMANCE_STATUS = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-ecog-performance-status" MCODE_KARNOFSKY = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-karnofsky-performance-status" -# GeneticVariantTested -MCODE_GENETIC_VARIANT_TESTED = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-cancer-genetic-variant" - -# GeneticVariantFound -MCODE_GENETIC_VARIANT_FOUND = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-cancer-genetic-variant" +# GeneticVariant +MCODE_GENETIC_VARIANT = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-cancer-genetic-variant" # GenomicsReport MCODE_GENOMICS_REPORT = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-cancer-genomics-report" diff --git a/chord_metadata_service/mcode/models.py b/chord_metadata_service/mcode/models.py index 5691ba33b..097750aa4 100644 --- a/chord_metadata_service/mcode/models.py +++ b/chord_metadata_service/mcode/models.py @@ -15,97 +15,16 @@ ) -class GeneticVariantTested(models.Model, IndexableMixin): - """ - Class to record an alteration in the most common DNA nucleotide sequence. - """ - - # TODO Discuss: Connection to Gene from Phenopackets - id = models.CharField(primary_key=True, max_length=200, - help_text=rec_help(d.GENETIC_VARIANT_TESTED, "id")) - # make writable if it doesn't exist - gene_studied = models.ForeignKey(Gene, blank=True, null=True, on_delete=models.SET_NULL, - help_text=rec_help(d.GENETIC_VARIANT_TESTED, "gene_studied")) - method = JSONField(blank=True, null=True, validators=[ontology_validator], - help_text=rec_help(d.GENETIC_VARIANT_TESTED, "method")) - variant_tested_identifier = JSONField(blank=True, null=True, validators=[ontology_validator], - help_text=rec_help(d.GENETIC_VARIANT_TESTED, "variant_tested_identifier")) - variant_tested_hgvs_name = ArrayField(models.CharField(max_length=200), blank=True, null=True, - help_text=rec_help(d.GENETIC_VARIANT_TESTED, "variant_tested_hgvs_name")) - variant_tested_description = models.CharField(max_length=200, blank=True, - help_text=rec_help(d.GENETIC_VARIANT_TESTED, - "variant_tested_description")) - data_value = JSONField(blank=True, null=True, validators=[ontology_validator], - help_text=rec_help(d.GENETIC_VARIANT_TESTED, "data_value")) - extra_properties = JSONField(blank=True, null=True, - help_text=rec_help(d.GENETIC_VARIANT_TESTED, "extra_properties")) - created = models.DateTimeField(auto_now=True) - updated = models.DateTimeField(auto_now_add=True) - - class Meta: - ordering = ['id'] - - def __str__(self): - return str(self.id) - - def clean(self): - if not (self.variant_tested_identifier or self.variant_tested_hgvs_name or self.variant_tested_description): - raise ValidationError('At least one element out of the following must be reported: ' - 'Variant Tested Identifier, Variant Tested HGVS Name, and Variant Tested Description') - - -class GeneticVariantFound(models.Model, IndexableMixin): - """ - Class to record whether a single discrete variant tested is present - or absent (denoted as positive or negative respectively). - """ - - # TODO Discuss: Connection to Gene from Phenopackets - id = models.CharField(primary_key=True, max_length=200, - help_text=rec_help(d.GENETIC_VARIANT_FOUND, "id")) - method = JSONField(blank=True, null=True, validators=[ontology_validator], - help_text=rec_help(d.GENETIC_VARIANT_FOUND, "method")) - variant_found_identifier = JSONField(blank=True, null=True, validators=[ontology_validator], - help_text=rec_help(d.GENETIC_VARIANT_FOUND, "variant_found_identifier")) - variant_found_hgvs_name = ArrayField(models.CharField(max_length=200), blank=True, null=True, - help_text=rec_help(d.GENETIC_VARIANT_FOUND, "variant_found_hgvs_name")) - variant_found_description = models.CharField( - max_length=200, blank=True, help_text=rec_help(d.GENETIC_VARIANT_FOUND, "variant_found_description")) - # loinc value set https://loinc.org/48002-0/ - genomic_source_class = JSONField(blank=True, null=True, validators=[ontology_validator], - help_text=rec_help(d.GENETIC_VARIANT_FOUND, "genomic_source_class")) - extra_properties = JSONField(blank=True, null=True, - help_text=rec_help(d.GENETIC_VARIANT_FOUND, "extra_properties")) - created = models.DateTimeField(auto_now=True) - updated = models.DateTimeField(auto_now_add=True) - - class Meta: - ordering = ['id'] - - def __str__(self): - return str(self.id) - - def clean(self): - if not (self.variant_found_identifier or self.variant_found_hgvs_name or self.variant_found_description): - raise ValidationError('At least one element out of the following must be reported: ' - 'Variant Found Identifier, Variant Found HGVS Name, and Variant Found Description') - - class GenomicsReport(models.Model, IndexableMixin): """ Genetic Analysis Summary """ id = models.CharField(primary_key=True, max_length=200, help_text=rec_help(d.GENOMICS_REPORT, "id")) - test_name = JSONField(validators=[ontology_validator], help_text=rec_help(d.GENOMICS_REPORT, "test_name")) + code = JSONField(validators=[ontology_validator], help_text=rec_help(d.GENOMICS_REPORT, "code")) performing_organization_name = models.CharField( max_length=200, blank=True, help_text=rec_help(d.GENOMICS_REPORT, "performing_organization_name")) - specimen_type = JSONField(blank=True, null=True, validators=[ontology_validator], - help_text=rec_help(d.GENOMICS_REPORT, "specimen_type")) - genetic_variant_tested = models.ManyToManyField(GeneticVariantTested, blank=True, - help_text=rec_help(d.GENOMICS_REPORT, "genetic_variant_tested")) - genetic_variant_found = models.ManyToManyField(GeneticVariantFound, blank=True, - help_text=rec_help(d.GENOMICS_REPORT, "genetic_variant_found")) + issued = models.DateTimeField(help_text=rec_help(d.GENOMICS_REPORT, "issued")) extra_properties = JSONField(blank=True, null=True, help_text=rec_help(d.GENOMICS_REPORT, "extra_properties")) created = models.DateTimeField(auto_now=True) @@ -174,17 +93,20 @@ class CancerCondition(models.Model, IndexableMixin): id = models.CharField(primary_key=True, max_length=200, help_text=rec_help(d.CANCER_CONDITION, "id")) condition_type = models.CharField(choices=CANCER_CONDITION_TYPE, max_length=200, help_text=rec_help(d.CANCER_CONDITION, "condition_type")) - # TODO add body_location_code validator array of json - body_location_code = JSONField(null=True, validators=[ontology_list_validator], - help_text=rec_help(d.CANCER_CONDITION, 'body_location_code')) + body_site = JSONField(null=True, validators=[ontology_list_validator], + help_text=rec_help(d.CANCER_CONDITION, 'body_site')) + laterality = JSONField(blank=True, null=True, validators=[ontology_validator], + help_text=rec_help(d.CANCER_CONDITION, "clinical_status")) clinical_status = JSONField(blank=True, null=True, validators=[ontology_validator], help_text=rec_help(d.CANCER_CONDITION, "clinical_status")) - condition_code = JSONField(validators=[ontology_validator], - help_text=rec_help(d.CANCER_CONDITION, "condition_code")) + code = JSONField(validators=[ontology_validator], + help_text=rec_help(d.CANCER_CONDITION, "code")) date_of_diagnosis = models.DateTimeField(blank=True, null=True, help_text=rec_help(d.CANCER_CONDITION, "date_of_diagnosis")) histology_morphology_behavior = JSONField(blank=True, null=True, validators=[ontology_validator], help_text=rec_help(d.CANCER_CONDITION, "histology_morphology_behavior")) + verification_status = JSONField(blank=True, null=True, validators=[ontology_validator], + help_text=rec_help(d.CANCER_CONDITION, "verification_status")) extra_properties = JSONField(blank=True, null=True, help_text=rec_help(d.CANCER_CONDITION, "extra_properties")) created = models.DateTimeField(auto_now=True) diff --git a/chord_metadata_service/mcode/schemas.py b/chord_metadata_service/mcode/schemas.py index dee1d4dda..4e4dbcb04 100644 --- a/chord_metadata_service/mcode/schemas.py +++ b/chord_metadata_service/mcode/schemas.py @@ -155,83 +155,23 @@ ############################## Metadata service mCode based schemas ############################## -MCODE_GENETIC_VARIANT_TESTED_SCHEMA = describe_schema({ - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "gene_studied": { - "type": "string" - }, - "method": ONTOLOGY_CLASS, - "variant_tested_identifier": ONTOLOGY_CLASS, - "variant_tested_hgvs_name": { - "type": "array", - "items": { - "type": "string" - } - }, - "variant_tested_description": { - "type": "string" - }, - "data_value": ONTOLOGY_CLASS, - "extra_properties": EXTRA_PROPERTIES_SCHEMA - }, - "required": ["id"] -}, GENETIC_VARIANT_TESTED) - - -MCODE_GENETIC_VARIANT_FOUND_SCHEMA = describe_schema({ - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "method": ONTOLOGY_CLASS, - "variant_found_identifier": ONTOLOGY_CLASS, - "variant_found_hgvs_name": { - "type": "array", - "items": { - "type": "string" - } - }, - "variant_found_description": { - "type": "string" - }, - "genomic_source_class": ONTOLOGY_CLASS, - "extra_properties": EXTRA_PROPERTIES_SCHEMA - }, - "required": ["id"] -}, GENETIC_VARIANT_FOUND) - - MCODE_GENOMICS_REPORT_SCHEMA = describe_schema({ "type": "object", "properties": { "id": { "type": "string" }, - "test_name": ONTOLOGY_CLASS, + "code": ONTOLOGY_CLASS, "performing_organization_name": { "type": "string" }, - "specimen_type": ONTOLOGY_CLASS, - "genetic_variant_tested": { - "type": "array", - "items": { - "string" - } - }, - "genetic_variant_found": { - "type": "array", - "items": { - "string" - } + "issued": { + "type": "string", + "format": "date-time" }, "extra_properties": EXTRA_PROPERTIES_SCHEMA }, - "required": ["id", "test_name"] + "required": ["id", "code", "issued"] }, GENOMICS_REPORT) @@ -280,9 +220,9 @@ "secondary" ] }, - "body_location_code": ONTOLOGY_CLASS_LIST, + "body_site": ONTOLOGY_CLASS_LIST, "clinical_status": ONTOLOGY_CLASS, - "condition_code": ONTOLOGY_CLASS, + "code": ONTOLOGY_CLASS, "date_of_diagnosis": { "type": "string", "format": "date-time" @@ -290,7 +230,7 @@ "histology_morphology_behavior": ONTOLOGY_CLASS, "extra_properties": EXTRA_PROPERTIES_SCHEMA }, - "required": ["id", "condition_type", "condition_code"] + "required": ["id", "condition_type", "code"] }, LABS_VITAL) diff --git a/chord_metadata_service/mcode/serializers.py b/chord_metadata_service/mcode/serializers.py index a6ca83737..fb8156462 100644 --- a/chord_metadata_service/mcode/serializers.py +++ b/chord_metadata_service/mcode/serializers.py @@ -4,8 +4,6 @@ __all__ = [ - "GeneticVariantTestedSerializer", - "GeneticVariantFoundSerializer", "GenomicsReportSerializer", "LabsVitalSerializer", "TNMStagingSerializer", @@ -16,20 +14,6 @@ ] -class GeneticVariantTestedSerializer(GenericSerializer): - - class Meta: - model = GeneticVariantTested - fields = '__all__' - - -class GeneticVariantFoundSerializer(GenericSerializer): - - class Meta: - model = GeneticVariantFound - fields = '__all__' - - class GenomicsReportSerializer(GenericSerializer): class Meta: @@ -42,10 +26,6 @@ def to_representation(self, instance): objects and return their nested serialization. """ response = super().to_representation(instance) - response['genetic_variant_tested'] = GeneticVariantTestedSerializer(instance.genetic_variant_tested, - many=True, required=False).data - response['genetic_variant_found'] = GeneticVariantFoundSerializer(instance.genetic_variant_found, - many=True, required=False).data return response diff --git a/chord_metadata_service/mcode/tests/constants.py b/chord_metadata_service/mcode/tests/constants.py index 7136643e0..61fd10ae3 100644 --- a/chord_metadata_service/mcode/tests/constants.py +++ b/chord_metadata_service/mcode/tests/constants.py @@ -19,112 +19,109 @@ "active": True } -VALID_GENETIC_VARIANT_TESTED = { - "id": "variant_tested:01", - "method": { - "id": "C17003", - "label": "Polymerase Chain Reaction" - }, - "variant_tested_identifier": { - "id": "360448", - "label": "360448", - }, - "variant_tested_hgvs_name": [ - "NC_000007.13:g.55086734A>G", - "NC_000007.14:g.55019041A>G", - "NM_001346897.2:c.-237A>G", - "NM_001346898.2:c.-237A>G", - "NM_001346899.1:c.-237A>G", - "NM_001346941.2:c.-237A>G", - "NM_005228.5:c.-237A>G", - "NM_201282.2:c.-237A>G", - "NM_201283.1:c.-237A>G", - "NM_201284.2:c.-237A>G", - "LRG_304t1:c.-237A>G", - "LRG_304:g.5010A>G", - "NG_007726.3:g.5010A>G" - ], - "variant_tested_description": "single nucleotide variant", - "data_value": { - "id": "LA6576-8", - "label": "Positive", - } -} +# VALID_GENETIC_VARIANT_TESTED = { +# "id": "variant_tested:01", +# "method": { +# "id": "C17003", +# "label": "Polymerase Chain Reaction" +# }, +# "variant_tested_identifier": { +# "id": "360448", +# "label": "360448", +# }, +# "variant_tested_hgvs_name": [ +# "NC_000007.13:g.55086734A>G", +# "NC_000007.14:g.55019041A>G", +# "NM_001346897.2:c.-237A>G", +# "NM_001346898.2:c.-237A>G", +# "NM_001346899.1:c.-237A>G", +# "NM_001346941.2:c.-237A>G", +# "NM_005228.5:c.-237A>G", +# "NM_201282.2:c.-237A>G", +# "NM_201283.1:c.-237A>G", +# "NM_201284.2:c.-237A>G", +# "LRG_304t1:c.-237A>G", +# "LRG_304:g.5010A>G", +# "NG_007726.3:g.5010A>G" +# ], +# "variant_tested_description": "single nucleotide variant", +# "data_value": { +# "id": "LA6576-8", +# "label": "Positive", +# } +# } -INVALID_GENETIC_VARIANT_TESTED = { - "id": "variant_tested:02", - "method": ["invalid_value"], - "variant_tested_identifier": { - "id": "360448", - "label": "360448", - }, - "variant_tested_hgvs_name": [ - "NC_000007.13:g.55086734A>G", - "NC_000007.14:g.55019041A>G", - "NM_001346897.2:c.-237A>G", - "NM_001346898.2:c.-237A>G", - "NM_001346899.1:c.-237A>G", - "NM_001346941.2:c.-237A>G", - "NM_005228.5:c.-237A>G", - "NM_201282.2:c.-237A>G", - "NM_201283.1:c.-237A>G", - "NM_201284.2:c.-237A>G", - "LRG_304t1:c.-237A>G", - "LRG_304:g.5010A>G", - "NG_007726.3:g.5010A>G" - ], - "variant_tested_description": "single nucleotide variant", - "data_value": { - "id": "LA6576-8", - "label": "Positive", - } -} - -VALID_GENETIC_VARIANT_FOUND = { - "id": "variant_found:01", - "method": { - "id": "C17003", - "label": "Polymerase Chain Reaction" - }, - "variant_found_identifier": { - "id": "360448", - "label": "360448", - }, - "variant_found_hgvs_name": [ - "NC_000007.13:g.55086734A>G", - "NC_000007.14:g.55019041A>G", - "NM_001346897.2:c.-237A>G", - "NM_001346898.2:c.-237A>G", - "NM_001346899.1:c.-237A>G", - "NM_001346941.2:c.-237A>G", - "NM_005228.5:c.-237A>G", - "NM_201282.2:c.-237A>G", - "NM_201283.1:c.-237A>G", - "NM_201284.2:c.-237A>G", - "LRG_304t1:c.-237A>G", - "LRG_304:g.5010A>G", - "NG_007726.3:g.5010A>G" - ], - "variant_found_description": "single nucleotide variant", - "genomic_source_class": { - "id": "LA6684-0", - "label": "Somatic", - } -} +# INVALID_GENETIC_VARIANT_TESTED = { +# "id": "variant_tested:02", +# "method": ["invalid_value"], +# "variant_tested_identifier": { +# "id": "360448", +# "label": "360448", +# }, +# "variant_tested_hgvs_name": [ +# "NC_000007.13:g.55086734A>G", +# "NC_000007.14:g.55019041A>G", +# "NM_001346897.2:c.-237A>G", +# "NM_001346898.2:c.-237A>G", +# "NM_001346899.1:c.-237A>G", +# "NM_001346941.2:c.-237A>G", +# "NM_005228.5:c.-237A>G", +# "NM_201282.2:c.-237A>G", +# "NM_201283.1:c.-237A>G", +# "NM_201284.2:c.-237A>G", +# "LRG_304t1:c.-237A>G", +# "LRG_304:g.5010A>G", +# "NG_007726.3:g.5010A>G" +# ], +# "variant_tested_description": "single nucleotide variant", +# "data_value": { +# "id": "LA6576-8", +# "label": "Positive", +# } +# } +# +# VALID_GENETIC_VARIANT_FOUND = { +# "id": "variant_found:01", +# "method": { +# "id": "C17003", +# "label": "Polymerase Chain Reaction" +# }, +# "variant_found_identifier": { +# "id": "360448", +# "label": "360448", +# }, +# "variant_found_hgvs_name": [ +# "NC_000007.13:g.55086734A>G", +# "NC_000007.14:g.55019041A>G", +# "NM_001346897.2:c.-237A>G", +# "NM_001346898.2:c.-237A>G", +# "NM_001346899.1:c.-237A>G", +# "NM_001346941.2:c.-237A>G", +# "NM_005228.5:c.-237A>G", +# "NM_201282.2:c.-237A>G", +# "NM_201283.1:c.-237A>G", +# "NM_201284.2:c.-237A>G", +# "LRG_304t1:c.-237A>G", +# "LRG_304:g.5010A>G", +# "NG_007726.3:g.5010A>G" +# ], +# "variant_found_description": "single nucleotide variant", +# "genomic_source_class": { +# "id": "LA6684-0", +# "label": "Somatic", +# } +# } def valid_genetic_report(): return { "id": "genomics_report:01", - "test_name": { + "code": { "id": "GTR000567625.2", "label": "PREVENTEST", }, - "performing_organization_name": "Test organization", - "specimen_type": { - "id": "119342007 ", - "label": "SAL (Saliva)", - } + "issued": "2018-11-13T20:20:39+00:00", + "performing_organization_name": "Test organization" } @@ -167,7 +164,7 @@ def valid_cancer_condition(): return { "id": "cancer_condition:01", "condition_type": "primary", - "body_location_code": [ + "body_site": [ { "id": "442083009", "label": "Anatomical or acquired body structure (body structure)" @@ -177,7 +174,7 @@ def valid_cancer_condition(): "id": "active", "label": "Active" }, - "condition_code": { + "code": { "id": "404087009", "label": "Carcinosarcoma of skin (disorder)" }, diff --git a/chord_metadata_service/mcode/tests/test_models.py b/chord_metadata_service/mcode/tests/test_models.py index 8fd4c93b9..8cb0d9c95 100644 --- a/chord_metadata_service/mcode/tests/test_models.py +++ b/chord_metadata_service/mcode/tests/test_models.py @@ -6,71 +6,16 @@ from rest_framework import serializers from django.core.validators import ValidationError -class GeneticVariantTestedTest(TestCase): - """ Test module for GeneticVariantTested model """ - - def setUp(self): - GeneticVariantTested.objects.create(**VALID_GENETIC_VARIANT_TESTED) - - def test_variant_tested(self): - variant_tested = GeneticVariantTested.objects.get(id='variant_tested:01') - self.assertIsInstance(variant_tested.method, dict) - self.assertEqual(variant_tested.method['label'], 'Polymerase Chain Reaction') - self.assertEqual(variant_tested.variant_tested_identifier['id'], - variant_tested.variant_tested_identifier['label']) - self.assertIsInstance(variant_tested.variant_tested_hgvs_name, list) - self.assertIn("NM_001346897.2:c.-237A>G", variant_tested.variant_tested_hgvs_name) - self.assertIsInstance(variant_tested.variant_tested_description, str) - self.assertEqual(variant_tested.variant_tested_description, 'single nucleotide variant') - self.assertEqual(variant_tested.data_value['id'], 'LA6576-8') - self.assertEqual(variant_tested.data_value['label'], 'Positive') - - def create(self, **kwargs): - e = GeneticVariantTested(**kwargs) - e.full_clean() - e.save() - - def test_validation(self): - self.assertRaises(serializers.ValidationError, self.create, **INVALID_GENETIC_VARIANT_TESTED) - - -class GeneticVariantFoundTest(TestCase): - """ Test module for GeneticVariantFound model """ - - def setUp(self): - GeneticVariantFound.objects.create(**VALID_GENETIC_VARIANT_FOUND) - - def test_variant_found(self): - variant_found = GeneticVariantFound.objects.get(id='variant_found:01') - self.assertIsInstance(variant_found.method, dict) - self.assertEqual(variant_found.method['label'], 'Polymerase Chain Reaction') - self.assertEqual(variant_found.variant_found_identifier['id'], - variant_found.variant_found_identifier['label']) - self.assertIsInstance(variant_found.variant_found_hgvs_name, list) - self.assertIn("NM_001346897.2:c.-237A>G", variant_found.variant_found_hgvs_name) - self.assertIsInstance(variant_found.variant_found_description, str) - self.assertEqual(variant_found.variant_found_description, 'single nucleotide variant') - self.assertEqual(variant_found.genomic_source_class['id'], 'LA6684-0') - self.assertEqual(variant_found.genomic_source_class['label'], 'Somatic') - class GenomicsReportTest(TestCase): """ Test module for Genomics Report model """ def setUp(self): - self.variant_tested = GeneticVariantTested.objects.create(**VALID_GENETIC_VARIANT_TESTED) - self.variant_found = GeneticVariantFound.objects.create(**VALID_GENETIC_VARIANT_FOUND) self.genomics_report = GenomicsReport.objects.create(**valid_genetic_report()) - self.genomics_report.genetic_variant_tested.set([self.variant_tested]) - self.genomics_report.genetic_variant_found.set([self.variant_found]) def test_genomics_report(self): genomics_report = GenomicsReport.objects.get(id='genomics_report:01') - self.assertEqual(genomics_report.test_name['id'], 'GTR000567625.2') - self.assertIsInstance(genomics_report.specimen_type, dict) - self.assertIsNotNone(genomics_report.genetic_variant_tested) - self.assertEqual(genomics_report.genetic_variant_tested.count(), 1) - self.assertEqual(genomics_report.genetic_variant_found.count(), 1) + self.assertEqual(genomics_report.code['id'], 'GTR000567625.2') class LabsVitalTest(TestCase): @@ -117,11 +62,11 @@ def setUp(self): def test_cancer_condition(self): cancer_condition = CancerCondition.objects.get(id='cancer_condition:01') self.assertEqual(cancer_condition.condition_type, 'primary') - self.assertIsInstance(cancer_condition.body_location_code, list) - self.assertEqual(cancer_condition.body_location_code[0]['id'], '442083009') + self.assertIsInstance(cancer_condition.body_site, list) + self.assertEqual(cancer_condition.body_site[0]['id'], '442083009') self.assertEqual(cancer_condition.clinical_status['id'], 'active') - condition_code_keys = [key for key in cancer_condition.condition_code.keys()] - self.assertEqual(condition_code_keys, ['id', 'label']) + code_keys = [key for key in cancer_condition.code.keys()] + self.assertEqual(code_keys, ['id', 'label']) self.assertEqual(cancer_condition.histology_morphology_behavior['id'], '372147008') diff --git a/chord_metadata_service/restapi/urls.py b/chord_metadata_service/restapi/urls.py index f8b4ebddf..49946fbd5 100644 --- a/chord_metadata_service/restapi/urls.py +++ b/chord_metadata_service/restapi/urls.py @@ -39,8 +39,6 @@ router.register(r'interpretations', phenopacket_views.InterpretationViewSet) # mCode app urls -router.register(r'geneticvariantstested', mcode_views.GeneticVariantTestedViewSet) -router.register(r'geneticvariantsfound', mcode_views.GeneticVariantFoundViewSet) router.register(r'genomicsreports', mcode_views.GenomicsReportViewSet) router.register(r'labsvital', mcode_views.LabsVitalViewSet) router.register(r'cancerconditions', mcode_views.CancerConditionViewSet) diff --git a/examples/mcode_example.json b/examples/mcode_example.json index aa59b5e6c..d5a312ca2 100644 --- a/examples/mcode_example.json +++ b/examples/mcode_example.json @@ -14,38 +14,11 @@ }, "genomics_report": { "id": "genomics_resport:01", - "test_name": { + "code": { "id": "GTR000511179.13", "label": "Clinical Exome" }, - "performing_organization_name": "Test organization", - "specimen_type": { - "id": "BDY", - "label": "Whole body" - }, - "genetic_variant_tested": [ - { - "id": "variant:03", - "method": { - "id": "FISH", - "label": "Fluorescent in situ hybridization (FISH)" - }, - "variant_tested_identifier": { - "id": "t12", - "label": "test" - }, - "variant_tested_description": "test" - } - ], - "genetic_variant_found": [ - { - "id": "variant_found:01", - "method": { - "id": "t12", - "label": "test" - } - } - ] + "performing_organization_name": "Test organization" }, "cancer_condition": { "id": "cancer_condition:01", @@ -81,7 +54,7 @@ } ], "condition_type": "primary", - "body_location_code": [ + "body_site": [ { "id": "442083009", "label": "Anatomical or acquired body structure (body structure)" @@ -91,7 +64,7 @@ "id": "active", "label": "Active" }, - "condition_code": { + "code": { "id": "404087009", "label": "Carcinosarcoma of skin (disorder)" }, @@ -99,6 +72,10 @@ "histology_morphology_behavior": { "id": "372147008", "label": "Kaposi's sarcoma - category (morphologic abnormality)" + }, + "verification_status": { + "id": "unconfirmed", + "label": "Unconfirmed" } }, "medication_statement": { From ee4b53e91591d634768aaf07666b9d371a836bad Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Wed, 10 Jun 2020 18:11:28 -0400 Subject: [PATCH 113/190] add genetic variant and specimen (models and descriptions) --- chord_metadata_service/mcode/descriptions.py | 36 +++++++++++ chord_metadata_service/mcode/models.py | 64 +++++++++++++++++++- 2 files changed, 98 insertions(+), 2 deletions(-) diff --git a/chord_metadata_service/mcode/descriptions.py b/chord_metadata_service/mcode/descriptions.py index cc494a761..afeb1b2f9 100644 --- a/chord_metadata_service/mcode/descriptions.py +++ b/chord_metadata_service/mcode/descriptions.py @@ -8,6 +8,42 @@ from chord_metadata_service.restapi.description_utils import EXTRA_PROPERTIES +GENOMIC_SPECIMEN = { + "description": "Class to describe a biosample used for genomics testing or analysis.", + "properties": { + "id": "An arbitrary identifier for the genetic specimen.", + "specimen_type": "The kind of material that forms the specimen.", + "collection_body": "The anatomical collection site.", + "laterality": "Body side of the collection site, if needed to distinguish from a similar " + "location on the other side of the body.", + **EXTRA_PROPERTIES + } +} + +CANCER_GENETIC_VARIANT = { + "description": "Class to record an alteration in DNA.", + "properties": { + "id": "An arbitrary identifier for the cancer genetic variant.", + "data_value": "The overall result of the genetic test; specifically, whether a variant is present, " + "absent, no call, or indeterminant.", + "method": "The method used to perform the genetic test.", + "amino_acid_change": "The symbolic representation of an amino acid variant reported using " + "HGVS nomenclature (pHGVS).", + "amino_acid_change_type": "The type of change related to the amino acid variant.", + "cytogenetic_location": "The cytogenetic (chromosome) location.", + "cytogenetic_nomenclature": "The cytogenetic (chromosome) location, represented using the International " + "System for Human Cytogenetic Nomenclature (ISCN).", + "gene_studied": "A gene targeted for mutation analysis, identified in " + "HUGO Gene Nomenclature Committee (HGNC) notation.", + "genomic_dna_change": "The symbolic representation of a genetic structural variant reported " + "using HGVS nomenclature (gHGVS).", + "genomic_source_class": "The genomic class of the specimen being analyzed, for example, germline for " + "inherited genome, somatic for cancer genome, and prenatal for fetal genome.", + "variation_code": "The variation ID assigned by ClinVar.", + **EXTRA_PROPERTIES + } +} + GENOMICS_REPORT = { "description": "Genetic Analysis Summary.", "properties": { diff --git a/chord_metadata_service/mcode/models.py b/chord_metadata_service/mcode/models.py index 097750aa4..accb8edd5 100644 --- a/chord_metadata_service/mcode/models.py +++ b/chord_metadata_service/mcode/models.py @@ -15,9 +15,69 @@ ) +class GeneticSpecimen(models.Model, IndexableMixin): + """ + Class to describe a biosample used for genomics testing or analysis. + """ + id = models.CharField(primary_key=True, max_length=200, help_text=rec_help(d.GENOMIC_SPECIMEN, "id")) + specimen_type = JSONField(validators=[ontology_validator], help_text=rec_help(d.GENOMIC_SPECIMEN, "specimen_type")) + collection_body = JSONField(blank=True, null=True, validators=[ontology_validator], + help_text=rec_help(d.GENOMIC_SPECIMEN, "collection_body")) + laterality = JSONField(blank=True, null=True, validators=[ontology_validator], + help_text=rec_help(d.GENOMIC_SPECIMEN, "laterality")) + extra_properties = JSONField(blank=True, null=True, + help_text=rec_help(d.GENOMIC_SPECIMEN, "extra_properties")) + created = models.DateTimeField(auto_now=True) + updated = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ['id'] + + def __str__(self): + return str(self.id) + + +class CancerGeneticVariant(models.Model, IndexableMixin): + """ + Class to record an alteration in DNA. + """ + id = models.CharField(primary_key=True, max_length=200, help_text=rec_help(d.CANCER_GENETIC_VARIANT, "id")) + data_value = JSONField(blank=True, null=True, validators=[ontology_validator], + help_text=rec_help(d.CANCER_GENETIC_VARIANT, "data_value")) + method = JSONField(blank=True, null=True, validators=[ontology_validator], + help_text=rec_help(d.CANCER_GENETIC_VARIANT, "method")) + amino_acid_change = JSONField(blank=True, null=True, validators=[ontology_validator], + help_text=rec_help(d.CANCER_GENETIC_VARIANT, "amino_acid_change")) + amino_acid_change_type = JSONField(blank=True, null=True, validators=[ontology_validator], + help_text=rec_help(d.CANCER_GENETIC_VARIANT, "amino_acid_change_type")) + cytogenetic_location = JSONField(blank=True, null=True, + help_text=rec_help(d.CANCER_GENETIC_VARIANT, "cytogenetic_location")) + cytogenetic_nomenclature = JSONField(blank=True, null=True, validators=[ontology_validator], + help_text=rec_help(d.CANCER_GENETIC_VARIANT, "cytogenetic_nomenclature")) + gene_studied = models.ManyToManyField(Gene, blank=True, on_delete=models.SET_NULL, + help_text=rec_help(d.CANCER_GENETIC_VARIANT, "gene_studied")) + genomic_dna_change = JSONField(blank=True, null=True, validators=[ontology_validator], + help_text=rec_help(d.CANCER_GENETIC_VARIANT, "genomic_dna_change")) + genomic_source_class = JSONField(blank=True, null=True, validators=[ontology_validator], + help_text=rec_help(d.CANCER_GENETIC_VARIANT, "genomic_source_class")) + variation_code = JSONField(blank=True, null=True, validators=[ontology_list_validator], + help_text=rec_help(d.CANCER_GENETIC_VARIANT, "variation_code")) + extra_properties = JSONField(blank=True, null=True, + help_text=rec_help(d.CANCER_GENETIC_VARIANT, "extra_properties")) + created = models.DateTimeField(auto_now=True) + updated = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ['id'] + + def __str__(self): + return str(self.id) + + + class GenomicsReport(models.Model, IndexableMixin): """ - Genetic Analysis Summary + Genetic Analysis Summary. """ id = models.CharField(primary_key=True, max_length=200, help_text=rec_help(d.GENOMICS_REPORT, "id")) @@ -96,7 +156,7 @@ class CancerCondition(models.Model, IndexableMixin): body_site = JSONField(null=True, validators=[ontology_list_validator], help_text=rec_help(d.CANCER_CONDITION, 'body_site')) laterality = JSONField(blank=True, null=True, validators=[ontology_validator], - help_text=rec_help(d.CANCER_CONDITION, "clinical_status")) + help_text=rec_help(d.CANCER_CONDITION, "laterality")) clinical_status = JSONField(blank=True, null=True, validators=[ontology_validator], help_text=rec_help(d.CANCER_CONDITION, "clinical_status")) code = JSONField(validators=[ontology_validator], From bc0a32501f93148a08ec1d819bf47494a59801c9 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Wed, 10 Jun 2020 18:18:44 -0400 Subject: [PATCH 114/190] add schemas for genetic specimen and variant --- chord_metadata_service/mcode/schemas.py | 43 +++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/chord_metadata_service/mcode/schemas.py b/chord_metadata_service/mcode/schemas.py index 4e4dbcb04..65b14ca96 100644 --- a/chord_metadata_service/mcode/schemas.py +++ b/chord_metadata_service/mcode/schemas.py @@ -155,6 +155,49 @@ ############################## Metadata service mCode based schemas ############################## +MCODE_GENETIC_SPECIMEN_SCHEMA = describe_schema({ + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "specimen_type": ONTOLOGY_CLASS, + "collection_body": ONTOLOGY_CLASS, + "laterality": ONTOLOGY_CLASS, + "extra_properties": EXTRA_PROPERTIES_SCHEMA + }, + "required": ["id", "specimen_type"] +}, GENOMICS_REPORT) + +MCODE_CANCER_GENETIC_VARIANT_SCHEMA = describe_schema({ + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "data_value": ONTOLOGY_CLASS, + "method": ONTOLOGY_CLASS, + "amino_acid_change": ONTOLOGY_CLASS, + "amino_acid_change_type": ONTOLOGY_CLASS, + "cytogenetic_location": { + "type": "object" + }, + "cytogenetic_nomenclature": ONTOLOGY_CLASS, + "gene_studied": { + "type": "array", + "items": { + "type": "string" + } + }, + "genomic_dna_change": ONTOLOGY_CLASS, + "genomic_source_class": ONTOLOGY_CLASS, + "variation_code": ONTOLOGY_CLASS_LIST, + "extra_properties": EXTRA_PROPERTIES_SCHEMA + }, + "required": ["id", "specimen_type"] +}, GENOMICS_REPORT) + + MCODE_GENOMICS_REPORT_SCHEMA = describe_schema({ "type": "object", "properties": { From eef5725dfd3967277d66eb74f4e90cfc9a14061e Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Wed, 10 Jun 2020 18:22:37 -0400 Subject: [PATCH 115/190] small fix --- chord_metadata_service/mcode/descriptions.py | 2 +- chord_metadata_service/mcode/schemas.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/chord_metadata_service/mcode/descriptions.py b/chord_metadata_service/mcode/descriptions.py index afeb1b2f9..46cbb4a09 100644 --- a/chord_metadata_service/mcode/descriptions.py +++ b/chord_metadata_service/mcode/descriptions.py @@ -8,7 +8,7 @@ from chord_metadata_service.restapi.description_utils import EXTRA_PROPERTIES -GENOMIC_SPECIMEN = { +GENETIC_SPECIMEN = { "description": "Class to describe a biosample used for genomics testing or analysis.", "properties": { "id": "An arbitrary identifier for the genetic specimen.", diff --git a/chord_metadata_service/mcode/schemas.py b/chord_metadata_service/mcode/schemas.py index 65b14ca96..424e3ea8f 100644 --- a/chord_metadata_service/mcode/schemas.py +++ b/chord_metadata_service/mcode/schemas.py @@ -167,7 +167,7 @@ "extra_properties": EXTRA_PROPERTIES_SCHEMA }, "required": ["id", "specimen_type"] -}, GENOMICS_REPORT) +}, GENETIC_SPECIMEN) MCODE_CANCER_GENETIC_VARIANT_SCHEMA = describe_schema({ "type": "object", @@ -195,7 +195,7 @@ "extra_properties": EXTRA_PROPERTIES_SCHEMA }, "required": ["id", "specimen_type"] -}, GENOMICS_REPORT) +}, CANCER_GENETIC_VARIANT) MCODE_GENOMICS_REPORT_SCHEMA = describe_schema({ From aff1306697211e0b8f83622aa3b4ebb61aae8bbe Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Wed, 10 Jun 2020 19:04:16 -0400 Subject: [PATCH 116/190] add genomic region studied model, description and schema --- chord_metadata_service/mcode/descriptions.py | 14 +++++++++ chord_metadata_service/mcode/models.py | 33 ++++++++++++++++++++ chord_metadata_service/mcode/schemas.py | 23 ++++++++++++++ 3 files changed, 70 insertions(+) diff --git a/chord_metadata_service/mcode/descriptions.py b/chord_metadata_service/mcode/descriptions.py index 46cbb4a09..d9b6bf369 100644 --- a/chord_metadata_service/mcode/descriptions.py +++ b/chord_metadata_service/mcode/descriptions.py @@ -44,6 +44,20 @@ } } +GENOMIC_REGION_STUDIED = { + "description": "Class to describe the area of the genome region referenced in testing for variants.", + "properties": { + "id": "An arbitrary identifier for the genomic region studied.", + "dna_ranges_examined": "The range(s) of the DNA sequence examined.", + "dna_region_description": "The description for the DNA region studied in the genomics report.", + "gene_mutation": "The gene mutations tested for in blood or tissue by molecular genetics methods.", + "gene_studied": "The ID for the gene studied.", + "genomic_reference_sequence_id": "Range(s) of DNA sequence examined.", + "genomic_region_coordinate_system": "The method of counting along the genome.", + **EXTRA_PROPERTIES + } +} + GENOMICS_REPORT = { "description": "Genetic Analysis Summary.", "properties": { diff --git a/chord_metadata_service/mcode/models.py b/chord_metadata_service/mcode/models.py index accb8edd5..0c9fe4e7f 100644 --- a/chord_metadata_service/mcode/models.py +++ b/chord_metadata_service/mcode/models.py @@ -74,6 +74,39 @@ def __str__(self): return str(self.id) +class GenomicRegionStudied(models.Model, IndexableMixin): + """ + Class to describe the area of the genome region referenced in testing for variants. + """ + id = models.CharField(primary_key=True, max_length=200, help_text=rec_help(d.GENOMIC_REGION_STUDIED, "id")) + # TODO schema Range list + dna_ranges_examined = JSONField(blank=True, null=True, validators=[ontology_list_validator], + help_text=rec_help(d.GENOMIC_REGION_STUDIED, "dna_ranges_examined")) + dna_region_description = ArrayField(models.CharField(max_length=100, + help_text=rec_help(d.GENOMIC_REGION_STUDIED, 'dna_region_description')), + blank=True, default=list) + gene_mutation = JSONField(blank=True, null=True, validators=[ontology_list_validator], + help_text=rec_help(d.GENOMIC_REGION_STUDIED, "gene_mutation")) + # TODO check: thisis not a Reference in mcode data dictionary why not? + gene_studied = JSONField(blank=True, null=True, validators=[ontology_list_validator], + help_text=rec_help(d.GENOMIC_REGION_STUDIED, "gene_studied")) + genomic_reference_sequence_id = JSONField(blank=True, null=True, + help_text=rec_help(d.GENOMIC_REGION_STUDIED, + "genomic_reference_sequence_id")) + genomic_region_coordinate_system = JSONField(blank=True, null=True, validators=[ontology_validator], + help_text=rec_help(d.GENOMIC_REGION_STUDIED, + "genomic_region_coordinate_system")) + extra_properties = JSONField(blank=True, null=True, + help_text=rec_help(d.GENOMIC_REGION_STUDIED, "extra_properties")) + created = models.DateTimeField(auto_now=True) + updated = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ['id'] + + def __str__(self): + return str(self.id) + class GenomicsReport(models.Model, IndexableMixin): """ diff --git a/chord_metadata_service/mcode/schemas.py b/chord_metadata_service/mcode/schemas.py index 424e3ea8f..61e78f5f8 100644 --- a/chord_metadata_service/mcode/schemas.py +++ b/chord_metadata_service/mcode/schemas.py @@ -197,6 +197,29 @@ "required": ["id", "specimen_type"] }, CANCER_GENETIC_VARIANT) +MCODE_GENOMIC_REGION_STUDIED_SCHEMA = describe_schema({ + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "dna_ranges_examined": ONTOLOGY_CLASS_LIST, + "dna_region_description": { + "type": "array", + "items": { + "type": "string" + } + }, + "gene_mutation": ONTOLOGY_CLASS_LIST, + "gene_studied": ONTOLOGY_CLASS_LIST, + "genomic_reference_sequence_id": { + "type": "object" + }, + "genomic_region_coordinate_system": ONTOLOGY_CLASS, + "extra_properties": EXTRA_PROPERTIES_SCHEMA + }, + "required": ["id", "specimen_type"] +}, GENOMIC_REGION_STUDIED) MCODE_GENOMICS_REPORT_SCHEMA = describe_schema({ "type": "object", From c8d6618c9359e2353a5eda9af6f18787c6904f96 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Wed, 10 Jun 2020 19:14:10 -0400 Subject: [PATCH 117/190] add relations from genomics report to specimen, variant and region studied --- chord_metadata_service/mcode/descriptions.py | 3 +++ chord_metadata_service/mcode/models.py | 6 ++++++ chord_metadata_service/mcode/schemas.py | 12 ++++++++++++ 3 files changed, 21 insertions(+) diff --git a/chord_metadata_service/mcode/descriptions.py b/chord_metadata_service/mcode/descriptions.py index d9b6bf369..4a760efea 100644 --- a/chord_metadata_service/mcode/descriptions.py +++ b/chord_metadata_service/mcode/descriptions.py @@ -66,6 +66,9 @@ "Accepted value sets: LOINC, GTR.", "performing_organization_name": "The name of the organization producing the genomics report.", "issued": "The date/time this report was issued.", + "genetic_specimen": "List of related genetic specimens.", + "genetic_variant": "Related genetic variant.", + "genomic_region_studied": "Related genomic region studied.", **EXTRA_PROPERTIES } } diff --git a/chord_metadata_service/mcode/models.py b/chord_metadata_service/mcode/models.py index 0c9fe4e7f..31c665f97 100644 --- a/chord_metadata_service/mcode/models.py +++ b/chord_metadata_service/mcode/models.py @@ -118,6 +118,12 @@ class GenomicsReport(models.Model, IndexableMixin): performing_organization_name = models.CharField( max_length=200, blank=True, help_text=rec_help(d.GENOMICS_REPORT, "performing_organization_name")) issued = models.DateTimeField(help_text=rec_help(d.GENOMICS_REPORT, "issued")) + genetic_specimen = models.ManyToManyField(GeneticSpecimen, blank=True, + help_text=rec_help(d.GENOMICS_REPORT, "genetic_specimen")) + genetic_variant = models.ForeignKey(CancerGeneticVariant, blank=True, null=True, on_delete=models.SET_NULL, + help_text=rec_help(d.GENOMICS_REPORT, "genetic_variant")) + genomic_region_studied = models.ForeignKey(GenomicRegionStudied, blank=True, null=True, on_delete=models.SET_NULL, + help_text=rec_help(d.GENOMICS_REPORT, "genomic_region_studied")) extra_properties = JSONField(blank=True, null=True, help_text=rec_help(d.GENOMICS_REPORT, "extra_properties")) created = models.DateTimeField(auto_now=True) diff --git a/chord_metadata_service/mcode/schemas.py b/chord_metadata_service/mcode/schemas.py index 61e78f5f8..e871caee1 100644 --- a/chord_metadata_service/mcode/schemas.py +++ b/chord_metadata_service/mcode/schemas.py @@ -235,6 +235,18 @@ "type": "string", "format": "date-time" }, + "genetic_specimen": { + "type": "array", + "items": { + "type": "string" + } + }, + "genetic_variant": { + "type": "string" + }, + "genomic_region_studied": { + "type": "string" + }, "extra_properties": EXTRA_PROPERTIES_SCHEMA }, "required": ["id", "code", "issued"] From b4e8359b31f14c598434d025fcd586e9c0adb280 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Wed, 10 Jun 2020 21:36:28 -0400 Subject: [PATCH 118/190] change labs/vital class --- chord_metadata_service/mcode/descriptions.py | 12 +--- chord_metadata_service/mcode/models.py | 24 ++----- chord_metadata_service/mcode/schemas.py | 67 ++++++------------- .../mcode/tests/constants.py | 30 +-------- .../mcode/tests/test_models.py | 41 +++++------- chord_metadata_service/mcode/validators.py | 2 +- 6 files changed, 50 insertions(+), 126 deletions(-) diff --git a/chord_metadata_service/mcode/descriptions.py b/chord_metadata_service/mcode/descriptions.py index 4a760efea..918c37617 100644 --- a/chord_metadata_service/mcode/descriptions.py +++ b/chord_metadata_service/mcode/descriptions.py @@ -78,16 +78,8 @@ "properties": { "id": "An arbitrary identifier for the labs/vital tests.", "individual": "The individual who is the subject of the tests.", - "body_height": "The patient\'s height.", - "body_weight": "The patient\'s weight.", - "cbc_with_auto_differential_panel": "Reference to a laboratory observation in the CBC with Auto Differential" - "Panel test.", - "comprehensive_metabolic_2000": "Reference to a laboratory observation in the CMP 2000 test.", - "blood_pressure_diastolic": "The blood pressure after the contraction of the heart while the chambers of " - "the heart refill with blood, when the pressure is lowest.", - "blood_pressure_systolic": "The blood pressure during the contraction of the left ventricle of the heart, " - "when blood pressure is at its highest.", - "tumor_marker_test": "An ontology or controlled vocabulary term to identify tumor marker test.", + "tumor_marker_code": "A code identifying the type of tumor marker test.", + "tumor_marker_data_value": "The result of a tumor marker test.", **EXTRA_PROPERTIES } } diff --git a/chord_metadata_service/mcode/models.py b/chord_metadata_service/mcode/models.py index 31c665f97..9dc560fb3 100644 --- a/chord_metadata_service/mcode/models.py +++ b/chord_metadata_service/mcode/models.py @@ -9,7 +9,7 @@ from chord_metadata_service.restapi.validators import ontology_validator, ontology_list_validator from .validators import ( quantity_validator, - tumor_marker_test_validator, + tumor_marker_data_value_validator, complex_ontology_validator, time_or_period_validator ) @@ -148,20 +148,11 @@ class LabsVital(models.Model, IndexableMixin): help_text=rec_help(d.LABS_VITAL, "id")) individual = models.ForeignKey(Individual, on_delete=models.CASCADE, help_text=rec_help(d.LABS_VITAL, "individual")) - body_height = JSONField(validators=[quantity_validator], help_text=rec_help(d.LABS_VITAL, "body_height")) - body_weight = JSONField(validators=[quantity_validator], help_text=rec_help(d.LABS_VITAL, "body_weight")) - # corresponds to DiagnosticReport.result - complex element, probably should be changed to Array of json - cbc_with_auto_differential_panel = ArrayField(models.CharField(max_length=200), blank=True, null=True, - help_text=rec_help(d.LABS_VITAL, "cbc_with_auto_differential_panel")) - comprehensive_metabolic_2000 = ArrayField(models.CharField(max_length=200), blank=True, null=True, - help_text=rec_help(d.LABS_VITAL, "comprehensive_metabolic_2000")) - blood_pressure_diastolic = JSONField(blank=True, null=True, validators=[quantity_validator], - help_text=rec_help(d.LABS_VITAL, "blood_pressure_diastolic")) - blood_pressure_systolic = JSONField(blank=True, null=True, validators=[quantity_validator], - help_text=rec_help(d.LABS_VITAL, "blood_pressure_systolic")) # TODO Change CodeableConcept to Ontology class - tumor_marker_test = JSONField(validators=[tumor_marker_test_validator], - help_text=rec_help(d.LABS_VITAL, "tumor_marker_test")) + tumor_marker_code = JSONField(validators=[ontology_validator], + help_text=rec_help(d.LABS_VITAL, "tumor_marker_code")) + tumor_marker_data_value = JSONField(blank=True, null=True, validators=[tumor_marker_data_value_validator], + help_text=rec_help(d.LABS_VITAL, "tumor_marker_data_value")) extra_properties = JSONField(blank=True, null=True, help_text=rec_help(d.LABS_VITAL, "extra_properties")) created = models.DateTimeField(auto_now=True) @@ -173,11 +164,6 @@ class Meta: def __str__(self): return str(self.id) - def clean(self): - if not (self.blood_pressure_diastolic or self.blood_pressure_systolic): - raise ValidationError('At least one of the following must be reported: Systolic Blood Pressure or' - 'Diastolic Blood Pressure.') - ################################# Disease ################################# diff --git a/chord_metadata_service/mcode/schemas.py b/chord_metadata_service/mcode/schemas.py index e871caee1..269ed08c6 100644 --- a/chord_metadata_service/mcode/schemas.py +++ b/chord_metadata_service/mcode/schemas.py @@ -4,7 +4,6 @@ from chord_metadata_service.patients.schemas import INDIVIDUAL_SCHEMA from .descriptions import * - ################################## mCode/FHIR based schemas ################################## ### FHIR datatypes @@ -37,7 +36,6 @@ "additionalProperties": False } - # FHIR CodeableConcept https://www.hl7.org/fhir/datatypes.html#CodeableConcept CODEABLE_CONCEPT = { "$schema": "http://json-schema.org/draft-07/schema#", @@ -66,7 +64,6 @@ "additionalProperties": False } - # FHIR Period https://www.hl7.org/fhir/datatypes.html#Period PERIOD = { "$schema": "http://json-schema.org/draft-07/schema#", @@ -87,7 +84,6 @@ "additionalProperties": False } - # FHIR Ratio https://www.hl7.org/fhir/datatypes.html#Ratio RATIO = { "$schema": "http://json-schema.org/draft-07/schema#", @@ -102,7 +98,6 @@ "additionalProperties": False } - ### FHIR based mCode elements TIME_OR_PERIOD = { @@ -122,6 +117,24 @@ "additionalProperties": False } +TUMOR_MARKER_DATA_VALUE = { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "chord_metadata_service:tumor_marker_data_value", + "title": "Tumor marker data value", + "description": "Tumor marker data value schema.", + "type": "object", + "properties": { + "value": { + "anyOf": [ + ONTOLOGY_CLASS, + QUANTITY, + RATIO + ] + } + }, + "additionalProperties": False +} + # TODO this is definitely should be changed, fhir datatypes are too complex use Ontology_ class COMPLEX_ONTOLOGY = customize_schema( first_typeof=ONTOLOGY_CLASS, @@ -134,23 +147,6 @@ required=["data_value"] ) -# TODO this is definitely should be changed, fhir datatypes are too complex use Ontology_ class -TUMOR_MARKER_TEST = customize_schema( - first_typeof=ONTOLOGY_CLASS, - second_typeof={ - "anyOf": [ - ONTOLOGY_CLASS, - QUANTITY, - RATIO - ] - }, - first_property="code", - second_property="data_value", - schema_id="chord_metadata_service:tumor_marker_test", - title="Tumor marker test", - description="Tumor marker test schema.", - required=["code"] -) ############################## Metadata service mCode based schemas ############################## @@ -252,36 +248,20 @@ "required": ["id", "code", "issued"] }, GENOMICS_REPORT) - MCODE_LABS_VITAL_SCHEMA = describe_schema({ "type": "object", "properties": { "id": { "type": "string" }, - "subject": { + "individual": { "type": "string" }, - "body_height": QUANTITY, - "body_weight": QUANTITY, - "cbc_with_auto_differential_panel": { - "type": "array", - "items": { - "type": "string" - } - }, - "comprehensive_metabolic_2000": { - "type": "array", - "items": { - "type": "string" - } - }, - "blood_pressure_diastolic": QUANTITY, - "blood_pressure_systolic": QUANTITY, - "tumor_marker_test": TUMOR_MARKER_TEST, + "tumor_marker_code": ONTOLOGY_CLASS, + "tumor_marker_data_value": TUMOR_MARKER_DATA_VALUE, "extra_properties": EXTRA_PROPERTIES_SCHEMA }, - "required": ["id", "individual", "body_height", "body_weight", "tumor_marker_test"] + "required": ["id", "individual", "tumor_marker_code"] }, LABS_VITAL) # TODO check required inb data dictionary @@ -311,7 +291,6 @@ "required": ["id", "condition_type", "code"] }, LABS_VITAL) - MCODE_TNM_STAGING_SCHEMA = describe_schema({ "type": "object", "properties": { @@ -345,7 +324,6 @@ ] }, TNM_STAGING) - MCODE_CANCER_RELATED_PROCEDURE_SCHEMA = describe_schema({ "type": "object", "properties": { @@ -368,7 +346,6 @@ "required": ["id", "procedure_type", "code", "occurence_time_or_period"] }, CANCER_RELATED_PROCEDURE) - MCODE_MEDICATION_STATEMENT_SCHEMA = describe_schema({ "type": "object", "properties": { diff --git a/chord_metadata_service/mcode/tests/constants.py b/chord_metadata_service/mcode/tests/constants.py index 61fd10ae3..c0dad878a 100644 --- a/chord_metadata_service/mcode/tests/constants.py +++ b/chord_metadata_service/mcode/tests/constants.py @@ -128,33 +128,9 @@ def valid_genetic_report(): def valid_labs_vital(individual): return { "id": "labs_vital:01", - "body_height": { - "value": 1.70, - "unit": "m" - }, - "body_weight": { - "value": 60, - "unit": "kg" - }, - "cbc_with_auto_differential_panel": ["Test"], - "comprehensive_metabolic_2000": ["Test"], - "blood_pressure_diastolic": { - "value": 80, - "unit": "mmHg" - }, - "blood_pressure_systolic": { - "value": 120, - "unit": "mmHg" - }, - "tumor_marker_test": { - "code": { - "id": "50610-5", - "label": "Alpha-1-Fetoprotein" - }, - "data_value": { - "value": 10, - "unit": "ng/mL" - } + "tumor_marker_code": { + "id": "50610-5", + "label": "Alpha-1-Fetoprotein" }, "individual": individual, } diff --git a/chord_metadata_service/mcode/tests/test_models.py b/chord_metadata_service/mcode/tests/test_models.py index 8cb0d9c95..9799bef77 100644 --- a/chord_metadata_service/mcode/tests/test_models.py +++ b/chord_metadata_service/mcode/tests/test_models.py @@ -27,30 +27,23 @@ def setUp(self): def test_labs_vital(self): labs_vital = LabsVital.objects.get(id='labs_vital:01') - self.assertEqual(labs_vital.body_height['value'], 1.70) - self.assertEqual(labs_vital.body_height['unit'], 'm') - self.assertEqual(labs_vital.body_weight['value'], 60) - self.assertEqual(labs_vital.blood_pressure_diastolic['value'], 80) - self.assertEqual(labs_vital.blood_pressure_systolic['value'], 120) - self.assertIsInstance(labs_vital.tumor_marker_test, dict) - self.assertIsInstance(labs_vital.tumor_marker_test['code'], dict) - self.assertEqual(labs_vital.tumor_marker_test['data_value']['value'], 10) - - def test_validation(self): - invalid_obj = valid_labs_vital(self.individual) - invalid_obj["id"] = "labs_vital:02" - invalid_obj["tumor_marker_test"]["code"] = { - "coding": [ - { - "code": "50610-5", - "display": "Alpha-1-Fetoprotein", - "system": "loinc.org" - } - ] - } - invalid = LabsVital.objects.create(**invalid_obj) - with self.assertRaises(serializers.ValidationError): - invalid.full_clean() + self.assertIsInstance(labs_vital.tumor_marker_code, dict) + + # def test_validation(self): + # invalid_obj = valid_labs_vital(self.individual) + # invalid_obj["id"] = "labs_vital:02" + # invalid_obj["tumor_marker_test"]["code"] = { + # "coding": [ + # { + # "code": "50610-5", + # "display": "Alpha-1-Fetoprotein", + # "system": "loinc.org" + # } + # ] + # } + # invalid = LabsVital.objects.create(**invalid_obj) + # with self.assertRaises(serializers.ValidationError): + # invalid.full_clean() class CancerConditionTest(TestCase): diff --git a/chord_metadata_service/mcode/validators.py b/chord_metadata_service/mcode/validators.py index 9fcb9cea1..d403806b1 100644 --- a/chord_metadata_service/mcode/validators.py +++ b/chord_metadata_service/mcode/validators.py @@ -3,6 +3,6 @@ quantity_validator = JsonSchemaValidator(schema=QUANTITY, formats=['uri']) -tumor_marker_test_validator = JsonSchemaValidator(schema=TUMOR_MARKER_TEST) +tumor_marker_data_value_validator = JsonSchemaValidator(schema=TUMOR_MARKER_DATA_VALUE) complex_ontology_validator = JsonSchemaValidator(schema=COMPLEX_ONTOLOGY, formats=['uri']) time_or_period_validator = JsonSchemaValidator(schema=TIME_OR_PERIOD, formats=['date-time']) From 01a322234f2f3fb85cbcb197be1cdc2385ef3ce9 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Wed, 10 Jun 2020 22:36:46 -0400 Subject: [PATCH 119/190] changes to medication statement and mcodepacket --- chord_metadata_service/mcode/descriptions.py | 13 +++++++---- chord_metadata_service/mcode/models.py | 17 +++++++++----- chord_metadata_service/mcode/schemas.py | 22 ++++++++++++------- .../mcode/tests/constants.py | 11 ++-------- .../mcode/tests/test_models.py | 5 ++--- examples/mcode_example.json | 11 ++-------- 6 files changed, 41 insertions(+), 38 deletions(-) diff --git a/chord_metadata_service/mcode/descriptions.py b/chord_metadata_service/mcode/descriptions.py index 918c37617..5c77e59cc 100644 --- a/chord_metadata_service/mcode/descriptions.py +++ b/chord_metadata_service/mcode/descriptions.py @@ -129,11 +129,14 @@ "description": "Description of radiological treatment or surgical action addressing a cancer condition.", "properties": { "id": "An arbitrary identifier for the procedure.", - "procedure_type": "Type of cancer related procedure: radion or surgical.", + "procedure_type": "Type of cancer related procedure: radiation or surgical.", "code": "Code for the procedure performed.", - "occurence_time_or_period": "The date/time that a procedure was performed.", - "target_body_site": "The body location(s) where the procedure was performed.", + "body_site": "The body location(s) where the procedure was performed.", + "laterality": "Body side of the body location, if needed to distinguish from a similar location " + "on the other side of the body.", "treatment_intent": "The purpose of a treatment.", + "reason_code": "The explanation or justification for why the surgical procedure was performed.", + "reason_reference": "Reference to a primary or secondary cancer condition.", **EXTRA_PROPERTIES } } @@ -148,7 +151,6 @@ "treatment_intent": "The purpose of a treatment. Accepted ontologies: SNOMED CT.", "start_date": "The start date/time of the medication.", "end_date": "The end date/time of the medication.", - "date_time": "The date/time the medication was administered.", **EXTRA_PROPERTIES } } @@ -162,6 +164,9 @@ "cancer_condition": "An Individual's cancer condition.", "cancer_related_procedures": "A radiological or surgical procedures addressing a cancer condition.", "medication_statement": "Medication treatment addressed to an Individual.", + "date_of_death": "An indication that the patient is no longer living, given by a date of death or boolean.", + "cancer_disease_status": "A clinician's qualitative judgment on the current trend of the cancer, e.g., " + "whether it is stable, worsening (progressing), or improving (responding).", **EXTRA_PROPERTIES } } diff --git a/chord_metadata_service/mcode/models.py b/chord_metadata_service/mcode/models.py index 9dc560fb3..7ce4527ec 100644 --- a/chord_metadata_service/mcode/models.py +++ b/chord_metadata_service/mcode/models.py @@ -254,12 +254,17 @@ class CancerRelatedProcedure(models.Model, IndexableMixin): procedure_type = models.CharField(choices=PROCEDURE_TYPES, max_length=200, help_text=rec_help(d.CANCER_RELATED_PROCEDURE, "procedure_type")) code = JSONField(validators=[ontology_validator], help_text=rec_help(d.CANCER_RELATED_PROCEDURE, "code")) - occurence_time_or_period = JSONField(validators=[time_or_period_validator], - help_text=rec_help(d.CANCER_RELATED_PROCEDURE, "occurence_time_or_period")) - target_body_site = JSONField(null=True, validators=[ontology_list_validator], - help_text=rec_help(d.CANCER_RELATED_PROCEDURE, 'target_body_site')) + body_site = JSONField(null=True, validators=[ontology_list_validator], + help_text=rec_help(d.CANCER_RELATED_PROCEDURE, 'body_site')) + laterality = JSONField(blank=True, null=True, validators=[ontology_validator], + help_text=rec_help(d.CANCER_RELATED_PROCEDURE, "laterality")) treatment_intent = JSONField(blank=True, null=True, validators=[ontology_validator], help_text=rec_help(d.CANCER_RELATED_PROCEDURE, "treatment_intent")) + # Only for Surgical Procedure + reason_code = JSONField(blank=True, null=True, validators=[ontology_validator], + help_text=rec_help(d.CANCER_RELATED_PROCEDURE, "reason_code")) + reason_reference = models.ManyToManyField(CancerCondition, blank=True, + help_text=rec_help(d.CANCER_RELATED_PROCEDURE, "reason_reference")) extra_properties = JSONField(blank=True, null=True, help_text=rec_help(d.CANCER_RELATED_PROCEDURE, "extra_properties")) created = models.DateTimeField(auto_now=True) @@ -290,7 +295,6 @@ class MedicationStatement(models.Model, IndexableMixin): help_text=rec_help(d.MEDICATION_STATEMENT, "treatment_intent")) start_date = models.DateTimeField(blank=True, null=True, help_text=rec_help(d.MEDICATION_STATEMENT, "start_date")) end_date = models.DateTimeField(blank=True, null=True, help_text=rec_help(d.MEDICATION_STATEMENT, "end_date")) - date_time = models.DateTimeField(blank=True, null=True, help_text=rec_help(d.MEDICATION_STATEMENT, "date_time")) extra_properties = JSONField(blank=True, null=True, help_text=rec_help(d.MEDICATION_STATEMENT, "extra_properties")) created = models.DateTimeField(auto_now=True) @@ -319,6 +323,9 @@ class MCodePacket(models.Model, IndexableMixin): help_text=rec_help(d.MCODEPACKET, "cancer_related_procedures")) medication_statement = models.ForeignKey(MedicationStatement, blank=True, null=True, on_delete=models.SET_NULL, help_text=rec_help(d.MCODEPACKET, "medication_statement")) + date_of_death = models.CharField(blank=True, help_text=rec_help(d.MCODEPACKET, "date_of_death")) + cancer_disease_status = JSONField(blank=True, null=True, validators=[ontology_validator], + help_text=rec_help(d.MCODEPACKET, "cancer_disease_status")) extra_properties = JSONField(blank=True, null=True, help_text=rec_help(d.MCODEPACKET, "extra_properties")) created = models.DateTimeField(auto_now=True) diff --git a/chord_metadata_service/mcode/schemas.py b/chord_metadata_service/mcode/schemas.py index 269ed08c6..c2453c25e 100644 --- a/chord_metadata_service/mcode/schemas.py +++ b/chord_metadata_service/mcode/schemas.py @@ -338,12 +338,19 @@ ] }, "code": ONTOLOGY_CLASS, - "occurence_time_or_period": TIME_OR_PERIOD, - "target_body_site": ONTOLOGY_CLASS_LIST, + "body_site": ONTOLOGY_CLASS_LIST, + "laterality": ONTOLOGY_CLASS, "treatment_intent": ONTOLOGY_CLASS, + "reason_code": ONTOLOGY_CLASS, + "reason_reference": { + "type": "array", + "items": { + "type": "string" + } + }, "extra_properties": EXTRA_PROPERTIES_SCHEMA }, - "required": ["id", "procedure_type", "code", "occurence_time_or_period"] + "required": ["id", "procedure_type", "code"] }, CANCER_RELATED_PROCEDURE) MCODE_MEDICATION_STATEMENT_SCHEMA = describe_schema({ @@ -363,11 +370,6 @@ "type": "string", "format": "date-time" }, - "date_time": { - "type": "string", - "format": "date-time" - }, - "extra_properties": EXTRA_PROPERTIES_SCHEMA }, "required": ["id", "medication_code"] @@ -389,6 +391,10 @@ "cancer_condition": MCODE_CANCER_CONDITION_SCHEMA, "cancer_related_procedures": MCODE_CANCER_RELATED_PROCEDURE_SCHEMA, "medication_statement": MCODE_MEDICATION_STATEMENT_SCHEMA, + "date_of_death": { + "type": "string" + }, + "cancer_disease_status": ONTOLOGY_CLASS, "extra_properties": EXTRA_PROPERTIES_SCHEMA } }, MCODEPACKET) diff --git a/chord_metadata_service/mcode/tests/constants.py b/chord_metadata_service/mcode/tests/constants.py index c0dad878a..46ab4ce22 100644 --- a/chord_metadata_service/mcode/tests/constants.py +++ b/chord_metadata_service/mcode/tests/constants.py @@ -221,13 +221,7 @@ def valid_cancer_related_procedure(): "id": "33356009", "label": "Betatron teleradiotherapy (procedure)" }, - "occurence_time_or_period": { - "value": { - "start": "2018-11-13T20:20:39+00:00", - "end": "2019-04-13T20:20:39+00:00" - } - }, - "target_body_site": [ + "body_site": [ { "id": "74805009", "label": "Mammary gland sinus" @@ -258,6 +252,5 @@ def valid_medication_statement(): "label": "Curative - procedure intent" }, "start_date": "2018-11-13T20:20:39+00:00", - "end_date": "2019-04-13T20:20:39+00:00", - "date_time": "2019-04-13T20:20:39+00:00" + "end_date": "2019-04-13T20:20:39+00:00" } diff --git a/chord_metadata_service/mcode/tests/test_models.py b/chord_metadata_service/mcode/tests/test_models.py index 9799bef77..01c83c540 100644 --- a/chord_metadata_service/mcode/tests/test_models.py +++ b/chord_metadata_service/mcode/tests/test_models.py @@ -98,8 +98,7 @@ def test_cancer_related_procedure(self): cancer_related_procedure = CancerRelatedProcedure.objects.get(id='cancer_related_procedure:01') self.assertEqual(cancer_related_procedure.procedure_type, 'radiation') self.assertEqual(cancer_related_procedure.code['id'], '33356009') - self.assertEqual(cancer_related_procedure.occurence_time_or_period['value']['start'], '2018-11-13T20:20:39+00:00') - self.assertIsInstance(cancer_related_procedure.target_body_site, list) + self.assertIsInstance(cancer_related_procedure.body_site, list) self.assertEqual(cancer_related_procedure.treatment_intent['label'], 'Curative - procedure intent') @@ -116,5 +115,5 @@ def test_cancer_related_procedure(self): self.assertEqual(medication_statement.medication_code['id'], '92052') self.assertIsInstance(medication_statement.termination_reason, list) self.assertEqual(medication_statement.treatment_intent['label'], 'Curative - procedure intent') - for date in [medication_statement.start_date, medication_statement.end_date, medication_statement.date_time]: + for date in [medication_statement.start_date, medication_statement.end_date]: self.assertIsInstance(date, datetime.datetime) diff --git a/examples/mcode_example.json b/examples/mcode_example.json index d5a312ca2..db163d729 100644 --- a/examples/mcode_example.json +++ b/examples/mcode_example.json @@ -99,8 +99,7 @@ "label": "Curative - procedure intent" }, "start_date": "2018-11-13T20:20:39Z", - "end_date": "2019-04-13T20:20:39Z", - "date_time": "2019-04-13T20:20:39Z" + "end_date": "2019-04-13T20:20:39Z" }, "cancer_related_procedures": [ { @@ -110,13 +109,7 @@ "id": "33356009", "label": "Betatron teleradiotherapy (procedure)" }, - "occurence_time_or_period": { - "value": { - "end": "2019-04-13T20:20:39+00:00", - "start": "2018-11-13T20:20:39+00:00" - } - }, - "target_body_site": [ + "body_site": [ { "id": "74805009", "label": "Mammary gland sinus" From 1bdca5b5582760c2a78776e9a40271a2a16bae28 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Wed, 10 Jun 2020 22:43:20 -0400 Subject: [PATCH 120/190] small fixes --- chord_metadata_service/mcode/models.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/chord_metadata_service/mcode/models.py b/chord_metadata_service/mcode/models.py index 7ce4527ec..7a1b251a8 100644 --- a/chord_metadata_service/mcode/models.py +++ b/chord_metadata_service/mcode/models.py @@ -19,14 +19,14 @@ class GeneticSpecimen(models.Model, IndexableMixin): """ Class to describe a biosample used for genomics testing or analysis. """ - id = models.CharField(primary_key=True, max_length=200, help_text=rec_help(d.GENOMIC_SPECIMEN, "id")) - specimen_type = JSONField(validators=[ontology_validator], help_text=rec_help(d.GENOMIC_SPECIMEN, "specimen_type")) + id = models.CharField(primary_key=True, max_length=200, help_text=rec_help(d.GENETIC_SPECIMEN, "id")) + specimen_type = JSONField(validators=[ontology_validator], help_text=rec_help(d.GENETIC_SPECIMEN, "specimen_type")) collection_body = JSONField(blank=True, null=True, validators=[ontology_validator], - help_text=rec_help(d.GENOMIC_SPECIMEN, "collection_body")) + help_text=rec_help(d.GENETIC_SPECIMEN, "collection_body")) laterality = JSONField(blank=True, null=True, validators=[ontology_validator], - help_text=rec_help(d.GENOMIC_SPECIMEN, "laterality")) + help_text=rec_help(d.GENETIC_SPECIMEN, "laterality")) extra_properties = JSONField(blank=True, null=True, - help_text=rec_help(d.GENOMIC_SPECIMEN, "extra_properties")) + help_text=rec_help(d.GENETIC_SPECIMEN, "extra_properties")) created = models.DateTimeField(auto_now=True) updated = models.DateTimeField(auto_now_add=True) @@ -54,8 +54,8 @@ class CancerGeneticVariant(models.Model, IndexableMixin): help_text=rec_help(d.CANCER_GENETIC_VARIANT, "cytogenetic_location")) cytogenetic_nomenclature = JSONField(blank=True, null=True, validators=[ontology_validator], help_text=rec_help(d.CANCER_GENETIC_VARIANT, "cytogenetic_nomenclature")) - gene_studied = models.ManyToManyField(Gene, blank=True, on_delete=models.SET_NULL, - help_text=rec_help(d.CANCER_GENETIC_VARIANT, "gene_studied")) + gene_studied = models.ManyToManyField(Gene, blank=True, + help_text=rec_help(d.CANCER_GENETIC_VARIANT, "gene_studied")) genomic_dna_change = JSONField(blank=True, null=True, validators=[ontology_validator], help_text=rec_help(d.CANCER_GENETIC_VARIANT, "genomic_dna_change")) genomic_source_class = JSONField(blank=True, null=True, validators=[ontology_validator], @@ -323,7 +323,7 @@ class MCodePacket(models.Model, IndexableMixin): help_text=rec_help(d.MCODEPACKET, "cancer_related_procedures")) medication_statement = models.ForeignKey(MedicationStatement, blank=True, null=True, on_delete=models.SET_NULL, help_text=rec_help(d.MCODEPACKET, "medication_statement")) - date_of_death = models.CharField(blank=True, help_text=rec_help(d.MCODEPACKET, "date_of_death")) + date_of_death = models.CharField(max_length=200, blank=True, help_text=rec_help(d.MCODEPACKET, "date_of_death")) cancer_disease_status = JSONField(blank=True, null=True, validators=[ontology_validator], help_text=rec_help(d.MCODEPACKET, "cancer_disease_status")) extra_properties = JSONField(blank=True, null=True, From dd90c7cbcf17bca7120c1a0e3b75aa6344bd268f Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Wed, 10 Jun 2020 22:57:24 -0400 Subject: [PATCH 121/190] add migrations for mcode --- .../migrations/0006_auto_20200610_2248.py | 227 ++++++++++++++++++ .../migrations/0007_auto_20200610_2254.py | 19 ++ chord_metadata_service/mcode/models.py | 3 +- 3 files changed, 248 insertions(+), 1 deletion(-) create mode 100644 chord_metadata_service/mcode/migrations/0006_auto_20200610_2248.py create mode 100644 chord_metadata_service/mcode/migrations/0007_auto_20200610_2254.py diff --git a/chord_metadata_service/mcode/migrations/0006_auto_20200610_2248.py b/chord_metadata_service/mcode/migrations/0006_auto_20200610_2248.py new file mode 100644 index 000000000..a7c60bd94 --- /dev/null +++ b/chord_metadata_service/mcode/migrations/0006_auto_20200610_2248.py @@ -0,0 +1,227 @@ +# Generated by Django 2.2.13 on 2020-06-11 02:48 + +import chord_metadata_service.restapi.models +import chord_metadata_service.restapi.validators +import datetime +import django.contrib.postgres.fields +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('phenopackets', '0012_auto_20200525_2116'), + ('mcode', '0005_auto_20200519_1538'), + ] + + operations = [ + migrations.CreateModel( + name='CancerGeneticVariant', + fields=[ + ('id', models.CharField(help_text='An arbitrary identifier for the cancer genetic variant.', max_length=200, primary_key=True, serialize=False)), + ('data_value', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The overall result of the genetic test; specifically, whether a variant is present, absent, no call, or indeterminant.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), + ('method', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The method used to perform the genetic test.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), + ('amino_acid_change', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The symbolic representation of an amino acid variant reported using HGVS nomenclature (pHGVS).', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), + ('amino_acid_change_type', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The type of change related to the amino acid variant.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), + ('cytogenetic_location', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The cytogenetic (chromosome) location.', null=True)), + ('cytogenetic_nomenclature', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The cytogenetic (chromosome) location, represented using the International System for Human Cytogenetic Nomenclature (ISCN).', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), + ('genomic_dna_change', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The symbolic representation of a genetic structural variant reported using HGVS nomenclature (gHGVS).', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), + ('genomic_source_class', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The genomic class of the specimen being analyzed, for example, germline for inherited genome, somatic for cancer genome, and prenatal for fetal genome.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), + ('variation_code', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The variation ID assigned by ClinVar.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)])), + ('extra_properties', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema.', null=True)), + ('created', models.DateTimeField(auto_now=True)), + ('updated', models.DateTimeField(auto_now_add=True)), + ('gene_studied', models.ManyToManyField(blank=True, help_text='A gene targeted for mutation analysis, identified in HUGO Gene Nomenclature Committee (HGNC) notation.', to='phenopackets.Gene')), + ], + options={ + 'ordering': ['id'], + }, + bases=(models.Model, chord_metadata_service.restapi.models.IndexableMixin), + ), + migrations.CreateModel( + name='GeneticSpecimen', + fields=[ + ('id', models.CharField(help_text='An arbitrary identifier for the genetic specimen.', max_length=200, primary_key=True, serialize=False)), + ('specimen_type', django.contrib.postgres.fields.jsonb.JSONField(help_text='The kind of material that forms the specimen.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), + ('collection_body', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The anatomical collection site.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), + ('laterality', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Body side of the collection site, if needed to distinguish from a similar location on the other side of the body.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), + ('extra_properties', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema.', null=True)), + ('created', models.DateTimeField(auto_now=True)), + ('updated', models.DateTimeField(auto_now_add=True)), + ], + options={ + 'ordering': ['id'], + }, + bases=(models.Model, chord_metadata_service.restapi.models.IndexableMixin), + ), + migrations.CreateModel( + name='GenomicRegionStudied', + fields=[ + ('id', models.CharField(help_text='An arbitrary identifier for the genomic region studied.', max_length=200, primary_key=True, serialize=False)), + ('dna_ranges_examined', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The range(s) of the DNA sequence examined.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)])), + ('dna_region_description', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(help_text='The description for the DNA region studied in the genomics report.', max_length=100), blank=True, default=list, size=None)), + ('gene_mutation', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The gene mutations tested for in blood or tissue by molecular genetics methods.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)])), + ('gene_studied', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The ID for the gene studied.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)])), + ('genomic_reference_sequence_id', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Range(s) of DNA sequence examined.', null=True)), + ('genomic_region_coordinate_system', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The method of counting along the genome.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), + ('extra_properties', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema.', null=True)), + ('created', models.DateTimeField(auto_now=True)), + ('updated', models.DateTimeField(auto_now_add=True)), + ], + options={ + 'ordering': ['id'], + }, + bases=(models.Model, chord_metadata_service.restapi.models.IndexableMixin), + ), + migrations.RemoveField( + model_name='geneticvarianttested', + name='gene_studied', + ), + migrations.RenameField( + model_name='cancercondition', + old_name='body_location_code', + new_name='body_site', + ), + migrations.RenameField( + model_name='cancercondition', + old_name='condition_code', + new_name='code', + ), + migrations.RenameField( + model_name='cancerrelatedprocedure', + old_name='target_body_site', + new_name='body_site', + ), + migrations.RenameField( + model_name='genomicsreport', + old_name='test_name', + new_name='code', + ), + migrations.RemoveField( + model_name='cancerrelatedprocedure', + name='occurence_time_or_period', + ), + migrations.RemoveField( + model_name='genomicsreport', + name='genetic_variant_found', + ), + migrations.RemoveField( + model_name='genomicsreport', + name='genetic_variant_tested', + ), + migrations.RemoveField( + model_name='genomicsreport', + name='specimen_type', + ), + migrations.RemoveField( + model_name='labsvital', + name='blood_pressure_diastolic', + ), + migrations.RemoveField( + model_name='labsvital', + name='blood_pressure_systolic', + ), + migrations.RemoveField( + model_name='labsvital', + name='body_height', + ), + migrations.RemoveField( + model_name='labsvital', + name='body_weight', + ), + migrations.RemoveField( + model_name='labsvital', + name='cbc_with_auto_differential_panel', + ), + migrations.RemoveField( + model_name='labsvital', + name='comprehensive_metabolic_2000', + ), + migrations.RemoveField( + model_name='labsvital', + name='tumor_marker_test', + ), + migrations.RemoveField( + model_name='medicationstatement', + name='date_time', + ), + migrations.AddField( + model_name='cancercondition', + name='laterality', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Body side of the body location, if needed to distinguish from a similar location on the other side of the body.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AddField( + model_name='cancercondition', + name='verification_status', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='A flag indicating whether the condition is unconfirmed, provisional, differential, confirmed, refuted, or entered-in-error.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AddField( + model_name='cancerrelatedprocedure', + name='laterality', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Body side of the body location, if needed to distinguish from a similar location on the other side of the body.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AddField( + model_name='cancerrelatedprocedure', + name='reason_code', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The explanation or justification for why the surgical procedure was performed.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AddField( + model_name='cancerrelatedprocedure', + name='reason_reference', + field=models.ManyToManyField(blank=True, help_text='Reference to a primary or secondary cancer condition.', to='mcode.CancerCondition'), + ), + migrations.AddField( + model_name='genomicsreport', + name='issued', + field=models.DateTimeField(default=datetime.datetime.now, help_text='The date/time this report was issued.'), + ), + migrations.AddField( + model_name='labsvital', + name='tumor_marker_code', + field=django.contrib.postgres.fields.jsonb.JSONField(default=None, help_text='A code identifying the type of tumor marker test.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + preserve_default=False, + ), + migrations.AddField( + model_name='labsvital', + name='tumor_marker_data_value', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The result of a tumor marker test.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:tumor_marker_data_value', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Tumor marker data value schema.', 'properties': {'value': {'anyOf': [{'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, {'$id': 'chord_metadata_service:quantity_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema for the datatype Quantity.', 'properties': {'code': {'type': 'string'}, 'comparator': {'enum': ['<', '>', '<=', '>=', '=']}, 'system': {'format': 'uri', 'type': 'string'}, 'unit': {'type': 'string'}, 'value': {'type': 'number'}}, 'title': 'Quantity schema', 'type': 'object'}, {'$id': 'chord_metadata_service:ratio', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Ratio schema.', 'properties': {'denominator': {'$id': 'chord_metadata_service:quantity_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema for the datatype Quantity.', 'properties': {'code': {'type': 'string'}, 'comparator': {'enum': ['<', '>', '<=', '>=', '=']}, 'system': {'format': 'uri', 'type': 'string'}, 'unit': {'type': 'string'}, 'value': {'type': 'number'}}, 'title': 'Quantity schema', 'type': 'object'}, 'numerator': {'$id': 'chord_metadata_service:quantity_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema for the datatype Quantity.', 'properties': {'code': {'type': 'string'}, 'comparator': {'enum': ['<', '>', '<=', '>=', '=']}, 'system': {'format': 'uri', 'type': 'string'}, 'unit': {'type': 'string'}, 'value': {'type': 'number'}}, 'title': 'Quantity schema', 'type': 'object'}}, 'title': 'Ratio', 'type': 'object'}]}}, 'title': 'Tumor marker data value', 'type': 'object'}, formats=None)]), + ), + migrations.AddField( + model_name='mcodepacket', + name='cancer_disease_status', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text="A clinician's qualitative judgment on the current trend of the cancer, e.g., whether it is stable, worsening (progressing), or improving (responding).", null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), + ), + migrations.AddField( + model_name='mcodepacket', + name='date_of_death', + field=models.CharField(blank=True, help_text='An indication that the patient is no longer living, given by a date of death or boolean.', max_length=200), + ), + migrations.AlterField( + model_name='cancerrelatedprocedure', + name='procedure_type', + field=models.CharField(choices=[('radiation', 'radiation'), ('surgical', 'surgical')], help_text='Type of cancer related procedure: radiation or surgical.', max_length=200), + ), + migrations.DeleteModel( + name='GeneticVariantFound', + ), + migrations.DeleteModel( + name='GeneticVariantTested', + ), + migrations.AddField( + model_name='genomicsreport', + name='genetic_specimen', + field=models.ManyToManyField(blank=True, help_text='List of related genetic specimens.', to='mcode.GeneticSpecimen'), + ), + migrations.AddField( + model_name='genomicsreport', + name='genetic_variant', + field=models.ForeignKey(blank=True, help_text='Related genetic variant.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='mcode.CancerGeneticVariant'), + ), + migrations.AddField( + model_name='genomicsreport', + name='genomic_region_studied', + field=models.ForeignKey(blank=True, help_text='Related genomic region studied.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='mcode.GenomicRegionStudied'), + ), + ] diff --git a/chord_metadata_service/mcode/migrations/0007_auto_20200610_2254.py b/chord_metadata_service/mcode/migrations/0007_auto_20200610_2254.py new file mode 100644 index 000000000..d4b67970c --- /dev/null +++ b/chord_metadata_service/mcode/migrations/0007_auto_20200610_2254.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.13 on 2020-06-11 02:54 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('mcode', '0006_auto_20200610_2248'), + ] + + operations = [ + migrations.AlterField( + model_name='genomicsreport', + name='issued', + field=models.DateTimeField(default=django.utils.timezone.now, help_text='The date/time this report was issued.'), + ), + ] diff --git a/chord_metadata_service/mcode/models.py b/chord_metadata_service/mcode/models.py index 7a1b251a8..2a239d4f6 100644 --- a/chord_metadata_service/mcode/models.py +++ b/chord_metadata_service/mcode/models.py @@ -1,3 +1,4 @@ +from django.utils.timezone import now from django.db import models from django.contrib.postgres.fields import JSONField, ArrayField from chord_metadata_service.restapi.models import IndexableMixin @@ -117,7 +118,7 @@ class GenomicsReport(models.Model, IndexableMixin): code = JSONField(validators=[ontology_validator], help_text=rec_help(d.GENOMICS_REPORT, "code")) performing_organization_name = models.CharField( max_length=200, blank=True, help_text=rec_help(d.GENOMICS_REPORT, "performing_organization_name")) - issued = models.DateTimeField(help_text=rec_help(d.GENOMICS_REPORT, "issued")) + issued = models.DateTimeField(default=now, help_text=rec_help(d.GENOMICS_REPORT, "issued")) genetic_specimen = models.ManyToManyField(GeneticSpecimen, blank=True, help_text=rec_help(d.GENOMICS_REPORT, "genetic_specimen")) genetic_variant = models.ForeignKey(CancerGeneticVariant, blank=True, null=True, on_delete=models.SET_NULL, From 5e54a492c943586a711b58ac6e96a12b80124a83 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Wed, 10 Jun 2020 23:23:06 -0400 Subject: [PATCH 122/190] add new mcode classes to serializers, views, urls, admin add migration --- chord_metadata_service/mcode/admin.py | 18 ++++++++++++++ chord_metadata_service/mcode/api_views.py | 16 +++++++++++++ .../migrations/0008_auto_20200610_2320.py | 20 ++++++++++++++++ chord_metadata_service/mcode/models.py | 9 +++---- chord_metadata_service/mcode/schemas.py | 12 +++------- chord_metadata_service/mcode/serializers.py | 24 +++++++++++++++++++ chord_metadata_service/mcode/validators.py | 1 + chord_metadata_service/restapi/urls.py | 3 +++ 8 files changed, 88 insertions(+), 15 deletions(-) create mode 100644 chord_metadata_service/mcode/migrations/0008_auto_20200610_2320.py diff --git a/chord_metadata_service/mcode/admin.py b/chord_metadata_service/mcode/admin.py index 6eacc2ab2..5e52ada17 100644 --- a/chord_metadata_service/mcode/admin.py +++ b/chord_metadata_service/mcode/admin.py @@ -2,6 +2,21 @@ from .models import * +@admin.register(GeneticSpecimen) +class GeneticSpecimenAdmin(admin.ModelAdmin): + pass + + +@admin.register(CancerGeneticVariant) +class CancerGeneticVariantAdmin(admin.ModelAdmin): + pass + + +@admin.register(GenomicRegionStudied) +class GenomicRegionStudiedAdmin(admin.ModelAdmin): + pass + + @admin.register(GenomicsReport) class GenomicsReportAdmin(admin.ModelAdmin): pass @@ -32,3 +47,6 @@ class MedicationStatementAdmin(admin.ModelAdmin): pass +@admin.register(MCodePacket) +class MCodePacketAdmin(admin.ModelAdmin): + pass diff --git a/chord_metadata_service/mcode/api_views.py b/chord_metadata_service/mcode/api_views.py index 1acbdde72..38a167c85 100644 --- a/chord_metadata_service/mcode/api_views.py +++ b/chord_metadata_service/mcode/api_views.py @@ -15,6 +15,22 @@ class McodeModelViewSet(viewsets.ModelViewSet): pagination_class = LargeResultsSetPagination renderer_classes = (*api_settings.DEFAULT_RENDERER_CLASSES, PhenopacketsRenderer) + +class GeneticSpecimenViewSet(McodeModelViewSet): + queryset = GeneticSpecimen.objects.all() + serializer_class = GeneticSpecimenSerializer + + +class CancerGeneticVariantViewSet(McodeModelViewSet): + queryset = CancerGeneticVariant.objects.all() + serializer_class = CancerGeneticVariantSerializer + + +class GenomicRegionStudiedViewSet(McodeModelViewSet): + queryset = GenomicRegionStudied.objects.all() + serializer_class = GenomicRegionStudiedSerializer + + class GenomicsReportViewSet(McodeModelViewSet): queryset = GenomicsReport.objects.all() serializer_class = GenomicsReportSerializer diff --git a/chord_metadata_service/mcode/migrations/0008_auto_20200610_2320.py b/chord_metadata_service/mcode/migrations/0008_auto_20200610_2320.py new file mode 100644 index 000000000..de0404817 --- /dev/null +++ b/chord_metadata_service/mcode/migrations/0008_auto_20200610_2320.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.13 on 2020-06-11 03:20 + +import datetime +from django.db import migrations, models +from django.utils.timezone import utc + + +class Migration(migrations.Migration): + + dependencies = [ + ('mcode', '0007_auto_20200610_2254'), + ] + + operations = [ + migrations.AlterField( + model_name='genomicsreport', + name='issued', + field=models.DateTimeField(default=datetime.datetime(2020, 6, 11, 3, 20, 6, 606068, tzinfo=utc), help_text='The date/time this report was issued.'), + ), + ] diff --git a/chord_metadata_service/mcode/models.py b/chord_metadata_service/mcode/models.py index 2a239d4f6..7d0e6af29 100644 --- a/chord_metadata_service/mcode/models.py +++ b/chord_metadata_service/mcode/models.py @@ -1,18 +1,15 @@ -from django.utils.timezone import now +from django.utils import timezone from django.db import models from django.contrib.postgres.fields import JSONField, ArrayField from chord_metadata_service.restapi.models import IndexableMixin from chord_metadata_service.phenopackets.models import Gene from chord_metadata_service.patients.models import Individual -from django.core.exceptions import ValidationError from chord_metadata_service.restapi.description_utils import rec_help import chord_metadata_service.mcode.descriptions as d from chord_metadata_service.restapi.validators import ontology_validator, ontology_list_validator from .validators import ( - quantity_validator, tumor_marker_data_value_validator, - complex_ontology_validator, - time_or_period_validator + complex_ontology_validator ) @@ -118,7 +115,7 @@ class GenomicsReport(models.Model, IndexableMixin): code = JSONField(validators=[ontology_validator], help_text=rec_help(d.GENOMICS_REPORT, "code")) performing_organization_name = models.CharField( max_length=200, blank=True, help_text=rec_help(d.GENOMICS_REPORT, "performing_organization_name")) - issued = models.DateTimeField(default=now, help_text=rec_help(d.GENOMICS_REPORT, "issued")) + issued = models.DateTimeField(default=timezone.now(), help_text=rec_help(d.GENOMICS_REPORT, "issued")) genetic_specimen = models.ManyToManyField(GeneticSpecimen, blank=True, help_text=rec_help(d.GENOMICS_REPORT, "genetic_specimen")) genetic_variant = models.ForeignKey(CancerGeneticVariant, blank=True, null=True, on_delete=models.SET_NULL, diff --git a/chord_metadata_service/mcode/schemas.py b/chord_metadata_service/mcode/schemas.py index c2453c25e..895a41281 100644 --- a/chord_metadata_service/mcode/schemas.py +++ b/chord_metadata_service/mcode/schemas.py @@ -233,16 +233,10 @@ }, "genetic_specimen": { "type": "array", - "items": { - "type": "string" - } - }, - "genetic_variant": { - "type": "string" - }, - "genomic_region_studied": { - "type": "string" + "items": MCODE_GENETIC_SPECIMEN_SCHEMA }, + "genetic_variant": MCODE_CANCER_GENETIC_VARIANT_SCHEMA, + "genomic_region_studied": MCODE_GENOMIC_REGION_STUDIED_SCHEMA, "extra_properties": EXTRA_PROPERTIES_SCHEMA }, "required": ["id", "code", "issued"] diff --git a/chord_metadata_service/mcode/serializers.py b/chord_metadata_service/mcode/serializers.py index fb8156462..188577d2e 100644 --- a/chord_metadata_service/mcode/serializers.py +++ b/chord_metadata_service/mcode/serializers.py @@ -4,6 +4,9 @@ __all__ = [ + "GeneticSpecimenSerializer", + "CancerGeneticVariantSerializer", + "GenomicRegionStudiedSerializer", "GenomicsReportSerializer", "LabsVitalSerializer", "TNMStagingSerializer", @@ -14,6 +17,27 @@ ] +class GeneticSpecimenSerializer(GenericSerializer): + + class Meta: + model = GeneticSpecimen + fields = '__all__' + + +class CancerGeneticVariantSerializer(GenericSerializer): + + class Meta: + model = CancerGeneticVariant + fields = '__all__' + + +class GenomicRegionStudiedSerializer(GenericSerializer): + + class Meta: + model = GenomicRegionStudied + fields = '__all__' + + class GenomicsReportSerializer(GenericSerializer): class Meta: diff --git a/chord_metadata_service/mcode/validators.py b/chord_metadata_service/mcode/validators.py index d403806b1..43374ff18 100644 --- a/chord_metadata_service/mcode/validators.py +++ b/chord_metadata_service/mcode/validators.py @@ -5,4 +5,5 @@ quantity_validator = JsonSchemaValidator(schema=QUANTITY, formats=['uri']) tumor_marker_data_value_validator = JsonSchemaValidator(schema=TUMOR_MARKER_DATA_VALUE) complex_ontology_validator = JsonSchemaValidator(schema=COMPLEX_ONTOLOGY, formats=['uri']) +# TODO delete? time_or_period_validator = JsonSchemaValidator(schema=TIME_OR_PERIOD, formats=['date-time']) diff --git a/chord_metadata_service/restapi/urls.py b/chord_metadata_service/restapi/urls.py index 49946fbd5..af96ab13c 100644 --- a/chord_metadata_service/restapi/urls.py +++ b/chord_metadata_service/restapi/urls.py @@ -39,6 +39,9 @@ router.register(r'interpretations', phenopacket_views.InterpretationViewSet) # mCode app urls +router.register(r'geneticspecimens', mcode_views.GeneticSpecimenViewSet) +router.register(r'cancergeneticvariants', mcode_views.CancerGeneticVariantViewSet) +router.register(r'genomicregionsstudied', mcode_views.GenomicRegionStudiedViewSet) router.register(r'genomicsreports', mcode_views.GenomicsReportViewSet) router.register(r'labsvital', mcode_views.LabsVitalViewSet) router.register(r'cancerconditions', mcode_views.CancerConditionViewSet) From 3b3614564617873522a242c0bb78a6b6eb8cfaf6 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Wed, 10 Jun 2020 23:37:51 -0400 Subject: [PATCH 123/190] add the rest of mcode profiles v1.0.0 --- .../mcode/mappings/mcode_profiles.py | 41 ++++++++++++------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/chord_metadata_service/mcode/mappings/mcode_profiles.py b/chord_metadata_service/mcode/mappings/mcode_profiles.py index 734742c30..c1d58559b 100644 --- a/chord_metadata_service/mcode/mappings/mcode_profiles.py +++ b/chord_metadata_service/mcode/mappings/mcode_profiles.py @@ -5,20 +5,21 @@ MCODE_ECOG_PERFORMANCE_STATUS = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-ecog-performance-status" MCODE_KARNOFSKY = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-karnofsky-performance-status" +# GeneticSpecimen +MCODE_GENETIC_SPECIMEN = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-genetic-specimen" + # GeneticVariant MCODE_GENETIC_VARIANT = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-cancer-genetic-variant" +# GenomicRegionStudied +MCODE_GENOMIC_REGION_STUDIED = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-genomic-region-studied" + # GenomicsReport MCODE_GENOMICS_REPORT = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-cancer-genomics-report" # LabsVital # the following are present in Ballout 1 version but not in 1.0.0 version -MCODE_BODY_HEIGHT = "" -MCODE_BODY_WEIGHT = "" -MCODE_CBC_WITH_AUTO_DIFFERENTIAL_PANEL = "" -MCODE_COMPREHENSIVE_METABOLIC_2000 = "" -MCODE_BLOOD_PRESSURE = "" -MCODE_TUMOR_MARKER_TEST = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-tumor-marker" +MCODE_TUMOR_MARKER = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-tumor-marker" # CancerCondition MCODE_PRIMARY_CANCER_CONDITION = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-primary-cancer-condition" @@ -26,16 +27,16 @@ # TNMStaging # CLINICAL -MCODE_CLINICAL_STAGE_GROUP = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-tnm-clinical-stage-group" -MCODE_CLINICAL_PRIMARY_TUMOR_CATEGORY = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-tnm-clinical-primary-tumor-category" -MCODE_CLINICAL_REGIONAL_NODES_CATEGORY = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-tnm-clinical-regional-nodes-category" -MCODE_CLINICAL_DISTANT_METASTASES_CATEGORY = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-tnm-clinical-distant-metastases-category" +MCODE_TNM_CLINICAL_STAGE_GROUP = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-tnm-clinical-stage-group" +MCODE_TNM_CLINICAL_PRIMARY_TUMOR_CATEGORY = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-tnm-clinical-primary-tumor-category" +MCODE_TNM_CLINICAL_REGIONAL_NODES_CATEGORY = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-tnm-clinical-regional-nodes-category" +MCODE_TNM_CLINICAL_DISTANT_METASTASES_CATEGORY = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-tnm-clinical-distant-metastases-category" # PATHOLOGIC -MCODE_PATHOLOGIC_STAGE_GROUP = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-tnm-pathological-stage-group" -MCODE_PATHOLOGIC_PRIMARY_TUMOR_CATEGORY = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-tnm-pathological-primary-tumor-category" -MCODE_PATHOLOGIC_REGIONAL_NODES_CATEGORY = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-tnm-pathological-regional-nodes-category" -MCODE_PATHOLOGIC_DISTANT_METASTASES_CATEGORY = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-tnm-pathological-distant-metastases-category" +MCODE_TNM_PATHOLOGIC_STAGE_GROUP = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-tnm-pathological-stage-group" +MCODE_TNM_PATHOLOGIC_PRIMARY_TUMOR_CATEGORY = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-tnm-pathological-primary-tumor-category" +MCODE_TNM_PATHOLOGIC_REGIONAL_NODES_CATEGORY = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-tnm-pathological-regional-nodes-category" +MCODE_TNM_PATHOLOGIC_DISTANT_METASTASES_CATEGORY = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-tnm-pathological-distant-metastases-category" # CancerRelatedProcedure # CancerRelatedRadiationProcedure @@ -46,5 +47,15 @@ # MedicationStatement MCODE_MEDICATION_STATEMENT = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-cancer-related-medication-statement" -# add it to mCodepacket +# mCodePacket MCODE_CANCER_DISEASE_STATUS = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-cancer-disease-status" + +# Extension definitions +MCODE_LATERALITY = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-laterality" + +# CancerCondition histology_morphology_behavior +MCODE_HISTOLOGY_MORPHOLOGY_BEHAVIOR = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-histology-morphology-behavior" + +# MedicationStatement +MCODE_TERMINATION_REASON = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-termination-reason" +MCODE_TREATMENT_INTENT = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-treatment-intent" From 19636db1f4e1eb053b865aeda9fe34f109af48ff Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Thu, 11 Jun 2020 10:11:08 -0400 Subject: [PATCH 124/190] add mappings between mcode app and mcode profiles --- .../mcode/mappings/mappings.py | 77 +++++++++++++++++++ .../mcode/mappings/mcode_profiles.py | 2 +- 2 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 chord_metadata_service/mcode/mappings/mappings.py diff --git a/chord_metadata_service/mcode/mappings/mappings.py b/chord_metadata_service/mcode/mappings/mappings.py new file mode 100644 index 000000000..4149d704b --- /dev/null +++ b/chord_metadata_service/mcode/mappings/mappings.py @@ -0,0 +1,77 @@ +from .mcode_profiles import * + +MCODE_PROFILES_MAPPING = { + "patient": { + "profile": MCODE_PATIENT, + "properties_profile": { + "comorbid_condition": MCODE_COMORBID_CONDITION, + "ecog_performance_status": MCODE_ECOG_PERFORMANCE_STATUS, + "karnofsky": MCODE_KARNOFSKY + } + }, + "genetic_specimen": { + "profile": MCODE_GENETIC_SPECIMEN, + "properties_profile": { + "laterality": MCODE_LATERALITY + } + }, + "cancer_genetic_variant": { + "profile": MCODE_CANCER_GENETIC_VARIANT + }, + "genomic_region_studied": { + "profile": MCODE_GENOMIC_REGION_STUDIED + }, + "genomics_report": { + "profile": MCODE_GENOMICS_REPORT + }, + "labs_vital": { + "profile": MCODE_TUMOR_MARKER + }, + "cancer_condition": { + "profile": { + "primary": MCODE_PRIMARY_CANCER_CONDITION, + "secondary": MCODE_SECONDARY_CANCER_CONDITION + }, + "properties_profile": { + "histology_morphology_behavior": MCODE_HISTOLOGY_MORPHOLOGY_BEHAVIOR + } + }, + "tnm_staging": { + "properties_profile": { + "stage_group": { + "clinical": MCODE_TNM_CLINICAL_STAGE_GROUP, + "pathologic": MCODE_TNM_PATHOLOGIC_STAGE_GROUP + }, + "primary_tumor_category": { + "clinical": MCODE_TNM_CLINICAL_PRIMARY_TUMOR_CATEGORY, + "pathologic": MCODE_TNM_PATHOLOGIC_PRIMARY_TUMOR_CATEGORY + }, + "regional_nodes_category": { + "clinical": MCODE_TNM_CLINICAL_REGIONAL_NODES_CATEGORY, + "pathologic": MCODE_TNM_PATHOLOGIC_REGIONAL_NODES_CATEGORY + }, + "distant_metastases_category": { + "clinical": MCODE_TNM_CLINICAL_DISTANT_METASTASES_CATEGORY, + "pathologic": MCODE_TNM_PATHOLOGIC_DISTANT_METASTASES_CATEGORY + } + } + }, + "cancer_related_procedure": { + "profile": { + "radiation": MCODE_CANCER_RELATED_RADIATION_PROCEDURE, + "surgical": MCODE_CANCER_RELATED_SURGICAL_PROCEDURE + } + }, + "medication_statement": { + "profile": MCODE_MEDICATION_STATEMENT, + "properties_profile": { + "termination_reason": MCODE_TERMINATION_REASON, + "treatment_intent": MCODE_TREATMENT_INTENT + } + }, + "mcodepacket": { + "properties_profile": { + "cancer_disease_status": MCODE_CANCER_DISEASE_STATUS + } + } +} diff --git a/chord_metadata_service/mcode/mappings/mcode_profiles.py b/chord_metadata_service/mcode/mappings/mcode_profiles.py index c1d58559b..b252af8df 100644 --- a/chord_metadata_service/mcode/mappings/mcode_profiles.py +++ b/chord_metadata_service/mcode/mappings/mcode_profiles.py @@ -9,7 +9,7 @@ MCODE_GENETIC_SPECIMEN = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-genetic-specimen" # GeneticVariant -MCODE_GENETIC_VARIANT = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-cancer-genetic-variant" +MCODE_CANCER_GENETIC_VARIANT = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-cancer-genetic-variant" # GenomicRegionStudied MCODE_GENOMIC_REGION_STUDIED = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-genomic-region-studied" From ff1ea65c253544f755023c67570cd2fd8b126c7e Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Fri, 12 Jun 2020 09:59:33 -0400 Subject: [PATCH 125/190] Add input requirements for workflows --- chord_metadata_service/chord/ingest.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/chord_metadata_service/chord/ingest.py b/chord_metadata_service/chord/ingest.py index 2288a8ce5..becf3f437 100644 --- a/chord_metadata_service/chord/ingest.py +++ b/chord_metadata_service/chord/ingest.py @@ -41,6 +41,7 @@ { "id": "json_document", "type": "file", + "required": True, "extensions": [".json"] } ], @@ -62,6 +63,7 @@ { "id": "json_document", "type": "file", + "required": True, "extensions": [".json"] } ], @@ -84,25 +86,30 @@ { "id": "patients", "type": "file", + "required": True, "extensions": [".json"] }, { "id": "observations", "type": "file", + "required": False, "extensions": [".json"] }, { "id": "conditions", "type": "file", + "required": False, "extensions": [".json"] }, { "id": "specimens", "type": "file", + "required": False, "extensions": [".json"] }, { "id": "created_by", + "required": False, "type": "string" }, From 9c9d52d4ed747d8ccb06520399dcd575ad060434 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Fri, 12 Jun 2020 10:47:34 -0400 Subject: [PATCH 126/190] Fix manifest for test json and workflow wdls --- MANIFEST.in | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 461cc729d..60049fcb3 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ -include chord_metadata_service/chord/workflows/phenopackets_json.wdl -include chord_metadata_service/chord/tests/example_phenopacket.json +include chord_metadata_service/chord/workflows/*.wdl +include chord_metadata_service/chord/tests/*.json include chord_metadata_service/dats/* include chord_metadata_service/package.cfg From e99bdaaffa81959ea2b8fe978556b684258a3c60 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Fri, 12 Jun 2020 14:30:47 -0400 Subject: [PATCH 127/190] Remove pointless returns --- chord_metadata_service/restapi/fhir_ingest.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/chord_metadata_service/restapi/fhir_ingest.py b/chord_metadata_service/restapi/fhir_ingest.py index 2e8410f5f..940ebe4dc 100644 --- a/chord_metadata_service/restapi/fhir_ingest.py +++ b/chord_metadata_service/restapi/fhir_ingest.py @@ -56,7 +56,6 @@ def ingest_patients(patients_data, table_id, created_by): table=Table.objects.get(ownership_record_id=table_id) ) logger.info(f'Phenopacket {phenopacket.id} created') - return def ingest_observations(observations_data): @@ -77,7 +76,6 @@ def ingest_observations(observations_data): **phenotypic_feature_data ) logger.info(f'PhenotypicFeature {phenotypic_feature.id} created') - return def ingest_conditions(conditions_data): @@ -97,7 +95,6 @@ def ingest_conditions(conditions_data): phenopacket = Phenopacket.objects.get(subject=Individual.objects.get(id=subject)) phenopacket.diseases.add(disease) logger.info(f'Disease {disease.id} created') - return def ingest_specimens(specimens_data): @@ -123,4 +120,3 @@ def ingest_specimens(specimens_data): phenopacket = Phenopacket.objects.get(subject=Individual.objects.get(id=individual_id)) phenopacket.biosamples.add(biosample) logger.info(f'Biosample {biosample.id} created') - return From 6f9dbc1eb7a159a0d16789cc28d6a0a63864f845 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Fri, 12 Jun 2020 14:46:54 -0400 Subject: [PATCH 128/190] Try to fix optional output issues with wdl --- .../chord/workflows/fhir_json.wdl | 49 ++++++++++++------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/chord_metadata_service/chord/workflows/fhir_json.wdl b/chord_metadata_service/chord/workflows/fhir_json.wdl index e99ddd3a5..d37574a80 100644 --- a/chord_metadata_service/chord/workflows/fhir_json.wdl +++ b/chord_metadata_service/chord/workflows/fhir_json.wdl @@ -3,33 +3,46 @@ workflow fhir_json { File? observations File? conditions File? specimens - String? created_by call identity_task { - input: - patients_in = patients, - observations_in = observations, - conditions_in = conditions, - specimens_in = specimens, - created_by_in = created_by + input: json_in = patients + } + + call optional_fhir_json_task { + input: json_in = observations, file_name = "observations.json" + } + call optional_fhir_json_task { + input: json_in = conditions, file_name = "conditions.json" + } + call optional_fhir_json_task { + input: json_in = specimens, file_name = "specimens.json" } } task identity_task { - File patients_in - File? observations_in - File? conditions_in - File? specimens_in - String? created_by_in + File json_in command { true } + output { - File patients = "${patients_in}" - File? observations = "${observations_in}" - File? conditions = "${conditions_in}" - File? specimens = "${specimens_in}" - String? created_by = "${created_by_in}" + File json_out = "${json_in}" } -} \ No newline at end of file +} + +task optional_fhir_json_task { + File? json_in + String file_name + + command { + if [[ "${json_in}" = "None" ]]; then + echo '{"resourceType": "bundle", "entry": []}' > "${file_name}"; + else + mv "${observations_in}" "${file_name}"; + fi + } + output { + File json_out = "${file_name}" + } +} From 15f69c830e9c20ffbef575caa7d75674d1607595 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Fri, 12 Jun 2020 16:07:55 -0400 Subject: [PATCH 129/190] add parsing for fhir mcode data FHIR v4.0 --- .../mcode/parse_fhir_mcode.py | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 chord_metadata_service/mcode/parse_fhir_mcode.py diff --git a/chord_metadata_service/mcode/parse_fhir_mcode.py b/chord_metadata_service/mcode/parse_fhir_mcode.py new file mode 100644 index 000000000..ed96e5b9f --- /dev/null +++ b/chord_metadata_service/mcode/parse_fhir_mcode.py @@ -0,0 +1,107 @@ +import uuid +import json + +from .mappings.mappings import * +from .mappings.mcode_profiles import * +from chord_metadata_service.restapi.schemas import FHIR_BUNDLE_SCHEMA +from chord_metadata_service.restapi.fhir_ingest import _check_schema +from chord_metadata_service.restapi.fhir_utils import patient_to_individual + + +def observation_to_labs_vital(resource): + """ Observation with tumor marker to LabsVital. """ + labs_vital = { + "id": resource["id"] + } + if "code" in resource: + labs_vital["tumor_marker_code"] = { + "id": f"{resource['code']['coding'][0]['system']}:{resource['code']['coding'][0]['code']}", + "label": f"{resource['code']['coding'][0]['display']}" + } + if "valueCodeableConcept" in resource: + labs_vital["tumor_marker_data_value"] = { + "id": f"{resource['valueCodeableConcept']['coding'][0]['system']}:{resource['code']['coding'][0]['code']}", + "label": f"{resource['valueCodeableConcept']['coding'][0]['display']}" + } + return labs_vital + + +def condition_to_cancer_condition(resource): + """ FHIR Condition to Mcode Cancer Condition. """ + + cancer_condition = {} + cancer_condition["id"] = resource["id"] + # condition = cond.Condition(resource) + if "clinicalStatus" in resource: + cancer_condition["clinical_status"] = { + "id": f"{resource['clinicalStatus']['coding'][0]['code']}", + "label": f"{resource['clinicalStatus']['coding'][0]['code']}" + } + if "verificationStatus" in resource: + cancer_condition["verification_status"] = { + "id": f"{resource['verificationStatus']['coding'][0]['code']}", + "label": f"{resource['verificationStatus']['coding'][0]['code']}" + } + if "code" in resource: + cancer_condition["code"] = { + "id": f"{resource['code']['coding'][0]['system']}:{resource['code']['coding'][0]['code']}", + "label": f"{resource['code']['coding'][0]['display']}" + } + if "recordedDate" in resource: + cancer_condition["date_of_diagnosis"] = resource["recordedDate"] + if "bodySite" in resource: + cancer_condition["body_site"] = [] + for item in resource["bodySite"]['coding']: + coding = { + "id": f"{item['system']}:{item['code']}", + "label": f"{item['display']}", + } + cancer_condition["body_site"].append(coding) + if "laterality" in resource: + cancer_condition["laterality"] = { + "id": f"{resource['laterality']['coding'][0]['system']}:" + f"{resource['laterality']['coding'][0]['code']}", + "label": f"{resource['laterality']['coding'][0]['display']}" + } + if "histologyMorphologyBehavior" in resource: + cancer_condition["histology_morphology_behavior"] = { + "id": f"{resource['histologyMorphologyBehavior']['coding'][0]['system']}:" + f"{resource['histologyMorphologyBehavior']['coding'][0]['code']}", + "label": f"{resource['histologyMorphologyBehavior']['coding'][0]['display']}" + } + return cancer_condition + + +def parse_bundle(bundle): + """ + Parse fhir Bundle and extract all relevant profiles. + :param bundle: FHIR resourceType Bundle object + :return: + """ + _check_schema(FHIR_BUNDLE_SCHEMA, bundle, 'bundle') + mcodepacket = { + "id": str(uuid.uuid4()) + } + tumor_markers = [] + for item in bundle["entry"]: + resource = item["resource"] + if resource["resourceType"] == "Patient": + # patient = patient_to_individual(resource) + mcodepacket["subject"] = { + "id": resource["id"] + } + if resource["resourceType"] == "Condition": + resource_profiles = resource["meta"]["profile"] + cancer_conditions = [MCODE_PRIMARY_CANCER_CONDITION, MCODE_SECONDARY_CANCER_CONDITION] + for cc in cancer_conditions: + if cc in resource_profiles: + cancer_condition = condition_to_cancer_condition(resource) + mcodepacket["cancer_condition"] = cancer_condition + + if resource["resourceType"] == "Observation" and "meta" in resource: + if MCODE_TUMOR_MARKER in resource["meta"]["profile"]: + labs_vital = observation_to_labs_vital(resource) + tumor_markers.append(labs_vital) + mcodepacket["tumor_marker"] = tumor_markers + + return mcodepacket From 94fc56a97518ca50a7927676ecd49ce9ea0c8ef3 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Fri, 12 Jun 2020 16:22:54 -0400 Subject: [PATCH 130/190] add condition_type --- chord_metadata_service/mcode/parse_fhir_mcode.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/chord_metadata_service/mcode/parse_fhir_mcode.py b/chord_metadata_service/mcode/parse_fhir_mcode.py index ed96e5b9f..09bfa44cb 100644 --- a/chord_metadata_service/mcode/parse_fhir_mcode.py +++ b/chord_metadata_service/mcode/parse_fhir_mcode.py @@ -96,7 +96,10 @@ def parse_bundle(bundle): for cc in cancer_conditions: if cc in resource_profiles: cancer_condition = condition_to_cancer_condition(resource) - mcodepacket["cancer_condition"] = cancer_condition + for key, value in MCODE_PROFILES_MAPPING["cancer_condition"]["profile"].items(): + if cc == value: + cancer_condition["condition_type"] = key + mcodepacket["cancer_condition"] = cancer_condition if resource["resourceType"] == "Observation" and "meta" in resource: if MCODE_TUMOR_MARKER in resource["meta"]["profile"]: From a7f60aab672067f15513830566e336dbb37b7ce6 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 15 Jun 2020 09:06:40 -0400 Subject: [PATCH 131/190] Escape curly brackets in wdl --- chord_metadata_service/chord/workflows/fhir_json.wdl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chord_metadata_service/chord/workflows/fhir_json.wdl b/chord_metadata_service/chord/workflows/fhir_json.wdl index d37574a80..d7c6f0cbe 100644 --- a/chord_metadata_service/chord/workflows/fhir_json.wdl +++ b/chord_metadata_service/chord/workflows/fhir_json.wdl @@ -37,7 +37,7 @@ task optional_fhir_json_task { command { if [[ "${json_in}" = "None" ]]; then - echo '{"resourceType": "bundle", "entry": []}' > "${file_name}"; + echo '*{"resourceType": "bundle", "entry": []}*' > "${file_name}"; else mv "${observations_in}" "${file_name}"; fi From 8b06cbb7086c84c928d4bf099a850621183627ca Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 15 Jun 2020 09:07:31 -0400 Subject: [PATCH 132/190] Fix potential issue in importing FHIR data (which would result if multiple phenopackets for the same patient were present) --- chord_metadata_service/chord/ingest.py | 12 ++++-- chord_metadata_service/restapi/fhir_ingest.py | 43 +++++++++++++------ .../restapi/tests/test_fhir_ingest.py | 3 +- 3 files changed, 39 insertions(+), 19 deletions(-) diff --git a/chord_metadata_service/chord/ingest.py b/chord_metadata_service/chord/ingest.py index becf3f437..d7ecfc964 100644 --- a/chord_metadata_service/chord/ingest.py +++ b/chord_metadata_service/chord/ingest.py @@ -350,22 +350,26 @@ def ingest_phenopacket_workflow(workflow_outputs, table_id): def ingest_fhir_workflow(workflow_outputs, table_id): with open(workflow_outputs["patients"], "r") as pf: patients_data = json.load(pf) - ingest_patients(patients_data, table_id, workflow_outputs.get("created_by") or "Imported from file.") + phenopacket_ids = ingest_patients( + patients_data, + table_id, + workflow_outputs.get("created_by") or "Imported from file.", + ) if "observations" in workflow_outputs: with open(workflow_outputs["observations"], "r") as of: observations_data = json.load(of) - ingest_observations(observations_data) + ingest_observations(phenopacket_ids, observations_data) if "conditions" in workflow_outputs: with open(workflow_outputs["conditions"], "r") as cf: conditions_data = json.load(cf) - ingest_conditions(conditions_data) + ingest_conditions(phenopacket_ids, conditions_data) if "specimens" in workflow_outputs: with open(workflow_outputs["specimens"], "r") as sf: specimens_data = json.load(sf) - ingest_specimens(specimens_data) + ingest_specimens(phenopacket_ids, specimens_data) WORKFLOW_INGEST_FUNCTION_MAP = { diff --git a/chord_metadata_service/restapi/fhir_ingest.py b/chord_metadata_service/restapi/fhir_ingest.py index 940ebe4dc..12206d8a2 100644 --- a/chord_metadata_service/restapi/fhir_ingest.py +++ b/chord_metadata_service/restapi/fhir_ingest.py @@ -2,6 +2,8 @@ import jsonschema import logging +from typing import Dict + from .schemas import FHIR_BUNDLE_SCHEMA from .fhir_utils import ( patient_to_individual, @@ -29,9 +31,10 @@ def _check_schema(schema, obj, additional_info=None): except jsonschema.exceptions.ValidationError: v = jsonschema.Draft7Validator(schema) errors = [e for e in v.iter_errors(obj)] - error_messages = [] - for i, error in enumerate(errors, 1): - error_messages.append(f"{i} validation error {'.'.join(str(v) for v in error.path)}: {error.message}") + error_messages = [ + f"{i} validation error {'.'.join(str(v) for v in error.path)}: {error.message}" + for i, error in enumerate(errors, 1) + ] raise ValidationError(f"{additional_info + ' ' if additional_info else None}errors: {error_messages}") @@ -39,6 +42,8 @@ def ingest_patients(patients_data, table_id, created_by): """ Takes FHIR Bundle containing Patient resources. """ # check if Patients data follows FHIR Bundle schema _check_schema(FHIR_BUNDLE_SCHEMA, patients_data, 'patients data') + + phenopacket_ids = {} for item in patients_data["entry"]: individual_data = patient_to_individual(item["resource"]) individual, _ = Individual.objects.get_or_create(**individual_data) @@ -49,21 +54,26 @@ def ingest_patients(patients_data, table_id, created_by): external_references=[] ) # create new phenopacket for each individual + phenopacket_ids[individual.id] = str(uuid.uuid4()) phenopacket = Phenopacket.objects.create( - id=str(uuid.uuid4()), + id=phenopacket_ids[individual.id], subject=individual, meta_data=meta_data_obj, table=Table.objects.get(ownership_record_id=table_id) ) logger.info(f'Phenopacket {phenopacket.id} created') + return phenopacket_ids -def ingest_observations(observations_data): + +def ingest_observations(phenopacket_ids: Dict[str, str], observations_data): """ Takes FHIR Bundle containing Observation resources. """ # check if Observations data follows FHIR Bundle schema _check_schema(FHIR_BUNDLE_SCHEMA, observations_data, 'observations data') + for item in observations_data["entry"]: phenotypic_feature_data = observation_to_phenotypic_feature(item["resource"]) + # Observation must have a subject try: item["resource"]["subject"] @@ -72,19 +82,22 @@ def ingest_observations(observations_data): subject = _parse_reference(item["resource"]["subject"]["reference"]) phenotypic_feature, _ = PhenotypicFeature.objects.get_or_create( - phenopacket=Phenopacket.objects.get(subject=Individual.objects.get(id=subject)), + phenopacket=Phenopacket.objects.get(id=phenopacket_ids[subject]), **phenotypic_feature_data ) + logger.info(f'PhenotypicFeature {phenotypic_feature.id} created') -def ingest_conditions(conditions_data): +def ingest_conditions(phenopacket_ids: Dict[str, str], conditions_data): """ Takes FHIR Bundle containing Condition resources. """ # check if Conditions data follows FHIR Bundle schema _check_schema(FHIR_BUNDLE_SCHEMA, conditions_data, 'conditions data') + for item in conditions_data["entry"]: disease_data = condition_to_disease(item["resource"]) disease = Disease.objects.create(**disease_data) + # Condition must have a subject try: item["resource"]["subject"] @@ -92,22 +105,24 @@ def ingest_conditions(conditions_data): raise KeyError(f"Condition {item['resource']['id']} doesn't have a subject.") subject = _parse_reference(item["resource"]["subject"]["reference"]) - phenopacket = Phenopacket.objects.get(subject=Individual.objects.get(id=subject)) + + phenopacket = Phenopacket.objects.get(id=phenopacket_ids[subject]) phenopacket.diseases.add(disease) + logger.info(f'Disease {disease.id} created') -def ingest_specimens(specimens_data): +def ingest_specimens(phenopacket_ids: Dict[str, str], specimens_data): """ Takes FHIR Bundle containing Specimen resources. """ # check if Specimens data follows FHIR Bundle schema _check_schema(FHIR_BUNDLE_SCHEMA, specimens_data, 'specimens data') + for item in specimens_data["entry"]: biosample_data = specimen_to_biosample(item["resource"]) procedure, _ = Procedure.objects.get_or_create(**biosample_data["procedure"]) + # Specimen must have a subject - try: - biosample_data["individual"] - except KeyError: + if not biosample_data.get("individual"): raise KeyError(f"Specimen {item['resource']['id']} doesn't have a subject.") individual_id = _parse_reference(biosample_data["individual"]) @@ -117,6 +132,8 @@ def ingest_specimens(specimens_data): individual=Individual.objects.get(id=individual_id), sampled_tissue=biosample_data["sampled_tissue"] ) - phenopacket = Phenopacket.objects.get(subject=Individual.objects.get(id=individual_id)) + + phenopacket = Phenopacket.objects.get(id=phenopacket_ids[individual_id]) phenopacket.biosamples.add(biosample) + logger.info(f'Biosample {biosample.id} created') diff --git a/chord_metadata_service/restapi/tests/test_fhir_ingest.py b/chord_metadata_service/restapi/tests/test_fhir_ingest.py index 95f66cc29..7e078c591 100644 --- a/chord_metadata_service/restapi/tests/test_fhir_ingest.py +++ b/chord_metadata_service/restapi/tests/test_fhir_ingest.py @@ -18,7 +18,6 @@ def setUp(self) -> None: dataset=self.d) self.t = Table.objects.create(ownership_record=to, name="Table 1", data_type=DATA_TYPE_PHENOPACKET) - def test_fhir_bundle_schema(self): with self.assertRaises(ValidationError): @@ -32,6 +31,6 @@ def test_required_subject(self): with self.assertRaises(KeyError): try: - ingest_observations(INVALID_SUBJECT_NOT_PRESENT) + ingest_observations({}, INVALID_SUBJECT_NOT_PRESENT) except KeyError as e: raise e From 2a36aa0845801e54954b7a57beaeec834687284c Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 15 Jun 2020 09:35:00 -0400 Subject: [PATCH 133/190] Fix issues with fhir json wdl --- chord_metadata_service/chord/workflows/fhir_json.wdl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/chord_metadata_service/chord/workflows/fhir_json.wdl b/chord_metadata_service/chord/workflows/fhir_json.wdl index d7c6f0cbe..63a6a0e13 100644 --- a/chord_metadata_service/chord/workflows/fhir_json.wdl +++ b/chord_metadata_service/chord/workflows/fhir_json.wdl @@ -8,13 +8,13 @@ workflow fhir_json { input: json_in = patients } - call optional_fhir_json_task { + call optional_fhir_json_task as ofjt1 { input: json_in = observations, file_name = "observations.json" } - call optional_fhir_json_task { + call optional_fhir_json_task as ofjt2 { input: json_in = conditions, file_name = "conditions.json" } - call optional_fhir_json_task { + call optional_fhir_json_task as ofjt3 { input: json_in = specimens, file_name = "specimens.json" } } @@ -35,13 +35,13 @@ task optional_fhir_json_task { File? json_in String file_name - command { + command <<< if [[ "${json_in}" = "None" ]]; then echo '*{"resourceType": "bundle", "entry": []}*' > "${file_name}"; else mv "${observations_in}" "${file_name}"; fi - } + >>> output { File json_out = "${file_name}" } From f7b5720449ce39acf4b075701c580b835101db6e Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 15 Jun 2020 09:37:33 -0400 Subject: [PATCH 134/190] Fix bad var reference in wdl --- chord_metadata_service/chord/workflows/fhir_json.wdl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chord_metadata_service/chord/workflows/fhir_json.wdl b/chord_metadata_service/chord/workflows/fhir_json.wdl index 63a6a0e13..5e9503103 100644 --- a/chord_metadata_service/chord/workflows/fhir_json.wdl +++ b/chord_metadata_service/chord/workflows/fhir_json.wdl @@ -39,7 +39,7 @@ task optional_fhir_json_task { if [[ "${json_in}" = "None" ]]; then echo '*{"resourceType": "bundle", "entry": []}*' > "${file_name}"; else - mv "${observations_in}" "${file_name}"; + mv "${json_in}" "${file_name}"; fi >>> output { From 20282212f3ad359e13a6d78dd8787a97198b2e30 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 15 Jun 2020 09:58:57 -0400 Subject: [PATCH 135/190] Add debug echo --- chord_metadata_service/chord/workflows/fhir_json.wdl | 1 + 1 file changed, 1 insertion(+) diff --git a/chord_metadata_service/chord/workflows/fhir_json.wdl b/chord_metadata_service/chord/workflows/fhir_json.wdl index 5e9503103..e5d30962a 100644 --- a/chord_metadata_service/chord/workflows/fhir_json.wdl +++ b/chord_metadata_service/chord/workflows/fhir_json.wdl @@ -36,6 +36,7 @@ task optional_fhir_json_task { String file_name command <<< + echo "${json_in}" && if [[ "${json_in}" = "None" ]]; then echo '*{"resourceType": "bundle", "entry": []}*' > "${file_name}"; else From 3c6e49c4aba0eca1d6811d48d0e61b96d3ccf131 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 15 Jun 2020 10:19:26 -0400 Subject: [PATCH 136/190] More debug attempts --- chord_metadata_service/chord/workflows/fhir_json.wdl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chord_metadata_service/chord/workflows/fhir_json.wdl b/chord_metadata_service/chord/workflows/fhir_json.wdl index e5d30962a..ef6f9fc87 100644 --- a/chord_metadata_service/chord/workflows/fhir_json.wdl +++ b/chord_metadata_service/chord/workflows/fhir_json.wdl @@ -36,11 +36,11 @@ task optional_fhir_json_task { String file_name command <<< - echo "${json_in}" && if [[ "${json_in}" = "None" ]]; then echo '*{"resourceType": "bundle", "entry": []}*' > "${file_name}"; else - mv "${json_in}" "${file_name}"; + echo "${json_in}" > "${file_name}"; + # mv "${json_in}" "${file_name}"; fi >>> output { From fe8f0a1fd0414116c2a1a41384dfe6b7f6179393 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 15 Jun 2020 10:21:36 -0400 Subject: [PATCH 137/190] More debug attempts --- chord_metadata_service/chord/workflows/fhir_json.wdl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chord_metadata_service/chord/workflows/fhir_json.wdl b/chord_metadata_service/chord/workflows/fhir_json.wdl index ef6f9fc87..1ba181435 100644 --- a/chord_metadata_service/chord/workflows/fhir_json.wdl +++ b/chord_metadata_service/chord/workflows/fhir_json.wdl @@ -39,7 +39,7 @@ task optional_fhir_json_task { if [[ "${json_in}" = "None" ]]; then echo '*{"resourceType": "bundle", "entry": []}*' > "${file_name}"; else - echo "${json_in}" > "${file_name}"; + echo "${json_in}" > "/chord/data/${file_name}"; # mv "${json_in}" "${file_name}"; fi >>> From 9aafb3805e69b3a41a9cd874b49fa01dd16effab Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 15 Jun 2020 10:52:13 -0400 Subject: [PATCH 138/190] Try to fix wdl conditional ?????? More debug stuff --- chord_metadata_service/chord/workflows/fhir_json.wdl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/chord_metadata_service/chord/workflows/fhir_json.wdl b/chord_metadata_service/chord/workflows/fhir_json.wdl index 1ba181435..f52d353b1 100644 --- a/chord_metadata_service/chord/workflows/fhir_json.wdl +++ b/chord_metadata_service/chord/workflows/fhir_json.wdl @@ -36,10 +36,11 @@ task optional_fhir_json_task { String file_name command <<< - if [[ "${json_in}" = "None" ]]; then + if [[ "${json_in}" = *"None"* ]]; then echo '*{"resourceType": "bundle", "entry": []}*' > "${file_name}"; else - echo "${json_in}" > "/chord/data/${file_name}"; + echo "${json_in}" > "/chord/data/${file_name}" && + echo '*{"resourceType": "bundle", "entry": []}*' > "/chord/data/{file_name}_test.json"; # mv "${json_in}" "${file_name}"; fi >>> From 9df90fb477e0ee74887599e9b559991cbc64e53b Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 15 Jun 2020 11:14:23 -0400 Subject: [PATCH 139/190] Wdl again --- chord_metadata_service/chord/workflows/fhir_json.wdl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/chord_metadata_service/chord/workflows/fhir_json.wdl b/chord_metadata_service/chord/workflows/fhir_json.wdl index f52d353b1..a8f73b021 100644 --- a/chord_metadata_service/chord/workflows/fhir_json.wdl +++ b/chord_metadata_service/chord/workflows/fhir_json.wdl @@ -36,12 +36,12 @@ task optional_fhir_json_task { String file_name command <<< - if [[ "${json_in}" = *"None"* ]]; then - echo '*{"resourceType": "bundle", "entry": []}*' > "${file_name}"; - else + if [[ -f "${json_in}" ]]; then echo "${json_in}" > "/chord/data/${file_name}" && - echo '*{"resourceType": "bundle", "entry": []}*' > "/chord/data/{file_name}_test.json"; - # mv "${json_in}" "${file_name}"; + echo '{"resourceType": "bundle", "entry": []}' > "/chord/data/${file_name}_test.json" && + mv "${json_in}" "${file_name}"; + else + echo '{"resourceType": "bundle", "entry": []}' > "${file_name}"; fi >>> output { From 87796bdbaf5e4342ff229702b6270baaa4111e44 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 15 Jun 2020 15:27:18 -0400 Subject: [PATCH 140/190] Remove debug stuff --- chord_metadata_service/chord/workflows/fhir_json.wdl | 2 -- 1 file changed, 2 deletions(-) diff --git a/chord_metadata_service/chord/workflows/fhir_json.wdl b/chord_metadata_service/chord/workflows/fhir_json.wdl index a8f73b021..e3b2bae78 100644 --- a/chord_metadata_service/chord/workflows/fhir_json.wdl +++ b/chord_metadata_service/chord/workflows/fhir_json.wdl @@ -37,8 +37,6 @@ task optional_fhir_json_task { command <<< if [[ -f "${json_in}" ]]; then - echo "${json_in}" > "/chord/data/${file_name}" && - echo '{"resourceType": "bundle", "entry": []}' > "/chord/data/${file_name}_test.json" && mv "${json_in}" "${file_name}"; else echo '{"resourceType": "bundle", "entry": []}' > "${file_name}"; From 2dc647ffdca4eb1840bec8c1ad3fdb3b114bb393 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Mon, 15 Jun 2020 22:56:21 -0400 Subject: [PATCH 141/190] retrieve Observations based on their type and annotate TNM Staging with them --- .../mcode/parse_fhir_mcode.py | 100 +++++++++++++++++- 1 file changed, 99 insertions(+), 1 deletion(-) diff --git a/chord_metadata_service/mcode/parse_fhir_mcode.py b/chord_metadata_service/mcode/parse_fhir_mcode.py index 09bfa44cb..7d0f639ac 100644 --- a/chord_metadata_service/mcode/parse_fhir_mcode.py +++ b/chord_metadata_service/mcode/parse_fhir_mcode.py @@ -26,6 +26,47 @@ def observation_to_labs_vital(resource): return labs_vital +def observation_to_tnm_staging(resource): + """ Observation with tnm staging to TNMStaging. """ + tnm_staging = { + "id": resource["id"], + "tnm_staging_value": {} + } + if "valueCodeableConcept" in resource: + tnm_staging["tnm_staging_value"]["data_value"] = { + "id": f"{resource['valueCodeableConcept']['coding'][0]['system']}:" + f"{resource['valueCodeableConcept']['coding'][0]['code']}", + "label": f"{resource['valueCodeableConcept']['coding'][0]['display']}" + } + if "method" in resource: + tnm_staging["tnm_staging_value"]["staging_system"] = { + "id": f"{resource['method']['coding'][0]['system']}:{resource['method']['coding'][0]['code']}", + "label": f"{resource['method']['coding'][0]['display']}" + } + print(f"This is tnm stagin {tnm_staging}") + return tnm_staging + + +def _get_tnm_staging_property(resource: dict, profile_urls: list, category_type=None): + """" Retrieve Observation based on its profile. """ + for profile in profile_urls: + if profile in resource["meta"]["profile"]: + property_value = observation_to_tnm_staging(resource) + if type: + property_value["category_type"] = category_type + return property_value + + +def _get_profiles(resource: dict, profile_urls: list): + try: + resource_profiles = resource["meta"]["profile"] + for p in profile_urls: + if p in resource_profiles: + return True + except KeyError as e: + raise KeyError(e) + + def condition_to_cancer_condition(resource): """ FHIR Condition to Mcode Cancer Condition. """ @@ -76,20 +117,28 @@ def parse_bundle(bundle): """ Parse fhir Bundle and extract all relevant profiles. :param bundle: FHIR resourceType Bundle object - :return: + :return: mcodepacket object """ _check_schema(FHIR_BUNDLE_SCHEMA, bundle, 'bundle') mcodepacket = { "id": str(uuid.uuid4()) } tumor_markers = [] + # all tnm_stagings + tnm_stagings = [] + # dict that maps tnm_staging value to its member + staging_to_members = {} + # all tnm staging members + tnm_staging_members = [] for item in bundle["entry"]: resource = item["resource"] + # get Patient data if resource["resourceType"] == "Patient": # patient = patient_to_individual(resource) mcodepacket["subject"] = { "id": resource["id"] } + # get Patient's Cancer Condition if resource["resourceType"] == "Condition": resource_profiles = resource["meta"]["profile"] cancer_conditions = [MCODE_PRIMARY_CANCER_CONDITION, MCODE_SECONDARY_CANCER_CONDITION] @@ -101,10 +150,59 @@ def parse_bundle(bundle): cancer_condition["condition_type"] = key mcodepacket["cancer_condition"] = cancer_condition + # get TNM staging stage category + if resource["resourceType"] == "Observation" and "meta" in resource: + resource_profiles = resource["meta"]["profile"] + stage_groups = [MCODE_TNM_CLINICAL_STAGE_GROUP, MCODE_TNM_PATHOLOGIC_STAGE_GROUP] + for sg in stage_groups: + if sg in resource_profiles: + tnm_staging = {"id": resource["id"]} + tnm_stage_group = observation_to_tnm_staging(resource) + tnm_staging["stage_group"] = tnm_stage_group["tnm_staging_value"] + for key, value in MCODE_PROFILES_MAPPING["tnm_staging"]["properties_profile"]["stage_group"].items(): + if sg == value: + tnm_staging["tnm_type"] = key + if "hasMember" in resource: + members = [] + for member in resource["hasMember"]: + member_observation_id = member["reference"].split('/')[-1] + members.append(member_observation_id) + # collect all members of this staging in a dict + staging_to_members[resource["id"]] = members + tnm_stagings.append(tnm_staging) + + # get all TNM staging members + if resource["resourceType"] == "Observation" and "meta" in resource: + primary_tumor_category = _get_tnm_staging_property(resource, + [MCODE_TNM_CLINICAL_PRIMARY_TUMOR_CATEGORY, + MCODE_TNM_PATHOLOGIC_PRIMARY_TUMOR_CATEGORY], + 'primary_tumor_category') + regional_nodes_category = _get_tnm_staging_property(resource, + [MCODE_TNM_CLINICAL_REGIONAL_NODES_CATEGORY, + MCODE_TNM_PATHOLOGIC_REGIONAL_NODES_CATEGORY], + 'regional_nodes_category') + distant_metastases_category = _get_tnm_staging_property(resource, + [MCODE_TNM_CLINICAL_REGIONAL_NODES_CATEGORY, + MCODE_TNM_PATHOLOGIC_REGIONAL_NODES_CATEGORY], + 'distant_metastases_category') + + for category in [primary_tumor_category, regional_nodes_category, distant_metastases_category]: + if category: + tnm_staging_members.append(category) + + # get tumor marker if resource["resourceType"] == "Observation" and "meta" in resource: if MCODE_TUMOR_MARKER in resource["meta"]["profile"]: labs_vital = observation_to_labs_vital(resource) tumor_markers.append(labs_vital) + + # annotate tnm staging with its members + for tnm_staging_item in tnm_stagings: + for member in tnm_staging_members: + if member["id"] in staging_to_members[tnm_staging_item["id"]]: + tnm_staging_item[member["category_type"]] = member["tnm_staging_value"] + + mcodepacket["tnm_staging"] = tnm_stagings mcodepacket["tumor_marker"] = tumor_markers return mcodepacket From 4bd97257bfba4ece3bd8a8c8b16f9ed1a909b4ba Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Mon, 15 Jun 2020 23:27:31 -0400 Subject: [PATCH 142/190] retrieve cancer related procedure --- chord_metadata_service/mcode/models.py | 1 + .../mcode/parse_fhir_mcode.py | 48 ++++++++++++++++++- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/chord_metadata_service/mcode/models.py b/chord_metadata_service/mcode/models.py index 7d0e6af29..b804fc476 100644 --- a/chord_metadata_service/mcode/models.py +++ b/chord_metadata_service/mcode/models.py @@ -259,6 +259,7 @@ class CancerRelatedProcedure(models.Model, IndexableMixin): treatment_intent = JSONField(blank=True, null=True, validators=[ontology_validator], help_text=rec_help(d.CANCER_RELATED_PROCEDURE, "treatment_intent")) # Only for Surgical Procedure + # TODO CHANGE to ontology list validator reason_code = JSONField(blank=True, null=True, validators=[ontology_validator], help_text=rec_help(d.CANCER_RELATED_PROCEDURE, "reason_code")) reason_reference = models.ManyToManyField(CancerCondition, blank=True, diff --git a/chord_metadata_service/mcode/parse_fhir_mcode.py b/chord_metadata_service/mcode/parse_fhir_mcode.py index 7d0f639ac..105bd69e8 100644 --- a/chord_metadata_service/mcode/parse_fhir_mcode.py +++ b/chord_metadata_service/mcode/parse_fhir_mcode.py @@ -43,10 +43,41 @@ def observation_to_tnm_staging(resource): "id": f"{resource['method']['coding'][0]['system']}:{resource['method']['coding'][0]['code']}", "label": f"{resource['method']['coding'][0]['display']}" } - print(f"This is tnm stagin {tnm_staging}") return tnm_staging +def procedure_to_crprocedure(resource): + """ Procedure to Cancer related Procedure. """ + + cancer_related_procedure = { + "id": resource["id"] + } + if "code" in resource: + cancer_related_procedure["code"] = { + "id": f"{resource['code']['coding'][0]['system']}:" + f"{resource['code']['coding'][0]['code']}", + "label": f"{resource['code']['coding'][0]['display']}" + } + if "bodySite" in resource: + cancer_related_procedure["body_site"] = { + "id": f"{resource['bodySite']['coding'][0]['system']}:{resource['bodySite']['coding'][0]['code']}", + "label": f"{resource['bodySite']['coding'][0]['display']}" + } + if "reasonCode" in resource: + codes = [] + for code in resource["reasonCode"]["coding"]: + reason_code = { + "id": f"{code['system']}:{code['code']}", + "label": f"{code['display']}" + } + codes.append(reason_code) + cancer_related_procedure["reason_code"] = codes + if "reasonReference" in resource: + cancer_conditions = [cc["reference"].split("uuid:")[-1] for cc in resource["reasonReference"]] + cancer_related_procedure["reason_reference"] = cancer_conditions + return cancer_related_procedure + + def _get_tnm_staging_property(resource: dict, profile_urls: list, category_type=None): """" Retrieve Observation based on its profile. """ for profile in profile_urls: @@ -130,6 +161,8 @@ def parse_bundle(bundle): staging_to_members = {} # all tnm staging members tnm_staging_members = [] + # all procedure + cancer_related_procedures = [] for item in bundle["entry"]: resource = item["resource"] # get Patient data @@ -190,6 +223,18 @@ def parse_bundle(bundle): if category: tnm_staging_members.append(category) + # get Cancer Related Procedure + if resource["resourceType"] == "Procedure" and "meta" in resource: + resource_profiles = resource["meta"]["profile"] + procedure_profiles = [MCODE_CANCER_RELATED_RADIATION_PROCEDURE, MCODE_CANCER_RELATED_SURGICAL_PROCEDURE] + for pp in procedure_profiles: + if pp in resource_profiles: + procedure = procedure_to_crprocedure(resource) + for key, value in MCODE_PROFILES_MAPPING["cancer_related_procedure"]["profile"].items(): + if pp == value: + procedure["procedure_type"] = key + cancer_related_procedures.append(procedure) + # get tumor marker if resource["resourceType"] == "Observation" and "meta" in resource: if MCODE_TUMOR_MARKER in resource["meta"]["profile"]: @@ -204,5 +249,6 @@ def parse_bundle(bundle): mcodepacket["tnm_staging"] = tnm_stagings mcodepacket["tumor_marker"] = tumor_markers + mcodepacket["cancer_related_procedures"] = cancer_related_procedures return mcodepacket From 866cdc5aba295c2536cd02ea6a1394a34432fcb3 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Mon, 15 Jun 2020 23:41:19 -0400 Subject: [PATCH 143/190] get medication statement --- .../mcode/parse_fhir_mcode.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/chord_metadata_service/mcode/parse_fhir_mcode.py b/chord_metadata_service/mcode/parse_fhir_mcode.py index 105bd69e8..bc7fd583f 100644 --- a/chord_metadata_service/mcode/parse_fhir_mcode.py +++ b/chord_metadata_service/mcode/parse_fhir_mcode.py @@ -78,6 +78,21 @@ def procedure_to_crprocedure(resource): return cancer_related_procedure +def get_medication_statement(resource): + """ Medication Statement to Medication Statement. """ + medication_statement = { + "id": resource["id"] + } + if "medicationCodeableConcept" in resource: + medication_statement["medication_code"] = { + "id": f"{resource['medicationCodeableConcept']['coding'][0]['system']}:" + f"{resource['medicationCodeableConcept']['coding'][0]['code']}", + "label": f"{resource['medicationCodeableConcept']['coding'][0]['display']}" + } + # TODO the rest + return medication_statement + + def _get_tnm_staging_property(resource: dict, profile_urls: list, category_type=None): """" Retrieve Observation based on its profile. """ for profile in profile_urls: @@ -241,6 +256,11 @@ def parse_bundle(bundle): labs_vital = observation_to_labs_vital(resource) tumor_markers.append(labs_vital) + # get Medication Statement + if resource["resourceType"] == "MedicationStatement" and "meta" in resource: + if MCODE_MEDICATION_STATEMENT in resource["meta"]["profile"]: + mcodepacket["medication_statement"] = get_medication_statement(resource) + # annotate tnm staging with its members for tnm_staging_item in tnm_stagings: for member in tnm_staging_members: From 75980425fa20ca7d15e681a594ee4c08c23193ef Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Mon, 15 Jun 2020 23:53:21 -0400 Subject: [PATCH 144/190] add cancer disease status --- chord_metadata_service/mcode/parse_fhir_mcode.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/chord_metadata_service/mcode/parse_fhir_mcode.py b/chord_metadata_service/mcode/parse_fhir_mcode.py index bc7fd583f..835ea4cc6 100644 --- a/chord_metadata_service/mcode/parse_fhir_mcode.py +++ b/chord_metadata_service/mcode/parse_fhir_mcode.py @@ -261,6 +261,12 @@ def parse_bundle(bundle): if MCODE_MEDICATION_STATEMENT in resource["meta"]["profile"]: mcodepacket["medication_statement"] = get_medication_statement(resource) + # get Cancer Disease Status + if resource["resourceType"] == "Observation" and "meta" in resource: + if MCODE_CANCER_DISEASE_STATUS in resource["meta"]["profile"]: + # TODO change refactor observation conversion + mcodepacket["cancer_disease_status"] = observation_to_labs_vital(resource)["tumor_marker_data_value"] + # annotate tnm staging with its members for tnm_staging_item in tnm_stagings: for member in tnm_staging_members: From c4bd80c5813a0c521f39209d7b4ebb3e8bd7ab1b Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Tue, 16 Jun 2020 15:18:05 -0400 Subject: [PATCH 145/190] add function to get ontology value --- .../mcode/parse_fhir_mcode.py | 92 ++++++++----------- 1 file changed, 37 insertions(+), 55 deletions(-) diff --git a/chord_metadata_service/mcode/parse_fhir_mcode.py b/chord_metadata_service/mcode/parse_fhir_mcode.py index 835ea4cc6..4be0541ee 100644 --- a/chord_metadata_service/mcode/parse_fhir_mcode.py +++ b/chord_metadata_service/mcode/parse_fhir_mcode.py @@ -8,21 +8,37 @@ from chord_metadata_service.restapi.fhir_utils import patient_to_individual +def get_ontology_value(resource, codeable_concept_property): + """ + The function covers the most encountered use cases. + """ + try: + ontology_value = {} + if "system" in resource[codeable_concept_property]['coding'][0]: + ontology_value["id"] = f"{resource[codeable_concept_property]['coding'][0]['system']}:" \ + f"{resource[codeable_concept_property]['coding'][0]['code']}" + else: + ontology_value["id"] = f"{resource[codeable_concept_property]['coding'][0]['code']}" + + if "display" in resource[codeable_concept_property]['coding'][0]: + ontology_value["label"] = f"{resource[codeable_concept_property]['coding'][0]['display']}" + else: + ontology_value["label"] = f"{resource[codeable_concept_property]['coding'][0]['code']}" + return ontology_value + # will be raised if there is no "code" in Coding element + except KeyError as e: + raise KeyError(e) + + def observation_to_labs_vital(resource): """ Observation with tumor marker to LabsVital. """ labs_vital = { "id": resource["id"] } if "code" in resource: - labs_vital["tumor_marker_code"] = { - "id": f"{resource['code']['coding'][0]['system']}:{resource['code']['coding'][0]['code']}", - "label": f"{resource['code']['coding'][0]['display']}" - } + labs_vital["tumor_marker_code"] = get_ontology_value(resource, "code") if "valueCodeableConcept" in resource: - labs_vital["tumor_marker_data_value"] = { - "id": f"{resource['valueCodeableConcept']['coding'][0]['system']}:{resource['code']['coding'][0]['code']}", - "label": f"{resource['valueCodeableConcept']['coding'][0]['display']}" - } + labs_vital["tumor_marker_data_value"] = get_ontology_value(resource, "valueCodeableConcept") return labs_vital @@ -33,16 +49,9 @@ def observation_to_tnm_staging(resource): "tnm_staging_value": {} } if "valueCodeableConcept" in resource: - tnm_staging["tnm_staging_value"]["data_value"] = { - "id": f"{resource['valueCodeableConcept']['coding'][0]['system']}:" - f"{resource['valueCodeableConcept']['coding'][0]['code']}", - "label": f"{resource['valueCodeableConcept']['coding'][0]['display']}" - } + tnm_staging["tnm_staging_value"]["data_value"] = get_ontology_value(resource, "valueCodeableConcept") if "method" in resource: - tnm_staging["tnm_staging_value"]["staging_system"] = { - "id": f"{resource['method']['coding'][0]['system']}:{resource['method']['coding'][0]['code']}", - "label": f"{resource['method']['coding'][0]['display']}" - } + tnm_staging["tnm_staging_value"]["staging_system"] = get_ontology_value(resource, "method") return tnm_staging @@ -53,16 +62,9 @@ def procedure_to_crprocedure(resource): "id": resource["id"] } if "code" in resource: - cancer_related_procedure["code"] = { - "id": f"{resource['code']['coding'][0]['system']}:" - f"{resource['code']['coding'][0]['code']}", - "label": f"{resource['code']['coding'][0]['display']}" - } + cancer_related_procedure["code"] = get_ontology_value(resource, "code") if "bodySite" in resource: - cancer_related_procedure["body_site"] = { - "id": f"{resource['bodySite']['coding'][0]['system']}:{resource['bodySite']['coding'][0]['code']}", - "label": f"{resource['bodySite']['coding'][0]['display']}" - } + cancer_related_procedure["body_site"] = get_ontology_value(resource, "bodySite") if "reasonCode" in resource: codes = [] for code in resource["reasonCode"]["coding"]: @@ -84,11 +86,7 @@ def get_medication_statement(resource): "id": resource["id"] } if "medicationCodeableConcept" in resource: - medication_statement["medication_code"] = { - "id": f"{resource['medicationCodeableConcept']['coding'][0]['system']}:" - f"{resource['medicationCodeableConcept']['coding'][0]['code']}", - "label": f"{resource['medicationCodeableConcept']['coding'][0]['display']}" - } + medication_statement["medication_code"] = get_ontology_value(resource, "medicationCodeableConcept") # TODO the rest return medication_statement @@ -116,24 +114,16 @@ def _get_profiles(resource: dict, profile_urls: list): def condition_to_cancer_condition(resource): """ FHIR Condition to Mcode Cancer Condition. """ - cancer_condition = {} - cancer_condition["id"] = resource["id"] + cancer_condition = { + "id": resource["id"] + } # condition = cond.Condition(resource) if "clinicalStatus" in resource: - cancer_condition["clinical_status"] = { - "id": f"{resource['clinicalStatus']['coding'][0]['code']}", - "label": f"{resource['clinicalStatus']['coding'][0]['code']}" - } + cancer_condition["clinical_status"] = get_ontology_value(resource, "clinicalStatus") if "verificationStatus" in resource: - cancer_condition["verification_status"] = { - "id": f"{resource['verificationStatus']['coding'][0]['code']}", - "label": f"{resource['verificationStatus']['coding'][0]['code']}" - } + cancer_condition["verification_status"] = get_ontology_value(resource, "verificationStatus") if "code" in resource: - cancer_condition["code"] = { - "id": f"{resource['code']['coding'][0]['system']}:{resource['code']['coding'][0]['code']}", - "label": f"{resource['code']['coding'][0]['display']}" - } + cancer_condition["code"] = get_ontology_value(resource, "code") if "recordedDate" in resource: cancer_condition["date_of_diagnosis"] = resource["recordedDate"] if "bodySite" in resource: @@ -145,17 +135,9 @@ def condition_to_cancer_condition(resource): } cancer_condition["body_site"].append(coding) if "laterality" in resource: - cancer_condition["laterality"] = { - "id": f"{resource['laterality']['coding'][0]['system']}:" - f"{resource['laterality']['coding'][0]['code']}", - "label": f"{resource['laterality']['coding'][0]['display']}" - } + cancer_condition["laterality"] = get_ontology_value(resource, "laterality") if "histologyMorphologyBehavior" in resource: - cancer_condition["histology_morphology_behavior"] = { - "id": f"{resource['histologyMorphologyBehavior']['coding'][0]['system']}:" - f"{resource['histologyMorphologyBehavior']['coding'][0]['code']}", - "label": f"{resource['histologyMorphologyBehavior']['coding'][0]['display']}" - } + cancer_condition["histology_morphology_behavior"] = get_ontology_value(resource, "histologyMorphologyBehavior") return cancer_condition From c1567aac72f813d0238898a21b563adc2f705575 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Wed, 17 Jun 2020 18:13:35 -0400 Subject: [PATCH 146/190] add mcode ingest adjust mcode parsing --- chord_metadata_service/mcode/mcode_ingest.py | 89 +++++++++++++++++++ .../mcode/parse_fhir_mcode.py | 17 +++- 2 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 chord_metadata_service/mcode/mcode_ingest.py diff --git a/chord_metadata_service/mcode/mcode_ingest.py b/chord_metadata_service/mcode/mcode_ingest.py new file mode 100644 index 000000000..b406dc1e3 --- /dev/null +++ b/chord_metadata_service/mcode/mcode_ingest.py @@ -0,0 +1,89 @@ +import uuid +import jsonschema +import logging + +from chord_metadata_service.restapi.schemas import FHIR_BUNDLE_SCHEMA +from chord_metadata_service.patients.models import Individual + +from .parse_fhir_mcode import parse_bundle +from .models import * + + +logger = logging.getLogger("mcode_ingest") +logger.setLevel(logging.INFO) + + +def ingest_mcodepacket(mcodepacket_data): + """ Ingests a single mcodepacket in mcode app and patients' metadata int patients app.""" + new_mcodepacket = mcodepacket_data["id"] + subject = mcodepacket_data["subject"] + genomics_report_data = mcodepacket_data.get("genomics_report", None) + cancer_condition_data = mcodepacket_data.get("cancer_condition", None) + cancer_related_procedures = mcodepacket_data.get("cancer_related_procedures", None) + medication_statement_data = mcodepacket_data.get("medication_statement", None) + date_of_death_data = mcodepacket_data.get("date_of_death", None) + cancer_disease_status_data = mcodepacket_data.get("cancer_disease_status", None) + tumor_markers = mcodepacket_data.get("tumor_marker", None) + + # get and create Patient + if subject: + subject, _ = Individual.objects.get_or_create(**subject) + + if genomics_report_data: + # don't have data for genomics report yet + pass + + # get and create CancerCondition + if cancer_condition_data: + cancer_condition, _ = CancerCondition.objects.get_or_create(**cancer_condition_data) + new_mcodepacket["cancer_condition"] = cancer_condition + if "tnm_staging" in cancer_condition_data: + for tnms in cancer_condition_data["tnm_staging"]: + tnm_staging, _ = TNMStaging.objects.get_or_create(**tnms) + + # get and create Cancer Related Procedure + crprocedures = [] + if cancer_related_procedures: + for crp in cancer_related_procedures: + cancer_related_procedure, _ = CancerRelatedProcedure.objects.get_or_create( + id=crp["id"], + code=crp["code"], + procedure_type=crp["procedure_type"], + body_site= crp.get("body_site", None), + laterality=crp.get("laterality", None), + treatment_intent=crp.get("treatment_intent", None), + reason_code=crp.get("reason_code", None), + extra_properties=crp.get("extra_properties", None) + ) + crprocedures.append(cancer_related_procedure) + if "reason_reference" in crp: + for rr_id in crp["reason_reference"]: + related_cancer_condition = CancerCondition.objects.get(id=rr_id) + cancer_related_procedure.add(related_cancer_condition) + + # get and create CancerCondition + if medication_statement_data: + medication_statement, _ = MedicationStatement.objects.get_or_create( + id=medication_statement_data["id"], + medication_code=medication_statement_data["medication_code"] + ) + new_mcodepacket["medication_statement"] = medication_statement + + # get date of death + if date_of_death_data: + new_mcodepacket["date_of_death"] = date_of_death_data + + # get cancer disease status + if cancer_disease_status_data: + new_mcodepacket["cancer_disease_status"] = cancer_disease_status_data + + # get tumor marker + if tumor_markers: + for tm in tumor_markers: + tumor_marker, _ = LabsVital.objects.get_or_create( + tumor_marker_code=tm["tumor_marker_code"], + tumor_marker_data_value=tm.get("tumor_marker_data_value", None), + individual=tm["individual"] + ) + + return new_mcodepacket diff --git a/chord_metadata_service/mcode/parse_fhir_mcode.py b/chord_metadata_service/mcode/parse_fhir_mcode.py index 4be0541ee..b6ba90c2d 100644 --- a/chord_metadata_service/mcode/parse_fhir_mcode.py +++ b/chord_metadata_service/mcode/parse_fhir_mcode.py @@ -39,6 +39,8 @@ def observation_to_labs_vital(resource): labs_vital["tumor_marker_code"] = get_ontology_value(resource, "code") if "valueCodeableConcept" in resource: labs_vital["tumor_marker_data_value"] = get_ontology_value(resource, "valueCodeableConcept") + if "subject": + labs_vital["individual"] = resource["subject"]["reference"].split("uuid:")[-1] return labs_vital @@ -52,6 +54,9 @@ def observation_to_tnm_staging(resource): tnm_staging["tnm_staging_value"]["data_value"] = get_ontology_value(resource, "valueCodeableConcept") if "method" in resource: tnm_staging["tnm_staging_value"]["staging_system"] = get_ontology_value(resource, "method") + # reference to Condition + if "focus" in resource: + tnm_staging["cancer_condition"] = resource["focus"][0]["reference"].split("/")[-1] return tnm_staging @@ -77,6 +82,7 @@ def procedure_to_crprocedure(resource): if "reasonReference" in resource: cancer_conditions = [cc["reference"].split("uuid:")[-1] for cc in resource["reasonReference"]] cancer_related_procedure["reason_reference"] = cancer_conditions + # TODO add laterality return cancer_related_procedure @@ -96,7 +102,7 @@ def _get_tnm_staging_property(resource: dict, profile_urls: list, category_type= for profile in profile_urls: if profile in resource["meta"]["profile"]: property_value = observation_to_tnm_staging(resource) - if type: + if category_type: property_value["category_type"] = category_type return property_value @@ -188,6 +194,7 @@ def parse_bundle(bundle): if sg in resource_profiles: tnm_staging = {"id": resource["id"]} tnm_stage_group = observation_to_tnm_staging(resource) + tnm_staging["cancer_condition"] = tnm_stage_group["cancer_condition"] tnm_staging["stage_group"] = tnm_stage_group["tnm_staging_value"] for key, value in MCODE_PROFILES_MAPPING["tnm_staging"]["properties_profile"]["stage_group"].items(): if sg == value: @@ -255,8 +262,14 @@ def parse_bundle(bundle): if member["id"] in staging_to_members[tnm_staging_item["id"]]: tnm_staging_item[member["category_type"]] = member["tnm_staging_value"] - mcodepacket["tnm_staging"] = tnm_stagings + # mcodepacket["tnm_staging"] = tnm_stagings mcodepacket["tumor_marker"] = tumor_markers mcodepacket["cancer_related_procedures"] = cancer_related_procedures + # TODO add nested tnm_stagings to cancer_condition + for tnms in tnm_stagings: + if tnms["cancer_condition"] == mcodepacket["cancer_condition"]["id"]: + mcodepacket["cancer_condition"]["tnm_staging"] = tnm_stagings + return mcodepacket + From 5cb4c0d7a5b3d9fad2e9e43052bdddf1ddfd2b10 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Thu, 18 Jun 2020 12:44:17 -0400 Subject: [PATCH 147/190] Add tox --- .travis.yml | 2 +- README.md | 16 +++++++++++----- requirements.txt | 21 +++++++++++++++++---- tox.ini | 10 ++++++++++ 4 files changed, 39 insertions(+), 10 deletions(-) create mode 100644 tox.ini diff --git a/.travis.yml b/.travis.yml index d4382e253..09dfe5110 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,7 @@ install: - pip install . script: - export POSTGRES_USER="postgres" && export POSTGRES_PASSWORD="hj38f3Ntr" && export POSTGRES_PORT=5433 - - python3 -m coverage run ./manage.py test + - python3 -m tox - codecov - rm -rf chord_metadata_service - python3 -m coverage run ./manage.py test chord_metadata_service diff --git a/README.md b/README.md index dc640f94a..766054a0c 100644 --- a/README.md +++ b/README.md @@ -131,22 +131,28 @@ out and tagged from the tagged major/minor release in `master`. Tests are located in tests directory in an individual app folder. -Run all tests for the whole project: +Run all tests and linting checks for the whole project: +```bash +tox ``` + +Run all tests for the whole project: + +```bash python manage.py test ``` Run tests for an individual app, e.g.: -``` +```bash python manage.py test chord_metadata_service.phenopackets.tests.test_api ``` -Create coverage html report: +Test and create `coverage` HTML report: -``` -coverage run manage.py test +```bash +tox coverage html ``` diff --git a/requirements.txt b/requirements.txt index 9678d5377..470d43f41 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,14 +1,16 @@ alabaster==0.7.12 +appdirs==1.4.4 attrs==19.3.0 Babel==2.8.0 -certifi==2020.4.5.1 +certifi==2020.4.5.2 chardet==3.0.4 chord-lib==0.9.0 -codecov==2.1.4 +codecov==2.1.7 colorama==0.4.3 coreapi==2.3.3 coreschema==0.0.4 coverage==5.1 +distlib==0.3.0 Django==2.2.13 django-filter==2.2.0 django-nose==1.4.6 @@ -17,7 +19,10 @@ djangorestframework==3.11.0 djangorestframework-camel-case==1.1.2 docutils==0.16 elasticsearch==7.6.0 +entrypoints==0.3 fhirclient==3.2.0 +filelock==3.0.12 +flake8==3.8.3 idna==2.9 imagesize==1.2.0 importlib-metadata==1.6.0 @@ -27,11 +32,16 @@ Jinja2==2.11.2 jsonschema==3.2.0 Markdown==3.2.2 MarkupSafe==1.1.1 -more-itertools==8.3.0 +mccabe==0.6.1 +more-itertools==8.4.0 nose==1.3.7 openapi-codec==1.3.2 packaging==20.4 +pluggy==0.13.1 psycopg2-binary==2.8.5 +py==1.8.2 +pycodestyle==2.6.0 +pyflakes==2.2.0 Pygments==2.6.1 pyparsing==2.4.7 pyrsistent==0.16.0 @@ -41,7 +51,7 @@ PyYAML==5.3.1 rdflib==4.2.2 rdflib-jsonld==0.4.0 redis==3.5.3 -requests==2.23.0 +requests==2.24.0 rfc3987==1.3.8 simplejson==3.17.0 six==1.15.0 @@ -56,8 +66,11 @@ sphinxcontrib-qthelp==1.0.3 sphinxcontrib-serializinghtml==1.1.4 sqlparse==0.3.1 strict-rfc3339==0.7 +toml==0.10.1 +tox==3.15.2 uritemplate==3.0.1 urllib3==1.25.9 +virtualenv==20.0.23 Werkzeug==1.0.1 wincertstore==0.2 zipp==3.1.0 diff --git a/tox.ini b/tox.ini new file mode 100644 index 000000000..4aff5bc18 --- /dev/null +++ b/tox.ini @@ -0,0 +1,10 @@ +[flake8] +max-line-length = 120 +exclude = .git, .tox, __pycache__, migrations + +[testenv] +skip_install = true +commands = + pip install -r requirements.txt + coverage run ./manage.py test + flake8 ./chord_metadata_service From 203d25ec195190e81dfd170f1784fd89b3a16189 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Thu, 18 Jun 2020 12:44:52 -0400 Subject: [PATCH 148/190] Lint and touch up restapi app --- .../restapi/api_renderers.py | 21 +++------ chord_metadata_service/restapi/fhir_ingest.py | 13 +++++- chord_metadata_service/restapi/fhir_utils.py | 40 +++++++++++------ .../restapi/jsonld_utils.py | 45 +++++++++---------- chord_metadata_service/restapi/schemas.py | 4 +- .../semantic_mappings/hl7_genomics_mapping.py | 2 +- .../phenopackets_on_fhir_mapping.py | 8 ++-- chord_metadata_service/restapi/serializers.py | 2 +- .../restapi/tests/test_fhir.py | 22 ++++++--- .../restapi/tests/test_fhir_ingest.py | 12 ++--- .../restapi/tests/test_jsonld.py | 10 +++-- chord_metadata_service/restapi/urls.py | 6 ++- 12 files changed, 107 insertions(+), 78 deletions(-) diff --git a/chord_metadata_service/restapi/api_renderers.py b/chord_metadata_service/restapi/api_renderers.py index 8d0fa9aa2..97a7efe97 100644 --- a/chord_metadata_service/restapi/api_renderers.py +++ b/chord_metadata_service/restapi/api_renderers.py @@ -1,12 +1,13 @@ -from rest_framework.renderers import JSONRenderer +import json from djangorestframework_camel_case.render import CamelCaseJSONRenderer -from .jsonld_utils import dataset_to_jsonld from rdflib import Graph -import json from rdflib.plugin import register, Serializer +from rest_framework.renderers import JSONRenderer +from uuid import UUID + +from .jsonld_utils import dataset_to_jsonld register('json-ld', Serializer, 'rdflib_jsonld.serializer', 'JsonLDSerializer') -from uuid import UUID class UUIDEncoder(json.JSONEncoder): @@ -31,11 +32,7 @@ def render(self, data, media_type=None, renderer_context=None): 'class_converter', 'objects' ) if 'results' in data: - final_data = {} - final_data[fhir_datatype_plural] = [] - for item in data.get('results'): - item_data = class_converter(item) - final_data[fhir_datatype_plural].append(item_data) + final_data = {fhir_datatype_plural: [class_converter(item) for item in data['results']]} else: final_data = class_converter(data) return super(FHIRRenderer, self).render(final_data, media_type, renderer_context) @@ -55,11 +52,7 @@ class JSONLDDatasetRenderer(PhenopacketsRenderer): def render(self, data, media_type=None, renderer_context=None): if 'results' in data: - json_obj = {} - json_obj['results'] = [] - for item in data['results']: - dataset_jsonld = dataset_to_jsonld(item) - json_obj['results'].append(dataset_jsonld) + json_obj = {'results': [dataset_to_jsonld(item) for item in data['results']]} else: json_obj = dataset_to_jsonld(data) diff --git a/chord_metadata_service/restapi/fhir_ingest.py b/chord_metadata_service/restapi/fhir_ingest.py index 12206d8a2..a3c28cd23 100644 --- a/chord_metadata_service/restapi/fhir_ingest.py +++ b/chord_metadata_service/restapi/fhir_ingest.py @@ -2,6 +2,7 @@ import jsonschema import logging +from django.core.exceptions import ValidationError from typing import Dict from .schemas import FHIR_BUNDLE_SCHEMA @@ -11,8 +12,16 @@ condition_to_disease, specimen_to_biosample ) -from chord_metadata_service.chord.models import * -from chord_metadata_service.phenopackets.models import * +from chord_metadata_service.chord.models import Table +from chord_metadata_service.patients.models import Individual +from chord_metadata_service.phenopackets.models import ( + Biosample, + Disease, + MetaData, + Phenopacket, + PhenotypicFeature, + Procedure, +) logger = logging.getLogger("fhir_ingest") diff --git a/chord_metadata_service/restapi/fhir_utils.py b/chord_metadata_service/restapi/fhir_utils.py index 3268150ef..9498c597c 100644 --- a/chord_metadata_service/restapi/fhir_utils.py +++ b/chord_metadata_service/restapi/fhir_utils.py @@ -1,15 +1,29 @@ from datetime import datetime +from fhirclient.models import ( + observation as obs, + patient as p, + extension, + age, + coding as c, + codeableconcept, + specimen as s, + identifier as fhir_indentifier, + annotation as a, + range as range_, + quantity, + fhirreference, + documentreference, + attachment, + fhirdate, + condition as cond, + composition as comp, +) + from chord_metadata_service.restapi.semantic_mappings.phenopackets_on_fhir_mapping import PHENOPACKETS_ON_FHIR_MAPPING from chord_metadata_service.restapi.semantic_mappings.hl7_genomics_mapping import HL7_GENOMICS_MAPPING -from fhirclient.models import (observation as obs, patient as p, extension, age, coding as c, - codeableconcept, specimen as s, identifier as fhir_indentifier, - annotation as a, range, quantity, fhirreference, - documentreference, attachment, fhirdate, condition as cond, - composition as comp - ) -##################### Generic FHIR conversion functions ##################### +# ===================== Generic FHIR conversion functions ===================== def fhir_coding_util(obj): @@ -57,7 +71,7 @@ def fhir_age(obj, mapping, field): age_extension.url = mapping if "start" in obj[field]: # Is an age range - age_extension.valueRange = range.Range() + age_extension.valueRange = range_.Range() age_extension.valueRange.low = quantity.Quantity() age_extension.valueRange.low.unit = obj[field]['start']['age'] age_extension.valueRange.high = quantity.Quantity() @@ -81,7 +95,7 @@ def check_disease_onset(disease): return False -##################### Phenopackets to FHIR class conversion functions ##################### +# ============== Phenopackets to FHIR class conversion functions ============== def fhir_patient(obj): @@ -141,7 +155,7 @@ def fhir_observation(obj): if 'description' in obj.keys(): observation.note = [] annotation = a.Annotation() - annotation.text = obj.get('description', None) + annotation.text = obj.get('description') observation.note.append(annotation) observation.code = fhir_codeable_concept(obj['type']) # required by FHIR specs but omitted by phenopackets, for now set for unknown @@ -158,8 +172,8 @@ def fhir_observation(obj): concept_extensions = codeable_concepts_fields( ['severity', 'modifier', 'onset'], 'phenotypic_feature', obj ) - for c in concept_extensions: - observation.extension.append(c) + for ce in concept_extensions: + observation.extension.append(ce) if 'evidence' in obj.keys(): evidence = extension.Extension() evidence.url = PHENOPACKETS_ON_FHIR_MAPPING['phenotypic_feature']['evidence']['url'] @@ -386,7 +400,7 @@ def fhir_composition(obj): return composition.as_json() -##################### FHIR to Phenopackets class conversion functions ##################### +# ============== FHIR to Phenopackets class conversion functions ============== # There is no guide to map FHIR to Phenopackets # SNOMED term to use as placeholder when collection method is not present in Specimen diff --git a/chord_metadata_service/restapi/jsonld_utils.py b/chord_metadata_service/restapi/jsonld_utils.py index bedddc7f5..3527d3643 100644 --- a/chord_metadata_service/restapi/jsonld_utils.py +++ b/chord_metadata_service/restapi/jsonld_utils.py @@ -1,9 +1,9 @@ -from .semantic_mappings.context import * +from .semantic_mappings.context import CONTEXT_TYPES, CONTEXT # utils to convert dataset json to json-ld -def obj_to_jsonld(obj, mapping) -> dict: +def obj_to_jsonld(obj: dict, mapping: str) -> dict: obj['@type'] = CONTEXT_TYPES[mapping]['type'] return obj @@ -34,38 +34,35 @@ def extra_properties_to_jsonld(extra_properties) -> list: return extra_properties +def _obj_identifiers_to_jsonld(obj): + if "identifier" in obj: + obj_to_jsonld(obj['identifier'], 'identifier') + if "alternate_identifiers" in obj: + for alt_id in obj["alternate_identifiers"]: + obj_to_jsonld(alt_id, "alternate_identifiers") + if "related_identifiers" in obj: + for rel_id in obj["related_identifiers"]: + obj_to_jsonld(rel_id, "related_identifiers") + + def spatial_coverage_to_jsonld(spatial_coverage) -> list: for sc in spatial_coverage: obj_to_jsonld(sc, 'spatial_coverage') - if 'identifier' in sc.keys(): - obj_to_jsonld(sc['identifier'], 'identifier') - if 'alternate_identifiers' in sc.keys(): - for alt_id in sc['alternate_identifiers']: - obj_to_jsonld(alt_id, 'alternate_identifiers') - if 'related_identifiers' in sc.keys(): - for rel_id in sc['related_identifiers']: - obj_to_jsonld(rel_id, 'related_identifiers') + _obj_identifiers_to_jsonld(sc) return spatial_coverage def distributions_to_jsonld(distributions) -> list: for distribution in distributions: obj_to_jsonld(distribution, 'distributions') - if 'identifier' in distribution.keys(): - obj_to_jsonld(distribution['identifier'], 'identifier') - if 'alternate_identifiers' in distribution.keys(): - for alt_id in distribution['alternate_identifiers']: - obj_to_jsonld(alt_id, 'alternate_identifiers') - if 'related_identifiers' in distribution.keys(): - for rel_id in distribution['related_identifiers']: - obj_to_jsonld(rel_id, 'related_identifiers') - if 'stored_in' in distribution.keys(): + _obj_identifiers_to_jsonld(distribution) + if 'stored_in' in distribution: obj_to_jsonld(distribution['stored_in'], 'stored_in') if 'dates' in distribution.keys(): dates_to_jsonld(distribution['dates']) - if 'licenses' in distribution.keys(): - for license in distribution['liceses']: - obj_to_jsonld(license, 'licenses') + if 'licenses' in distribution: + for license_ in distribution['liceses']: + obj_to_jsonld(license_, 'licenses') # access return distributions @@ -91,8 +88,8 @@ def dataset_to_jsonld(dataset): if 'information' in t.keys(): obj_to_jsonld(t['information'], 'annotation') if 'licenses' in dataset.keys(): - for license in dataset['licenses']: - obj_to_jsonld(license, 'licenses') + for license_ in dataset['licenses']: + obj_to_jsonld(license_, 'licenses') if 'extra_properties' in dataset.keys(): extra_properties_to_jsonld(dataset['extra_properties']) if 'alternate_identifiers' in dataset.keys(): diff --git a/chord_metadata_service/restapi/schemas.py b/chord_metadata_service/restapi/schemas.py index dd0034dad..ec6f9b2c2 100644 --- a/chord_metadata_service/restapi/schemas.py +++ b/chord_metadata_service/restapi/schemas.py @@ -17,7 +17,7 @@ ] -################################ Phenopackets based schemas ################################ +# ======================== Phenopackets based schemas ========================= ONTOLOGY_CLASS = describe_schema({ @@ -116,7 +116,7 @@ } -############################### FHIR INGEST SCHEMAS ############################### +# ============================ FHIR INGEST SCHEMAS ============================ # The schema used to validate FHIR data for ingestion diff --git a/chord_metadata_service/restapi/semantic_mappings/hl7_genomics_mapping.py b/chord_metadata_service/restapi/semantic_mappings/hl7_genomics_mapping.py index 984796de3..754a26278 100644 --- a/chord_metadata_service/restapi/semantic_mappings/hl7_genomics_mapping.py +++ b/chord_metadata_service/restapi/semantic_mappings/hl7_genomics_mapping.py @@ -21,4 +21,4 @@ "label": "Structural variant [Length]" } } -} \ No newline at end of file +} 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 dde8fc5dc..975e4e342 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 @@ -67,8 +67,10 @@ "biosample": { "title": "Biosample", "url": "http://ga4gh.org/fhir/phenopackets/StructureDefinition/Biosample", - "individual_age_at_collection": "http://ga4gh.org/fhir/phenopackets/StructureDefinition/biosample-individual-age-at-collection", - "histological_diagnosis": "http://ga4gh.org/fhir/phenopackets/StructureDefinition/biosample-histological-diagnosis", + "individual_age_at_collection": + "http://ga4gh.org/fhir/phenopackets/StructureDefinition/biosample-individual-age-at-collection", + "histological_diagnosis": + "http://ga4gh.org/fhir/phenopackets/StructureDefinition/biosample-histological-diagnosis", "tumor_progression": "http://ga4gh.org/fhir/phenopackets/StructureDefinition/biosample-tumor-progression", "tumor_grade": "http://ga4gh.org/fhir/phenopackets/StructureDefinition/biosample-tumor-grade", "diagnostic_markers": "http://ga4gh.org/fhir/phenopackets/StructureDefinition/biosample-diagnostic-markers", @@ -104,4 +106,4 @@ "onset": "http://ga4gh.org/fhir/phenopackets/StructureDefinition/disease-onset", "disease_stage": "http://ga4gh.org/fhir/phenopackets/StructureDefinition/disease-tumor-stage", } -} \ No newline at end of file +} diff --git a/chord_metadata_service/restapi/serializers.py b/chord_metadata_service/restapi/serializers.py index 1d866be87..65aca4611 100644 --- a/chord_metadata_service/restapi/serializers.py +++ b/chord_metadata_service/restapi/serializers.py @@ -1,5 +1,5 @@ -from rest_framework import serializers from collections import OrderedDict +from rest_framework import serializers from typing import Tuple diff --git a/chord_metadata_service/restapi/tests/test_fhir.py b/chord_metadata_service/restapi/tests/test_fhir.py index dd68205ae..56b6cc6ec 100644 --- a/chord_metadata_service/restapi/tests/test_fhir.py +++ b/chord_metadata_service/restapi/tests/test_fhir.py @@ -1,4 +1,15 @@ +from rest_framework import status from rest_framework.test import APITestCase + +from chord_metadata_service.patients.models import Individual +from chord_metadata_service.patients.tests.constants import VALID_INDIVIDUAL, VALID_INDIVIDUAL_2 +from chord_metadata_service.phenopackets.models import ( + MetaData, + Procedure, + Biosample, + Phenopacket, + PhenotypicFeature, +) from chord_metadata_service.phenopackets.tests.constants import ( VALID_INDIVIDUAL_1, VALID_META_DATA_2, @@ -11,10 +22,7 @@ valid_biosample_2, valid_phenotypic_feature, ) -from chord_metadata_service.patients.tests.constants import VALID_INDIVIDUAL, VALID_INDIVIDUAL_2 from chord_metadata_service.restapi.tests.utils import get_response -from chord_metadata_service.phenopackets.models import * -from rest_framework import status # Tests for FHIR conversion functions @@ -130,7 +138,7 @@ def setUp(self): self.valid_procedure = VALID_PROCEDURE_1 def test_get_fhir(self): - response = get_response('procedure-list', self.valid_procedure) + get_response('procedure-list', self.valid_procedure) get_resp = self.client.get('/api/procedures?format=fhir') self.assertEqual(get_resp.status_code, status.HTTP_200_OK) get_resp_obj = get_resp.json() @@ -191,7 +199,7 @@ def setUp(self): self.gene = VALID_GENE_1 def test_get_fhir(self): - response = get_response('gene-list', self.gene) + get_response('gene-list', self.gene) get_resp = self.client.get('/api/genes?format=fhir') self.assertEqual(get_resp.status_code, status.HTTP_200_OK) get_resp_obj = get_resp.json() @@ -211,7 +219,7 @@ def setUp(self): self.variant = VALID_VARIANT_1 def test_get_fhir(self): - response = get_response('variant-list', self.variant) + get_response('variant-list', self.variant) get_resp = self.client.get('/api/variants?format=fhir') self.assertEqual(get_resp.status_code, status.HTTP_200_OK) get_resp_obj = get_resp.json() @@ -230,7 +238,7 @@ def setUp(self): self.disease = VALID_DISEASE_1 def test_get_fhir(self): - response = get_response('disease-list', self.disease) + get_response('disease-list', self.disease) get_resp = self.client.get('/api/diseases?format=fhir') self.assertEqual(get_resp.status_code, status.HTTP_200_OK) get_resp_obj = get_resp.json() diff --git a/chord_metadata_service/restapi/tests/test_fhir_ingest.py b/chord_metadata_service/restapi/tests/test_fhir_ingest.py index 7e078c591..8c14b7156 100644 --- a/chord_metadata_service/restapi/tests/test_fhir_ingest.py +++ b/chord_metadata_service/restapi/tests/test_fhir_ingest.py @@ -1,11 +1,13 @@ +import uuid + +from django.core.exceptions import ValidationError from rest_framework.test import APITestCase -from chord_metadata_service.chord.models import * -from chord_metadata_service.phenopackets.models import * -from .constants import INVALID_FHIR_BUNDLE_1, INVALID_SUBJECT_NOT_PRESENT -from chord_metadata_service.chord.tests.constants import VALID_DATA_USE_1 + from chord_metadata_service.chord.data_types import DATA_TYPE_PHENOPACKET +from chord_metadata_service.chord.models import Project, Dataset, TableOwnership, Table +from chord_metadata_service.chord.tests.constants import VALID_DATA_USE_1 from chord_metadata_service.restapi.fhir_ingest import ingest_patients, ingest_observations -import uuid +from .constants import INVALID_FHIR_BUNDLE_1, INVALID_SUBJECT_NOT_PRESENT class TestFhirIngest(APITestCase): diff --git a/chord_metadata_service/restapi/tests/test_jsonld.py b/chord_metadata_service/restapi/tests/test_jsonld.py index c5fa01608..cc124b670 100644 --- a/chord_metadata_service/restapi/tests/test_jsonld.py +++ b/chord_metadata_service/restapi/tests/test_jsonld.py @@ -1,8 +1,11 @@ from rest_framework.test import APITestCase from django.test import override_settings from rest_framework import status -from chord_metadata_service.chord.tests.constants import (VALID_PROJECT_1, - VALID_DATS_CREATORS, dats_dataset) +from chord_metadata_service.chord.tests.constants import ( + VALID_PROJECT_1, + VALID_DATS_CREATORS, + dats_dataset, +) from chord_metadata_service.restapi.tests.utils import get_response @@ -13,8 +16,7 @@ def setUp(self) -> None: self.project = project.json() self.creators = VALID_DATS_CREATORS self.dataset = dats_dataset(self.project['identifier'], self.creators) - resp = get_response('dataset-list', self.dataset) - + get_response('dataset-list', self.dataset) def test_jsonld(self): get_resp = self.client.get('/api/datasets?format=json-ld') diff --git a/chord_metadata_service/restapi/urls.py b/chord_metadata_service/restapi/urls.py index f8b4ebddf..8236388c2 100644 --- a/chord_metadata_service/restapi/urls.py +++ b/chord_metadata_service/restapi/urls.py @@ -1,14 +1,16 @@ from django.urls import path, include from rest_framework import routers + from chord_metadata_service.chord import api_views as chord_views from chord_metadata_service.experiments import api_views as experiment_views +from chord_metadata_service.mcode import api_views as mcode_views from chord_metadata_service.patients import api_views as individual_views from chord_metadata_service.phenopackets import api_views as phenopacket_views -from chord_metadata_service.mcode import api_views as mcode_views - from chord_metadata_service.resources import api_views as resources_views +__all__ = ["router", "urlpatterns"] + router = routers.DefaultRouter(trailing_slash=False) From 117be6541e7685dd2dc7b0338f11c592e7a09688 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Thu, 18 Jun 2020 12:45:17 -0400 Subject: [PATCH 149/190] Lint / touch up other apps --- chord_metadata_service/experiments/models.py | 5 +- .../patients/descriptions.py | 2 +- .../commands/patients_build_index.py | 1 + .../patients/tests/es_mocks.py | 1 - chord_metadata_service/phenopackets/admin.py | 27 ++-- .../phenopackets/api_views.py | 51 ++++--- .../commands/phenopackets_build_index.py | 12 +- .../phenopackets/tests/constants.py | 3 +- .../phenopackets/tests/test_api.py | 131 ++++++++---------- .../phenopackets/tests/test_models.py | 130 ++++++++--------- chord_metadata_service/resources/models.py | 2 +- 11 files changed, 180 insertions(+), 185 deletions(-) diff --git a/chord_metadata_service/experiments/models.py b/chord_metadata_service/experiments/models.py index dd7caa0e0..b70bf1563 100644 --- a/chord_metadata_service/experiments/models.py +++ b/chord_metadata_service/experiments/models.py @@ -4,7 +4,6 @@ from chord_metadata_service.restapi.models import IndexableMixin from chord_metadata_service.restapi.description_utils import rec_help from chord_metadata_service.restapi.validators import ontology_list_validator, key_value_validator -from chord_metadata_service.patients.models import Individual from chord_metadata_service.phenopackets.models import Biosample import chord_metadata_service.experiments.descriptions as d @@ -53,8 +52,8 @@ class Experiment(models.Model, IndexableMixin): experiment_type = CharField(max_length=30, help_text=rec_help(d.EXPERIMENT, 'experiment_type')) experiment_ontology = JSONField(blank=True, default=list, validators=[ontology_list_validator], help_text=rec_help(d.EXPERIMENT, 'experiment_ontology')) - molecule_ontology = JSONField(blank=True, default=list, validators=[ontology_list_validator], - help_text=rec_help(d.EXPERIMENT, 'molecule_ontology')) + molecule_ontology = JSONField(blank=True, default=list, validators=[ontology_list_validator], + help_text=rec_help(d.EXPERIMENT, 'molecule_ontology')) molecule = CharField(choices=MOLECULE, max_length=20, blank=True, null=True, help_text=rec_help(d.EXPERIMENT, 'molecule')) library_strategy = CharField(choices=LIBRARY_STRATEGY, max_length=25, diff --git a/chord_metadata_service/patients/descriptions.py b/chord_metadata_service/patients/descriptions.py index 21938c7e6..f66bdf961 100644 --- a/chord_metadata_service/patients/descriptions.py +++ b/chord_metadata_service/patients/descriptions.py @@ -19,7 +19,7 @@ "from the NCBI Taxonomy resource are used, e.g. NCBITaxon:9606 for humans"), # FHIR-specific - "active": { + "active": { "description": "Whether a patient's record is in active use.", "help": "FHIR-specific property." }, diff --git a/chord_metadata_service/patients/management/commands/patients_build_index.py b/chord_metadata_service/patients/management/commands/patients_build_index.py index 3fc27f136..44206a81a 100644 --- a/chord_metadata_service/patients/management/commands/patients_build_index.py +++ b/chord_metadata_service/patients/management/commands/patients_build_index.py @@ -15,6 +15,7 @@ class Command(BaseCommand): Takes every individual in the DB, port them over to FHIR-compliant JSON and upload them into elasticsearch """ + def handle(self, *args, **options): # TODO: currently only place we create the index, will have to review if es: diff --git a/chord_metadata_service/patients/tests/es_mocks.py b/chord_metadata_service/patients/tests/es_mocks.py index 9581c4c71..f00664ffe 100644 --- a/chord_metadata_service/patients/tests/es_mocks.py +++ b/chord_metadata_service/patients/tests/es_mocks.py @@ -1,5 +1,4 @@ from unittest.mock import Mock -from chord_metadata_service.metadata.elastic import es INDEXING_SUCCESS = { diff --git a/chord_metadata_service/phenopackets/admin.py b/chord_metadata_service/phenopackets/admin.py index 531f85ca1..b1f598de1 100644 --- a/chord_metadata_service/phenopackets/admin.py +++ b/chord_metadata_service/phenopackets/admin.py @@ -1,61 +1,62 @@ from django.contrib import admin -from .models import * +from . import models as m -@admin.register(MetaData) +@admin.register(m.MetaData) class MetaDataAdmin(admin.ModelAdmin): pass -@admin.register(PhenotypicFeature) +@admin.register(m.PhenotypicFeature) class PhenotypicFeatureAdmin(admin.ModelAdmin): pass -@admin.register(Procedure) +@admin.register(m.Procedure) class ProcedureAdmin(admin.ModelAdmin): pass -@admin.register(HtsFile) +@admin.register(m.HtsFile) class HtsFileAdmin(admin.ModelAdmin): pass -@admin.register(Gene) +@admin.register(m.Gene) class GeneAdmin(admin.ModelAdmin): pass -@admin.register(Variant) +@admin.register(m.Variant) class VariantAdmin(admin.ModelAdmin): pass -@admin.register(Disease) +@admin.register(m.Disease) class DiseaseAdmin(admin.ModelAdmin): pass -@admin.register(Biosample) + +@admin.register(m.Biosample) class BiosampleAdmin(admin.ModelAdmin): pass -@admin.register(Phenopacket) +@admin.register(m.Phenopacket) class PhenopacketAdmin(admin.ModelAdmin): pass -@admin.register(GenomicInterpretation) +@admin.register(m.GenomicInterpretation) class GenomicInterpretationAdmin(admin.ModelAdmin): pass -@admin.register(Diagnosis) +@admin.register(m.Diagnosis) class DiagnosisAdmin(admin.ModelAdmin): pass -@admin.register(Interpretation) +@admin.register(m.Interpretation) class InterpretationAdmin(admin.ModelAdmin): pass diff --git a/chord_metadata_service/phenopackets/api_views.py b/chord_metadata_service/phenopackets/api_views.py index f4c4a65c3..1b5e72ac3 100644 --- a/chord_metadata_service/phenopackets/api_views.py +++ b/chord_metadata_service/phenopackets/api_views.py @@ -7,8 +7,7 @@ from chord_metadata_service.restapi.api_renderers import PhenopacketsRenderer, FHIRRenderer from chord_metadata_service.restapi.pagination import LargeResultsSetPagination from chord_metadata_service.phenopackets.schemas import PHENOPACKET_SCHEMA -from .models import * -from .serializers import * +from . import models as m, serializers as s class PhenopacketsModelViewSet(viewsets.ModelViewSet): @@ -29,8 +28,8 @@ class PhenotypicFeatureViewSet(ExtendedPhenopacketsModelViewSet): Create a new phenotypic feature """ - queryset = PhenotypicFeature.objects.all().order_by("id") - serializer_class = PhenotypicFeatureSerializer + queryset = m.PhenotypicFeature.objects.all().order_by("id") + serializer_class = s.PhenotypicFeatureSerializer class ProcedureViewSet(ExtendedPhenopacketsModelViewSet): @@ -42,8 +41,8 @@ class ProcedureViewSet(ExtendedPhenopacketsModelViewSet): Create a new procedure """ - queryset = Procedure.objects.all().order_by("id") - serializer_class = ProcedureSerializer + queryset = m.Procedure.objects.all().order_by("id") + serializer_class = s.ProcedureSerializer class HtsFileViewSet(ExtendedPhenopacketsModelViewSet): @@ -55,8 +54,8 @@ class HtsFileViewSet(ExtendedPhenopacketsModelViewSet): Create a new HTS file """ - queryset = HtsFile.objects.all().order_by("uri") - serializer_class = HtsFileSerializer + queryset = m.HtsFile.objects.all().order_by("uri") + serializer_class = s.HtsFileSerializer class GeneViewSet(ExtendedPhenopacketsModelViewSet): @@ -68,8 +67,8 @@ class GeneViewSet(ExtendedPhenopacketsModelViewSet): Create a new gene """ - queryset = Gene.objects.all().order_by("id") - serializer_class = GeneSerializer + queryset = m.Gene.objects.all().order_by("id") + serializer_class = s.GeneSerializer class VariantViewSet(ExtendedPhenopacketsModelViewSet): @@ -81,8 +80,8 @@ class VariantViewSet(ExtendedPhenopacketsModelViewSet): Create a new variant """ - queryset = Variant.objects.all().order_by("id") - serializer_class = VariantSerializer + queryset = m.Variant.objects.all().order_by("id") + serializer_class = s.VariantSerializer class DiseaseViewSet(ExtendedPhenopacketsModelViewSet): @@ -94,8 +93,8 @@ class DiseaseViewSet(ExtendedPhenopacketsModelViewSet): Create a new disease """ - queryset = Disease.objects.all().order_by("id") - serializer_class = DiseaseSerializer + queryset = m.Disease.objects.all().order_by("id") + serializer_class = s.DiseaseSerializer META_DATA_PREFETCH = ( @@ -112,8 +111,8 @@ class MetaDataViewSet(PhenopacketsModelViewSet): Create a new metadata record """ - queryset = MetaData.objects.all().prefetch_related(*META_DATA_PREFETCH).order_by("id") - serializer_class = MetaDataSerializer + queryset = m.MetaData.objects.all().prefetch_related(*META_DATA_PREFETCH).order_by("id") + serializer_class = s.MetaDataSerializer BIOSAMPLE_PREFETCH = ( @@ -132,8 +131,8 @@ class BiosampleViewSet(ExtendedPhenopacketsModelViewSet): post: Create a new biosample """ - queryset = Biosample.objects.all().prefetch_related(*BIOSAMPLE_PREFETCH).order_by("id") - serializer_class = BiosampleSerializer + queryset = m.Biosample.objects.all().prefetch_related(*BIOSAMPLE_PREFETCH).order_by("id") + serializer_class = s.BiosampleSerializer PHENOPACKET_PREFETCH = ( @@ -157,8 +156,8 @@ class PhenopacketViewSet(ExtendedPhenopacketsModelViewSet): Create a new phenopacket """ - queryset = Phenopacket.objects.all().prefetch_related(*PHENOPACKET_PREFETCH).order_by("id") - serializer_class = PhenopacketSerializer + queryset = m.Phenopacket.objects.all().prefetch_related(*PHENOPACKET_PREFETCH).order_by("id") + serializer_class = s.PhenopacketSerializer class GenomicInterpretationViewSet(PhenopacketsModelViewSet): @@ -170,8 +169,8 @@ class GenomicInterpretationViewSet(PhenopacketsModelViewSet): Create a new genomic interpretation """ - queryset = GenomicInterpretation.objects.all().order_by("id") - serializer_class = GenomicInterpretationSerializer + queryset = m.GenomicInterpretation.objects.all().order_by("id") + serializer_class = s.GenomicInterpretationSerializer class DiagnosisViewSet(PhenopacketsModelViewSet): @@ -183,8 +182,8 @@ class DiagnosisViewSet(PhenopacketsModelViewSet): Create a new diagnosis """ - queryset = Diagnosis.objects.all().order_by("id") - serializer_class = DiagnosisSerializer + queryset = m.Diagnosis.objects.all().order_by("id") + serializer_class = s.DiagnosisSerializer class InterpretationViewSet(PhenopacketsModelViewSet): @@ -196,8 +195,8 @@ class InterpretationViewSet(PhenopacketsModelViewSet): Create a new interpretation """ - queryset = Interpretation.objects.all().order_by("id") - serializer_class = InterpretationSerializer + queryset = m.Interpretation.objects.all().order_by("id") + serializer_class = s.InterpretationSerializer @api_view(["GET"]) diff --git a/chord_metadata_service/phenopackets/management/commands/phenopackets_build_index.py b/chord_metadata_service/phenopackets/management/commands/phenopackets_build_index.py index 81495ab10..719e2cfe5 100644 --- a/chord_metadata_service/phenopackets/management/commands/phenopackets_build_index.py +++ b/chord_metadata_service/phenopackets/management/commands/phenopackets_build_index.py @@ -24,9 +24,10 @@ class Command(BaseCommand): help = """ - Takes every phenopacket-related data in the DB, port them over + Takes every phenopacket-related data in the DB, port them over to FHIR-compliant JSON and upload them into elasticsearch """ + def handle(self, *args, **options): # TODO: currently only place we create the index, will have to review if es: @@ -48,18 +49,21 @@ def handle(self, *args, **options): for biosample in biosamples: created_or_updated = build_biosample_index(biosample) - logger.info(f"{created_or_updated} index for biosample ID {biosample.id} indexed id {biosample.index_id}") + logger.info(f"{created_or_updated} index for biosample ID {biosample.id} indexed id " + f"{biosample.index_id}") features = PhenotypicFeature.objects.all() for feature in features: created_or_updated = build_phenotypicfeature_index(feature) - logger.info(f"{created_or_updated} index for phenotypic feature ID {feature.index_id} indexed ID {feature.index_id}") + logger.info(f"{created_or_updated} index for phenotypic feature ID {feature.index_id} indexed ID " + f"{feature.index_id}") phenopackets = Phenopacket.objects.all() for phenopacket in phenopackets: created_or_updated = build_phenopacket_index(phenopacket) - logger.info(f"{created_or_updated} index for phenopacket ID {phenopacket.id} indexed ID {phenopacket.index_id}") + logger.info(f"{created_or_updated} index for phenopacket ID {phenopacket.id} indexed ID " + f"{phenopacket.index_id}") else: logger.error("No connection to elasticsearch") diff --git a/chord_metadata_service/phenopackets/tests/constants.py b/chord_metadata_service/phenopackets/tests/constants.py index cbef37fcd..8cd50c815 100644 --- a/chord_metadata_service/phenopackets/tests/constants.py +++ b/chord_metadata_service/phenopackets/tests/constants.py @@ -31,7 +31,8 @@ "external_references": [ { "id": "PMID:30808312", - "description": "Bao M, et al. COL6A1 mutation leading to Bethlem myopathy with recurrent hematuria: a case report. BMC Neurol. 2019;19(1):32." + "description": "Bao M, et al. COL6A1 mutation leading to Bethlem myopathy with recurrent hematuria: a case " + "report. BMC Neurol. 2019;19(1):32." }, { "id": "PMID:3080844", diff --git a/chord_metadata_service/phenopackets/tests/test_api.py b/chord_metadata_service/phenopackets/tests/test_api.py index 8de6b4cfd..5c7b06cac 100644 --- a/chord_metadata_service/phenopackets/tests/test_api.py +++ b/chord_metadata_service/phenopackets/tests/test_api.py @@ -1,18 +1,7 @@ from rest_framework import status from rest_framework.test import APITestCase -from .constants import * -from ..models import * -from ..serializers import ( - BiosampleSerializer, - DiagnosisSerializer, - DiseaseSerializer, - GeneSerializer, - GenomicInterpretationSerializer, - MetaDataSerializer, - PhenopacketSerializer, - PhenotypicFeatureSerializer, - VariantSerializer, -) +from . import constants as c +from .. import models as m, serializers as s from chord_metadata_service.restapi.tests.utils import get_response @@ -20,9 +9,9 @@ class CreateBiosampleTest(APITestCase): """ Test module for creating an Biosample. """ def setUp(self): - self.individual = Individual.objects.create(**VALID_INDIVIDUAL_1) - self.procedure = VALID_PROCEDURE_1 - self.valid_payload = valid_biosample_1(self.individual.id, self.procedure) + self.individual = m.Individual.objects.create(**c.VALID_INDIVIDUAL_1) + self.procedure = c.VALID_PROCEDURE_1 + self.valid_payload = c.valid_biosample_1(self.individual.id, self.procedure) self.invalid_payload = { "id": "biosample:1", "individual": self.individual.id, @@ -61,8 +50,8 @@ def test_create_biosample(self): response = get_response('biosample-list', self.valid_payload) self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(Biosample.objects.count(), 1) - self.assertEqual(Biosample.objects.get().id, 'biosample_id:1') + self.assertEqual(m.Biosample.objects.count(), 1) + self.assertEqual(m.Biosample.objects.get().id, 'biosample_id:1') def test_create_invalid_biosample(self): """ POST a new biosample with invalid data. """ @@ -70,28 +59,28 @@ def test_create_invalid_biosample(self): invalid_response = get_response('biosample-list', self.invalid_payload) self.assertEqual( invalid_response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(Biosample.objects.count(), 0) + self.assertEqual(m.Biosample.objects.count(), 0) def test_seriliazer_validate_invalid(self): - serializer = BiosampleSerializer(data=self.invalid_payload) + serializer = s.BiosampleSerializer(data=self.invalid_payload) self.assertEqual(serializer.is_valid(), False) def test_seriliazer_validate_valid(self): - serializer = BiosampleSerializer(data=self.valid_payload) + serializer = s.BiosampleSerializer(data=self.valid_payload) self.assertEqual(serializer.is_valid(), True) class CreatePhenotypicFeatureTest(APITestCase): def setUp(self): - valid_payload = valid_phenotypic_feature() + valid_payload = c.valid_phenotypic_feature() valid_payload.pop('pftype', None) valid_payload['type'] = { "id": "HP:0000520", "label": "Proptosis" } self.valid_phenotypic_feature = valid_payload - invalid_payload = invalid_phenotypic_feature() + invalid_payload = c.invalid_phenotypic_feature() invalid_payload['type'] = { "id": "HP:0000520", "label": "Proptosis" @@ -103,19 +92,19 @@ def test_create_phenotypic_feature(self): response = get_response('phenotypicfeature-list', self.valid_phenotypic_feature) self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(PhenotypicFeature.objects.count(), 1) + self.assertEqual(m.PhenotypicFeature.objects.count(), 1) def test_modifier(self): - serializer = PhenotypicFeatureSerializer(data=self.invalid_phenotypic_feature) + serializer = s.PhenotypicFeatureSerializer(data=self.invalid_phenotypic_feature) self.assertEqual(serializer.is_valid(), False) class CreateProcedureTest(APITestCase): def setUp(self): - self.valid_procedure = VALID_PROCEDURE_1 - self.duplicate_procedure = VALID_PROCEDURE_1 - self.valid_procedure_duplicate_code = VALID_PROCEDURE_2 + self.valid_procedure = c.VALID_PROCEDURE_1 + self.duplicate_procedure = c.VALID_PROCEDURE_1 + self.valid_procedure_duplicate_code = c.VALID_PROCEDURE_2 def test_procedure(self): response = get_response('procedure-list', self.valid_procedure) @@ -126,122 +115,122 @@ def test_procedure(self): self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response_duplicate.status_code, status.HTTP_201_CREATED) self.assertEqual(response_duplicate_code.status_code, status.HTTP_201_CREATED) - self.assertEqual(Procedure.objects.count(), 2) + self.assertEqual(m.Procedure.objects.count(), 2) class CreateHtsFileTest(APITestCase): def setUp(self): - self.hts_file = VALID_HTS_FILE + self.hts_file = c.VALID_HTS_FILE def test_hts_file(self): response = get_response('htsfile-list', self.hts_file) self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(HtsFile.objects.count(), 1) + self.assertEqual(m.HtsFile.objects.count(), 1) class CreateGeneTest(APITestCase): def setUp(self): - self.gene = VALID_GENE_1 - self.duplicate_gene = DUPLICATE_GENE_2 - self.invalid_gene = INVALID_GENE_2 + self.gene = c.VALID_GENE_1 + self.duplicate_gene = c.DUPLICATE_GENE_2 + self.invalid_gene = c.INVALID_GENE_2 def test_gene(self): response = get_response('gene-list', self.gene) response_duplicate = get_response('htsfile-list', self.duplicate_gene) self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response_duplicate.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(Gene.objects.count(), 1) + self.assertEqual(m.Gene.objects.count(), 1) def test_alternate_ids(self): - serializer = GeneSerializer(data=self.invalid_gene) + serializer = s.GeneSerializer(data=self.invalid_gene) self.assertEqual(serializer.is_valid(), False) class CreateVariantTest(APITestCase): def setUp(self): - self.variant = VALID_VARIANT_1 - self.variant_2 = VALID_VARIANT_2 + self.variant = c.VALID_VARIANT_1 + self.variant_2 = c.VALID_VARIANT_2 def test_variant(self): response = get_response('variant-list', self.variant) - serializer = VariantSerializer(data=self.variant) + serializer = s.VariantSerializer(data=self.variant) self.assertEqual(serializer.is_valid(), True) self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(Variant.objects.count(), 1) + self.assertEqual(m.Variant.objects.count(), 1) def test_to_represenation(self): response = get_response('variant-list', self.variant_2) - serializer = VariantSerializer(data=self.variant) + serializer = s.VariantSerializer(data=self.variant) self.assertEqual(serializer.is_valid(), True) self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(Variant.objects.count(), 1) + self.assertEqual(m.Variant.objects.count(), 1) class CreateDiseaseTest(APITestCase): def setUp(self): - self.disease = VALID_DISEASE_1 - self.invalid_disease = INVALID_DISEASE_2 + self.disease = c.VALID_DISEASE_1 + self.invalid_disease = c.INVALID_DISEASE_2 def test_disease(self): response = get_response('disease-list', self.disease) - serializer = DiseaseSerializer(data=self.disease) + serializer = s.DiseaseSerializer(data=self.disease) self.assertEqual(serializer.is_valid(), True) self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(Disease.objects.count(), 1) + self.assertEqual(m.Disease.objects.count(), 1) def test_invalid_disease(self): - serializer = DiseaseSerializer(data=self.invalid_disease) + serializer = s.DiseaseSerializer(data=self.invalid_disease) self.assertEqual(serializer.is_valid(), False) - self.assertEqual(Disease.objects.count(), 0) + self.assertEqual(m.Disease.objects.count(), 0) class CreateMetaDataTest(APITestCase): def setUp(self): - self.metadata = VALID_META_DATA_2 + self.metadata = c.VALID_META_DATA_2 def test_metadata(self): response = get_response('metadata-list', self.metadata) self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(MetaData.objects.count(), 1) + self.assertEqual(m.MetaData.objects.count(), 1) def test_serializer(self): # is_valid() calls validation on serializer - serializer = MetaDataSerializer(data=self.metadata) + serializer = s.MetaDataSerializer(data=self.metadata) self.assertEqual(serializer.is_valid(), True) class CreatePhenopacketTest(APITestCase): def setUp(self): - individual = Individual.objects.create(**VALID_INDIVIDUAL_1) + individual = m.Individual.objects.create(**c.VALID_INDIVIDUAL_1) self.subject = individual.id - meta = MetaData.objects.create(**VALID_META_DATA_2) + meta = m.MetaData.objects.create(**c.VALID_META_DATA_2) self.metadata = meta.id - self.phenopacket = valid_phenopacket( + self.phenopacket = c.valid_phenopacket( subject=self.subject, meta_data=self.metadata) def test_phenopacket(self): response = get_response('phenopacket-list', self.phenopacket) self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(Phenopacket.objects.count(), 1) + self.assertEqual(m.Phenopacket.objects.count(), 1) def test_serializer(self): - serializer = PhenopacketSerializer(data=self.phenopacket) + serializer = s.PhenopacketSerializer(data=self.phenopacket) self.assertEqual(serializer.is_valid(), True) class CreateGenomicInterpretationTest(APITestCase): def setUp(self): - self.gene = Gene.objects.create(**VALID_GENE_1).id - self.variant = Variant.objects.create(**VALID_VARIANT_1).id - self.genomic_interpretation = valid_genomic_interpretation( + self.gene = m.Gene.objects.create(**c.VALID_GENE_1).id + self.variant = m.Variant.objects.create(**c.VALID_VARIANT_1).id + self.genomic_interpretation = c.valid_genomic_interpretation( gene=self.gene, variant=self.variant ) @@ -250,40 +239,40 @@ def test_genomic_interpretation(self): response = get_response('genomicinterpretation-list', self.genomic_interpretation) self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(GenomicInterpretation.objects.count(), 1) + self.assertEqual(m.GenomicInterpretation.objects.count(), 1) def test_serializer(self): - serializer = GenomicInterpretationSerializer(data=self.genomic_interpretation) + serializer = s.GenomicInterpretationSerializer(data=self.genomic_interpretation) self.assertEqual(serializer.is_valid(), True) class CreateDiagnosisTest(APITestCase): def setUp(self): - self.disease = Disease.objects.create(**VALID_DISEASE_1).id - self.diagnosis = valid_diagnosis(self.disease) + self.disease = m.Disease.objects.create(**c.VALID_DISEASE_1).id + self.diagnosis = c.valid_diagnosis(self.disease) def test_diagnosis(self): response = get_response('diagnosis-list', self.diagnosis) self.assertEqual(response.status_code, status.HTTP_201_CREATED) - serializer = DiagnosisSerializer(data=self.diagnosis) + serializer = s.DiagnosisSerializer(data=self.diagnosis) self.assertEqual(serializer.is_valid(), True) class CreateInterpretationTest(APITestCase): def setUp(self): - self.individual = Individual.objects.create(**VALID_INDIVIDUAL_1) - self.metadata = MetaData.objects.create(**VALID_META_DATA_2) - self.phenopacket = Phenopacket.objects.create(**valid_phenopacket( + self.individual = m.Individual.objects.create(**c.VALID_INDIVIDUAL_1) + self.metadata = m.MetaData.objects.create(**c.VALID_META_DATA_2) + self.phenopacket = m.Phenopacket.objects.create(**c.valid_phenopacket( subject=self.individual, meta_data=self.metadata) ).id - self.metadata_interpretation = MetaData.objects.create(**VALID_META_DATA_2).id - self.disease = Disease.objects.create(**VALID_DISEASE_1) - self.diagnosis = Diagnosis.objects.create(**valid_diagnosis(self.disease)).id - self.interpretation = valid_interpretation( + self.metadata_interpretation = m.MetaData.objects.create(**c.VALID_META_DATA_2).id + self.disease = m.Disease.objects.create(**c.VALID_DISEASE_1) + self.diagnosis = m.Diagnosis.objects.create(**c.valid_diagnosis(self.disease)).id + self.interpretation = c.valid_interpretation( phenopacket=self.phenopacket, meta_data=self.metadata_interpretation ) diff --git a/chord_metadata_service/phenopackets/tests/test_models.py b/chord_metadata_service/phenopackets/tests/test_models.py index 96d55ee71..4faddd917 100644 --- a/chord_metadata_service/phenopackets/tests/test_models.py +++ b/chord_metadata_service/phenopackets/tests/test_models.py @@ -1,21 +1,23 @@ +from django.core.exceptions import ValidationError from django.db.utils import IntegrityError from django.test import TestCase -from ..models import * -from .constants import * + from chord_metadata_service.resources.tests.constants import VALID_RESOURCE_1, VALID_RESOURCE_2 +from . import constants as c +from .. import models as m class BiosampleTest(TestCase): """ Test module for Biosample model """ def setUp(self): - self.individual = Individual.objects.create(**VALID_INDIVIDUAL_1) - self.procedure = Procedure.objects.create(**VALID_PROCEDURE_1) - self.biosample_1 = Biosample.objects.create(**valid_biosample_1(self.individual, self.procedure)) - self.biosample_2 = Biosample.objects.create(**valid_biosample_2(None, self.procedure)) - self.meta_data = MetaData.objects.create(**VALID_META_DATA_1) + self.individual = m.Individual.objects.create(**c.VALID_INDIVIDUAL_1) + self.procedure = m.Procedure.objects.create(**c.VALID_PROCEDURE_1) + self.biosample_1 = m.Biosample.objects.create(**c.valid_biosample_1(self.individual, self.procedure)) + self.biosample_2 = m.Biosample.objects.create(**c.valid_biosample_2(None, self.procedure)) + self.meta_data = m.MetaData.objects.create(**c.VALID_META_DATA_1) - self.phenopacket = Phenopacket.objects.create( + self.phenopacket = m.Phenopacket.objects.create( id="phenopacket_id:1", subject=self.individual, meta_data=self.meta_data, @@ -23,7 +25,7 @@ def setUp(self): self.phenopacket.biosamples.set([self.biosample_1, self.biosample_2]) def test_biosample(self): - biosample_one = Biosample.objects.get( + biosample_one = m.Biosample.objects.get( tumor_progression__label='Primary Malignant Neoplasm', sampled_tissue__label__icontains='urinary bladder' ) @@ -42,33 +44,33 @@ class PhenotypicFeatureTest(TestCase): """ Test module for PhenotypicFeature model. """ def setUp(self): - self.individual_1 = Individual.objects.create(**VALID_INDIVIDUAL_1) - self.individual_2 = Individual.objects.create(**VALID_INDIVIDUAL_2) - self.procedure = Procedure.objects.create(**VALID_PROCEDURE_1) - self.biosample_1 = Biosample.objects.create(**valid_biosample_1( + self.individual_1 = m.Individual.objects.create(**c.VALID_INDIVIDUAL_1) + self.individual_2 = m.Individual.objects.create(**c.VALID_INDIVIDUAL_2) + self.procedure = m.Procedure.objects.create(**c.VALID_PROCEDURE_1) + self.biosample_1 = m.Biosample.objects.create(**c.valid_biosample_1( self.individual_1, self.procedure) ) - self.biosample_2 = Biosample.objects.create(**valid_biosample_2( + self.biosample_2 = m.Biosample.objects.create(**c.valid_biosample_2( self.individual_2, self.procedure) ) - self.meta_data = MetaData.objects.create(**VALID_META_DATA_1) - self.phenopacket = Phenopacket.objects.create( + self.meta_data = m.MetaData.objects.create(**c.VALID_META_DATA_1) + self.phenopacket = m.Phenopacket.objects.create( id='phenopacket_id:1', subject=self.individual_2, meta_data=self.meta_data, ) - self.phenotypic_feature_1 = PhenotypicFeature.objects.create( - **valid_phenotypic_feature(biosample=self.biosample_1)) - self.phenotypic_feature_2 = PhenotypicFeature.objects.create( - **valid_phenotypic_feature(biosample=self.biosample_2, phenopacket=self.phenopacket)) + self.phenotypic_feature_1 = m.PhenotypicFeature.objects.create( + **c.valid_phenotypic_feature(biosample=self.biosample_1)) + self.phenotypic_feature_2 = m.PhenotypicFeature.objects.create( + **c.valid_phenotypic_feature(biosample=self.biosample_2, phenopacket=self.phenopacket)) def test_phenotypic_feature(self): - phenotypic_feature_query = PhenotypicFeature.objects.filter( + phenotypic_feature_query = m.PhenotypicFeature.objects.filter( severity__label='Mild', pftype__label='Proptosis' ) - phenotypic_feature_2 = PhenotypicFeature.objects.filter(phenopacket__id='phenopacket_id:1') - self.assertEqual(PhenotypicFeature.objects.count(), 2) + phenotypic_feature_2 = m.PhenotypicFeature.objects.filter(phenopacket__id='phenopacket_id:1') + self.assertEqual(m.PhenotypicFeature.objects.count(), 2) self.assertEqual(phenotypic_feature_query.count(), 2) self.assertEqual(phenotypic_feature_2.count(), 1) @@ -78,24 +80,24 @@ def test_phenotypic_feature_str(self): class ProcedureTest(TestCase): def setUp(self): - self.procedure_1 = Procedure.objects.create(**VALID_PROCEDURE_1) - self.procedure_2 = Procedure.objects.create(**VALID_PROCEDURE_2) + self.procedure_1 = m.Procedure.objects.create(**c.VALID_PROCEDURE_1) + self.procedure_2 = m.Procedure.objects.create(**c.VALID_PROCEDURE_2) def test_procedure(self): - procedure_query_1 = Procedure.objects.filter( + procedure_query_1 = m.Procedure.objects.filter( body_site__label__icontains='arm' ) - procedure_query_2 = Procedure.objects.filter(code__id='NCIT:C28743') + procedure_query_2 = m.Procedure.objects.filter(code__id='NCIT:C28743') self.assertEqual(procedure_query_1.count(), 2) self.assertEqual(procedure_query_2.count(), 2) class HtsFileTest(TestCase): def setUp(self): - self.hts_file = HtsFile.objects.create(**VALID_HTS_FILE) + self.hts_file = m.HtsFile.objects.create(**c.VALID_HTS_FILE) def test_hts_file(self): - hts_file = HtsFile.objects.get(genome_assembly='GRCh38') + hts_file = m.HtsFile.objects.get(genome_assembly='GRCh38') self.assertEqual(hts_file.uri, 'https://data.example/genomes/germline_wgs.vcf.gz') def test_hts_file_str(self): @@ -104,13 +106,13 @@ def test_hts_file_str(self): class GeneTest(TestCase): def setUp(self): - self.gene_1 = Gene.objects.create(**VALID_GENE_1) + self.gene_1 = m.Gene.objects.create(**c.VALID_GENE_1) def test_gene(self): - gene_1 = Gene.objects.get(id='HGNC:347') + gene_1 = m.Gene.objects.get(id='HGNC:347') self.assertEqual(gene_1.symbol, 'ETF1') with self.assertRaises(IntegrityError): - Gene.objects.create(**DUPLICATE_GENE_2) + m.Gene.objects.create(**c.DUPLICATE_GENE_2) def test_gene_str(self): self.assertEqual(str(self.gene_1), "HGNC:347") @@ -118,10 +120,10 @@ def test_gene_str(self): class VariantTest(TestCase): def setUp(self): - self.variant = Variant.objects.create(**VALID_VARIANT_1) + self.variant = m.Variant.objects.create(**c.VALID_VARIANT_1) def test_variant(self): - variant_query = Variant.objects.filter(zygosity__id='NCBITaxon:9606') + variant_query = m.Variant.objects.filter(zygosity__id='NCBITaxon:9606') self.assertEqual(variant_query.count(), 1) def test_variant_str(self): @@ -130,10 +132,10 @@ def test_variant_str(self): class DiseaseTest(TestCase): def setUp(self): - self.disease_1 = Disease.objects.create(**VALID_DISEASE_1) + self.disease_1 = m.Disease.objects.create(**c.VALID_DISEASE_1) def test_disease(self): - disease_query = Disease.objects.filter(term__id='OMIM:164400') + disease_query = m.Disease.objects.filter(term__id='OMIM:164400') self.assertEqual(disease_query.count(), 1) def test_disease_str(self): @@ -142,21 +144,21 @@ def test_disease_str(self): class GenomicInterpretationTest(TestCase): def setUp(self): - self.gene = Gene.objects.create(**VALID_GENE_1) - self.variant = Variant.objects.create(**VALID_VARIANT_1) - self.genomic_interpretation = GenomicInterpretation.objects.create( - **valid_genomic_interpretation(self.gene, self.variant) + self.gene = m.Gene.objects.create(**c.VALID_GENE_1) + self.variant = m.Variant.objects.create(**c.VALID_VARIANT_1) + self.genomic_interpretation = m.GenomicInterpretation.objects.create( + **c.valid_genomic_interpretation(self.gene, self.variant) ) def test_genomic_interpretation(self): - genomic_interpretation_query = GenomicInterpretation.objects.filter( + genomic_interpretation_query = m.GenomicInterpretation.objects.filter( gene='HGNC:347') self.assertEqual(genomic_interpretation_query.count(), 1) - self.assertEqual(GenomicInterpretation.objects.count(), 1) + self.assertEqual(m.GenomicInterpretation.objects.count(), 1) def test_validation_gene_or_variant(self): with self.assertRaises(ValidationError): - GenomicInterpretation.objects.create(**valid_genomic_interpretation()).clean() + m.GenomicInterpretation.objects.create(**c.valid_genomic_interpretation()).clean() def test_genomic_interpretation_str(self): self.assertEqual(str(self.genomic_interpretation), str(self.genomic_interpretation.id)) @@ -164,17 +166,17 @@ def test_genomic_interpretation_str(self): class DiagnosisTest(TestCase): def setUp(self): - self.disease = Disease.objects.create(**VALID_DISEASE_1) + self.disease = m.Disease.objects.create(**c.VALID_DISEASE_1) - self.gene = Gene.objects.create(**VALID_GENE_1) - self.variant = Variant.objects.create(**VALID_VARIANT_1) - self.genomic_interpretation_1 = GenomicInterpretation.objects.create( - **valid_genomic_interpretation(self.gene, self.variant) + self.gene = m.Gene.objects.create(**c.VALID_GENE_1) + self.variant = m.Variant.objects.create(**c.VALID_VARIANT_1) + self.genomic_interpretation_1 = m.GenomicInterpretation.objects.create( + **c.valid_genomic_interpretation(self.gene, self.variant) ) - self.genomic_interpretation_2 = GenomicInterpretation.objects.create( - **valid_genomic_interpretation(self.gene) + self.genomic_interpretation_2 = m.GenomicInterpretation.objects.create( + **c.valid_genomic_interpretation(self.gene) ) - self.diagnosis = Diagnosis.objects.create(**valid_diagnosis( + self.diagnosis = m.Diagnosis.objects.create(**c.valid_diagnosis( self.disease)) self.diagnosis.genomic_interpretations.set([ self.genomic_interpretation_1, @@ -182,7 +184,7 @@ def setUp(self): ]) def test_diagnosis(self): - diagnosis = Diagnosis.objects.filter(disease__term__id='OMIM:164400') + diagnosis = m.Diagnosis.objects.filter(disease__term__id='OMIM:164400') self.assertEqual(diagnosis.count(), 1) def test_diagnosis_str(self): @@ -191,26 +193,26 @@ def test_diagnosis_str(self): class InterpretationTest(TestCase): def setUp(self): - self.disease = Disease.objects.create(**VALID_DISEASE_1) - self.diagnosis = Diagnosis.objects.create(**valid_diagnosis( + self.disease = m.Disease.objects.create(**c.VALID_DISEASE_1) + self.diagnosis = m.Diagnosis.objects.create(**c.valid_diagnosis( self.disease)) - self.meta_data_phenopacket = MetaData.objects.create(**VALID_META_DATA_1) - self.meta_data_interpretation = MetaData.objects.create(**VALID_META_DATA_2) + self.meta_data_phenopacket = m.MetaData.objects.create(**c.VALID_META_DATA_1) + self.meta_data_interpretation = m.MetaData.objects.create(**c.VALID_META_DATA_2) - self.individual = Individual.objects.create(**VALID_INDIVIDUAL_1) - self.phenopacket = Phenopacket.objects.create( + self.individual = m.Individual.objects.create(**c.VALID_INDIVIDUAL_1) + self.phenopacket = m.Phenopacket.objects.create( id="phenopacket_id:1", subject=self.individual, meta_data=self.meta_data_phenopacket, ) - self.interpretation = Interpretation.objects.create(**valid_interpretation( + self.interpretation = m.Interpretation.objects.create(**c.valid_interpretation( phenopacket=self.phenopacket, meta_data=self.meta_data_interpretation )) self.interpretation.diagnosis.set([self.diagnosis]) def test_interpretation(self): - interpretation_query = Interpretation.objects.filter( + interpretation_query = m.Interpretation.objects.filter( resolution_status='IN_PROGRESS' ) self.assertEqual(interpretation_query.count(), 1) @@ -221,13 +223,13 @@ def test_interpretation_str(self): class MetaDataTest(TestCase): def setUp(self): - self.resource_1 = Resource.objects.create(**VALID_RESOURCE_1) - self.resource_2 = Resource.objects.create(**VALID_RESOURCE_2) - self.metadata = MetaData.objects.create(**VALID_META_DATA_2) + self.resource_1 = m.Resource.objects.create(**VALID_RESOURCE_1) + self.resource_2 = m.Resource.objects.create(**VALID_RESOURCE_2) + self.metadata = m.MetaData.objects.create(**c.VALID_META_DATA_2) self.metadata.resources.set([self.resource_1, self.resource_2]) def test_metadata(self): - metadata = MetaData.objects.get(created_by__icontains='ksenia') + metadata = m.MetaData.objects.get(created_by__icontains='ksenia') self.assertEqual(metadata.submitted_by, 'Ksenia Zaytseva') self.assertEqual(metadata.resources.count(), 2) diff --git a/chord_metadata_service/resources/models.py b/chord_metadata_service/resources/models.py index 28c70d724..ef9b64a0d 100644 --- a/chord_metadata_service/resources/models.py +++ b/chord_metadata_service/resources/models.py @@ -43,7 +43,7 @@ def clean(self): def save(self, *args, **kwargs): self.clean() - return super().save(*args, **kwargs ) + return super().save(*args, **kwargs) def __str__(self): return str(self.id) From 2cbb522a35e64ce7b1e2a9ef2775e30cbbdf5052 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Thu, 18 Jun 2020 16:55:37 -0400 Subject: [PATCH 150/190] Try to pass chord/postgres env into tox --- tox.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tox.ini b/tox.ini index 4aff5bc18..0cee70a79 100644 --- a/tox.ini +++ b/tox.ini @@ -3,6 +3,9 @@ max-line-length = 120 exclude = .git, .tox, __pycache__, migrations [testenv] +passenv = + CHORD_* + POSTGRES_* skip_install = true commands = pip install -r requirements.txt From bf9a186704ac91789d09e2545e1977167ff9b2fa Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Thu, 18 Jun 2020 17:17:41 -0400 Subject: [PATCH 151/190] fix errors in mcode_ingest add link from mcodepacket to table (migrations) --- chord_metadata_service/mcode/mcode_ingest.py | 57 ++++++++++++++++--- .../migrations/0009_auto_20200618_1318.py | 27 +++++++++ chord_metadata_service/mcode/models.py | 2 + 3 files changed, 77 insertions(+), 9 deletions(-) create mode 100644 chord_metadata_service/mcode/migrations/0009_auto_20200618_1318.py diff --git a/chord_metadata_service/mcode/mcode_ingest.py b/chord_metadata_service/mcode/mcode_ingest.py index b406dc1e3..9727cff46 100644 --- a/chord_metadata_service/mcode/mcode_ingest.py +++ b/chord_metadata_service/mcode/mcode_ingest.py @@ -13,9 +13,10 @@ logger.setLevel(logging.INFO) -def ingest_mcodepacket(mcodepacket_data): +def ingest_mcodepacket(mcodepacket_data, table_id): """ Ingests a single mcodepacket in mcode app and patients' metadata int patients app.""" - new_mcodepacket = mcodepacket_data["id"] + + new_mcodepacket = {"id": mcodepacket_data["id"]} subject = mcodepacket_data["subject"] genomics_report_data = mcodepacket_data.get("genomics_report", None) cancer_condition_data = mcodepacket_data.get("cancer_condition", None) @@ -28,6 +29,7 @@ def ingest_mcodepacket(mcodepacket_data): # get and create Patient if subject: subject, _ = Individual.objects.get_or_create(**subject) + new_mcodepacket["subject"] = subject.id if genomics_report_data: # don't have data for genomics report yet @@ -35,11 +37,30 @@ def ingest_mcodepacket(mcodepacket_data): # get and create CancerCondition if cancer_condition_data: - cancer_condition, _ = CancerCondition.objects.get_or_create(**cancer_condition_data) + # cancer_condition, _ = CancerCondition.objects.get_or_create(**cancer_condition_data) + cancer_condition, _ = CancerCondition.objects.get_or_create( + id=cancer_condition_data["id"], + code=cancer_condition_data["code"], + condition_type=cancer_condition_data["condition_type"], + clinical_status=cancer_condition_data.get("clinical_status", None), + verification_status=cancer_condition_data.get("verification_status", None), + date_of_diagnosis=cancer_condition_data.get("date_of_diagnosis", None), + body_site=cancer_condition_data.get("body_site", None), + laterality=cancer_condition_data.get("laterality", None), + histology_morphology_behavior=cancer_condition_data.get("histology_morphology_behavior", None) + ) new_mcodepacket["cancer_condition"] = cancer_condition if "tnm_staging" in cancer_condition_data: for tnms in cancer_condition_data["tnm_staging"]: - tnm_staging, _ = TNMStaging.objects.get_or_create(**tnms) + tnm_staging, _ = TNMStaging.objects.get_or_create( + id=tnms["id"], + cancer_condition=cancer_condition, + stage_group=tnms["stage_group"], + tnm_type=tnms["tnm_type"], + primary_tumor_category=tnms.get("primary_tumor_category", None), + regional_nodes_category=tnms.get("regional_nodes_category", None), + distant_metastases_category=tnms.get("distant_metastases_category", None) + ) # get and create Cancer Related Procedure crprocedures = [] @@ -55,11 +76,13 @@ def ingest_mcodepacket(mcodepacket_data): reason_code=crp.get("reason_code", None), extra_properties=crp.get("extra_properties", None) ) - crprocedures.append(cancer_related_procedure) + crprocedures.append(cancer_related_procedure.id) if "reason_reference" in crp: + related_cancer_conditions = [] for rr_id in crp["reason_reference"]: - related_cancer_condition = CancerCondition.objects.get(id=rr_id) - cancer_related_procedure.add(related_cancer_condition) + condition = CancerCondition.objects.get(id=rr_id) + related_cancer_conditions.append(condition) + cancer_related_procedure.reason_reference.set(related_cancer_conditions) # get and create CancerCondition if medication_statement_data: @@ -81,9 +104,25 @@ def ingest_mcodepacket(mcodepacket_data): if tumor_markers: for tm in tumor_markers: tumor_marker, _ = LabsVital.objects.get_or_create( + id=tm["id"], tumor_marker_code=tm["tumor_marker_code"], tumor_marker_data_value=tm.get("tumor_marker_data_value", None), - individual=tm["individual"] + individual=Individual.objects.get(id=tm["individual"]) ) + if crprocedures: + new_mcodepacket["cancer_related_procedures"] = crprocedures + + mcodepacket = MCodePacket( + id=new_mcodepacket["id"], + subject=Individual.objects.get(id=new_mcodepacket["subject"]), + genomics_report=new_mcodepacket.get("genomics_report", None), + cancer_condition=new_mcodepacket.get("cancer_condition", None), + medication_statement=new_mcodepacket.get("medication_statement", None), + date_of_death=new_mcodepacket.get("date_of_death", ""), + cancer_disease_status=new_mcodepacket.get("cancer_disease_status", None), + table_id=table_id + ) + mcodepacket.save() + mcodepacket.cancer_related_procedures.set(crprocedures) - return new_mcodepacket + return mcodepacket diff --git a/chord_metadata_service/mcode/migrations/0009_auto_20200618_1318.py b/chord_metadata_service/mcode/migrations/0009_auto_20200618_1318.py new file mode 100644 index 000000000..97e6d7ad4 --- /dev/null +++ b/chord_metadata_service/mcode/migrations/0009_auto_20200618_1318.py @@ -0,0 +1,27 @@ +# Generated by Django 2.2.13 on 2020-06-18 17:18 + +import datetime +from django.db import migrations, models +import django.db.models.deletion +from django.utils.timezone import utc + + +class Migration(migrations.Migration): + + dependencies = [ + ('chord', '0018_auto_20200601_1708'), + ('mcode', '0008_auto_20200610_2320'), + ] + + operations = [ + migrations.AddField( + model_name='mcodepacket', + name='table', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='chord.Table'), + ), + migrations.AlterField( + model_name='genomicsreport', + name='issued', + field=models.DateTimeField(default=datetime.datetime(2020, 6, 18, 17, 18, 39, 482556, tzinfo=utc), help_text='The date/time this report was issued.'), + ), + ] diff --git a/chord_metadata_service/mcode/models.py b/chord_metadata_service/mcode/models.py index b804fc476..c5c11ac69 100644 --- a/chord_metadata_service/mcode/models.py +++ b/chord_metadata_service/mcode/models.py @@ -325,6 +325,8 @@ class MCodePacket(models.Model, IndexableMixin): date_of_death = models.CharField(max_length=200, blank=True, help_text=rec_help(d.MCODEPACKET, "date_of_death")) cancer_disease_status = JSONField(blank=True, null=True, validators=[ontology_validator], help_text=rec_help(d.MCODEPACKET, "cancer_disease_status")) + # link to dataset via the table + table = models.ForeignKey("chord.Table", on_delete=models.CASCADE, blank=True, null=True) # TODO: Help text extra_properties = JSONField(blank=True, null=True, help_text=rec_help(d.MCODEPACKET, "extra_properties")) created = models.DateTimeField(auto_now=True) From 1d28b8eb9e86485bc16609bc14a5c84870e7a274 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Thu, 18 Jun 2020 17:29:31 -0400 Subject: [PATCH 152/190] add mcode_fhir workflow --- chord_metadata_service/chord/data_types.py | 9 +++++ chord_metadata_service/chord/ingest.py | 36 ++++++++++++++++++- .../chord/workflows/mcode_fhir_json.wdl | 20 +++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 chord_metadata_service/chord/workflows/mcode_fhir_json.wdl diff --git a/chord_metadata_service/chord/data_types.py b/chord_metadata_service/chord/data_types.py index 3c639f0a1..5267b7196 100644 --- a/chord_metadata_service/chord/data_types.py +++ b/chord_metadata_service/chord/data_types.py @@ -1,14 +1,17 @@ from chord_metadata_service.experiments.search_schemas import EXPERIMENT_SEARCH_SCHEMA from chord_metadata_service.phenopackets.search_schemas import PHENOPACKET_SEARCH_SCHEMA +from chord_metadata_service.mcode.schemas import MCODE_SCHEMA __all__ = [ "DATA_TYPE_EXPERIMENT", "DATA_TYPE_PHENOPACKET", + "DATA_TYPE_MCODEPACKET", "DATA_TYPES", ] DATA_TYPE_EXPERIMENT = "experiment" DATA_TYPE_PHENOPACKET = "phenopacket" +DATA_TYPE_MCODEPACKET = "mcodepacket" DATA_TYPES = { DATA_TYPE_EXPERIMENT: { @@ -22,5 +25,11 @@ "metadata_schema": { "type": "object", # TODO } + }, + DATA_TYPE_MCODEPACKET: { + "schema": MCODE_SCHEMA, + "metadata_schema": { + "type": "object", # TODO + } } } diff --git a/chord_metadata_service/chord/ingest.py b/chord_metadata_service/chord/ingest.py index d7ecfc964..a42579d86 100644 --- a/chord_metadata_service/chord/ingest.py +++ b/chord_metadata_service/chord/ingest.py @@ -5,7 +5,7 @@ from dateutil.parser import isoparse from typing import Callable -from chord_metadata_service.chord.data_types import DATA_TYPE_EXPERIMENT, DATA_TYPE_PHENOPACKET +from chord_metadata_service.chord.data_types import DATA_TYPE_EXPERIMENT, DATA_TYPE_PHENOPACKET, DATA_TYPE_MCODEPACKET from chord_metadata_service.chord.models import Table, TableOwnership from chord_metadata_service.experiments import models as em from chord_metadata_service.phenopackets import models as pm @@ -16,6 +16,8 @@ ingest_conditions, ingest_specimens ) +from chord_metadata_service.mcode.parse_fhir_mcode import parse_bundle +from chord_metadata_service.mcode.mcode_ingest import ingest_mcodepacket __all__ = [ @@ -28,6 +30,7 @@ WORKFLOW_PHENOPACKETS_JSON = "phenopackets_json" WORKFLOW_EXPERIMENTS_JSON = "experiments_json" WORKFLOW_FHIR_JSON = "fhir_json" +WORKFLOW_MCODE_FHIR_JSON = "mcode_fhir_json" METADATA_WORKFLOWS = { "ingestion": { @@ -142,6 +145,29 @@ }, ] + }, + WORKFLOW_MCODE_FHIR_JSON: { + "name": "MCODE FHIR Resources JSON", + "description": "This ingestion workflow will validate and import a mCODE FHIR 4.0. schema-compatible " + "JSON document, and convert it to the Bento metadata service's internal mCODE-based " + "data model.", + "data_type": DATA_TYPE_MCODEPACKET, + "file": "mcode_fhir_json.wdl", + "inputs": [ + { + "id": "json_document", + "type": "file", + "required": True, + "extensions": [".json"] + } + ], + "outputs": [ + { + "id": "json_document", + "type": "file", + "value": "{json_document}" + } + ] } }, "analysis": {} @@ -372,8 +398,16 @@ def ingest_fhir_workflow(workflow_outputs, table_id): ingest_specimens(phenopacket_ids, specimens_data) +def ingest_mcode_fhir_workflow(workflow_outputs, table_id): + with open(workflow_outputs["json_document"], "r") as jf: + json_data = json.load(jf) + mcodepacket = parse_bundle(json_data) + ingest_mcodepacket(mcodepacket, table_id) + + WORKFLOW_INGEST_FUNCTION_MAP = { WORKFLOW_EXPERIMENTS_JSON: ingest_experiments_workflow, WORKFLOW_PHENOPACKETS_JSON: ingest_phenopacket_workflow, WORKFLOW_FHIR_JSON: ingest_fhir_workflow, + WORKFLOW_MCODE_FHIR_JSON: ingest_mcode_fhir_workflow, } diff --git a/chord_metadata_service/chord/workflows/mcode_fhir_json.wdl b/chord_metadata_service/chord/workflows/mcode_fhir_json.wdl new file mode 100644 index 000000000..8ca83c578 --- /dev/null +++ b/chord_metadata_service/chord/workflows/mcode_fhir_json.wdl @@ -0,0 +1,20 @@ +workflow mcode_fhir_json { + File json_document + + call identity_task { + input: json_document_in = json_document + } + +} + +task identity_task { + File json_document_in + + command { + true + } + + output { + File json_document = "${json_document_in}" + } +} From 18a7b1bfab790c1a6792189aba5f64ce60d3a393 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Thu, 18 Jun 2020 18:39:20 -0400 Subject: [PATCH 153/190] change mcodepacket to CancerCondition relation to m2m (migrations) --- chord_metadata_service/mcode/mcode_ingest.py | 65 +++++++++++-------- .../migrations/0010_auto_20200618_1825.py | 28 ++++++++ chord_metadata_service/mcode/models.py | 8 ++- .../mcode/parse_fhir_mcode.py | 10 ++- 4 files changed, 77 insertions(+), 34 deletions(-) create mode 100644 chord_metadata_service/mcode/migrations/0010_auto_20200618_1825.py diff --git a/chord_metadata_service/mcode/mcode_ingest.py b/chord_metadata_service/mcode/mcode_ingest.py index 9727cff46..ae185a4cb 100644 --- a/chord_metadata_service/mcode/mcode_ingest.py +++ b/chord_metadata_service/mcode/mcode_ingest.py @@ -36,31 +36,34 @@ def ingest_mcodepacket(mcodepacket_data, table_id): pass # get and create CancerCondition + cancer_conditions = [] if cancer_condition_data: - # cancer_condition, _ = CancerCondition.objects.get_or_create(**cancer_condition_data) - cancer_condition, _ = CancerCondition.objects.get_or_create( - id=cancer_condition_data["id"], - code=cancer_condition_data["code"], - condition_type=cancer_condition_data["condition_type"], - clinical_status=cancer_condition_data.get("clinical_status", None), - verification_status=cancer_condition_data.get("verification_status", None), - date_of_diagnosis=cancer_condition_data.get("date_of_diagnosis", None), - body_site=cancer_condition_data.get("body_site", None), - laterality=cancer_condition_data.get("laterality", None), - histology_morphology_behavior=cancer_condition_data.get("histology_morphology_behavior", None) - ) - new_mcodepacket["cancer_condition"] = cancer_condition - if "tnm_staging" in cancer_condition_data: - for tnms in cancer_condition_data["tnm_staging"]: - tnm_staging, _ = TNMStaging.objects.get_or_create( - id=tnms["id"], - cancer_condition=cancer_condition, - stage_group=tnms["stage_group"], - tnm_type=tnms["tnm_type"], - primary_tumor_category=tnms.get("primary_tumor_category", None), - regional_nodes_category=tnms.get("regional_nodes_category", None), - distant_metastases_category=tnms.get("distant_metastases_category", None) - ) + for cc in cancer_condition_data: + + cancer_condition, _ = CancerCondition.objects.get_or_create( + id=cc["id"], + code=cc["code"], + condition_type=cc["condition_type"], + clinical_status=cc.get("clinical_status", None), + verification_status=cc.get("verification_status", None), + date_of_diagnosis=cc.get("date_of_diagnosis", None), + body_site=cc.get("body_site", None), + laterality=cc.get("laterality", None), + histology_morphology_behavior=cc.get("histology_morphology_behavior", None) + ) + print(f"CANCER CONDITION {cancer_condition}") + cancer_conditions.append(cancer_condition.id) + if "tnm_staging" in cc: + for tnms in cc["tnm_staging"]: + tnm_staging, _ = TNMStaging.objects.get_or_create( + id=tnms["id"], + cancer_condition=cancer_condition, + stage_group=tnms["stage_group"], + tnm_type=tnms["tnm_type"], + primary_tumor_category=tnms.get("primary_tumor_category", None), + regional_nodes_category=tnms.get("regional_nodes_category", None), + distant_metastases_category=tnms.get("distant_metastases_category", None) + ) # get and create Cancer Related Procedure crprocedures = [] @@ -80,6 +83,7 @@ def ingest_mcodepacket(mcodepacket_data, table_id): if "reason_reference" in crp: related_cancer_conditions = [] for rr_id in crp["reason_reference"]: + print(f"REASON REFERENCE CANCER CONDITION {rr_id}") condition = CancerCondition.objects.get(id=rr_id) related_cancer_conditions.append(condition) cancer_related_procedure.reason_reference.set(related_cancer_conditions) @@ -109,20 +113,25 @@ def ingest_mcodepacket(mcodepacket_data, table_id): tumor_marker_data_value=tm.get("tumor_marker_data_value", None), individual=Individual.objects.get(id=tm["individual"]) ) - if crprocedures: - new_mcodepacket["cancer_related_procedures"] = crprocedures + # if cancer_conditions: + # new_mcodepacket["cancer_condition"] = cancer_conditions + # + # if crprocedures: + # new_mcodepacket["cancer_related_procedures"] = crprocedures mcodepacket = MCodePacket( id=new_mcodepacket["id"], subject=Individual.objects.get(id=new_mcodepacket["subject"]), genomics_report=new_mcodepacket.get("genomics_report", None), - cancer_condition=new_mcodepacket.get("cancer_condition", None), medication_statement=new_mcodepacket.get("medication_statement", None), date_of_death=new_mcodepacket.get("date_of_death", ""), cancer_disease_status=new_mcodepacket.get("cancer_disease_status", None), table_id=table_id ) mcodepacket.save() - mcodepacket.cancer_related_procedures.set(crprocedures) + if cancer_conditions: + mcodepacket.cancer_condition.set(cancer_conditions) + if crprocedures: + mcodepacket.cancer_related_procedures.set(crprocedures) return mcodepacket diff --git a/chord_metadata_service/mcode/migrations/0010_auto_20200618_1825.py b/chord_metadata_service/mcode/migrations/0010_auto_20200618_1825.py new file mode 100644 index 000000000..5f9761207 --- /dev/null +++ b/chord_metadata_service/mcode/migrations/0010_auto_20200618_1825.py @@ -0,0 +1,28 @@ +# Generated by Django 2.2.13 on 2020-06-18 22:25 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('mcode', '0009_auto_20200618_1318'), + ] + + operations = [ + migrations.AlterField( + model_name='genomicsreport', + name='issued', + field=models.DateTimeField(default=django.utils.timezone.now, help_text='The date/time this report was issued.'), + ), + migrations.RemoveField( + model_name='mcodepacket', + name='cancer_condition', + ), + migrations.AddField( + model_name='mcodepacket', + name='cancer_condition', + field=models.ManyToManyField(blank=True, help_text="An Individual's cancer condition.", to='mcode.CancerCondition'), + ), + ] diff --git a/chord_metadata_service/mcode/models.py b/chord_metadata_service/mcode/models.py index c5c11ac69..2d863dd5e 100644 --- a/chord_metadata_service/mcode/models.py +++ b/chord_metadata_service/mcode/models.py @@ -115,7 +115,7 @@ class GenomicsReport(models.Model, IndexableMixin): code = JSONField(validators=[ontology_validator], help_text=rec_help(d.GENOMICS_REPORT, "code")) performing_organization_name = models.CharField( max_length=200, blank=True, help_text=rec_help(d.GENOMICS_REPORT, "performing_organization_name")) - issued = models.DateTimeField(default=timezone.now(), help_text=rec_help(d.GENOMICS_REPORT, "issued")) + issued = models.DateTimeField(default=timezone.now, help_text=rec_help(d.GENOMICS_REPORT, "issued")) genetic_specimen = models.ManyToManyField(GeneticSpecimen, blank=True, help_text=rec_help(d.GENOMICS_REPORT, "genetic_specimen")) genetic_variant = models.ForeignKey(CancerGeneticVariant, blank=True, null=True, on_delete=models.SET_NULL, @@ -316,8 +316,10 @@ class MCodePacket(models.Model, IndexableMixin): help_text=rec_help(d.MCODEPACKET, "subject")) genomics_report = models.ForeignKey(GenomicsReport, blank=True, null=True, on_delete=models.SET_NULL, help_text=rec_help(d.MCODEPACKET, "genomics_report")) - cancer_condition = models.ForeignKey(CancerCondition, blank=True, null=True, on_delete=models.SET_NULL, - help_text=rec_help(d.MCODEPACKET, "cancer_condition")) + # cancer_condition = models.ForeignKey(CancerCondition, blank=True, null=True, on_delete=models.SET_NULL, + # help_text=rec_help(d.MCODEPACKET, "cancer_condition")) + cancer_condition = models.ManyToManyField(CancerCondition, blank=True, + help_text=rec_help(d.MCODEPACKET, "cancer_condition")) cancer_related_procedures = models.ManyToManyField(CancerRelatedProcedure, blank=True, help_text=rec_help(d.MCODEPACKET, "cancer_related_procedures")) medication_statement = models.ForeignKey(MedicationStatement, blank=True, null=True, on_delete=models.SET_NULL, diff --git a/chord_metadata_service/mcode/parse_fhir_mcode.py b/chord_metadata_service/mcode/parse_fhir_mcode.py index b6ba90c2d..cf1dcc1f7 100644 --- a/chord_metadata_service/mcode/parse_fhir_mcode.py +++ b/chord_metadata_service/mcode/parse_fhir_mcode.py @@ -166,6 +166,7 @@ def parse_bundle(bundle): tnm_staging_members = [] # all procedure cancer_related_procedures = [] + cancer_conditions = [] for item in bundle["entry"]: resource = item["resource"] # get Patient data @@ -177,14 +178,15 @@ def parse_bundle(bundle): # get Patient's Cancer Condition if resource["resourceType"] == "Condition": resource_profiles = resource["meta"]["profile"] - cancer_conditions = [MCODE_PRIMARY_CANCER_CONDITION, MCODE_SECONDARY_CANCER_CONDITION] - for cc in cancer_conditions: + cancer_conditions_profiles = [MCODE_PRIMARY_CANCER_CONDITION, MCODE_SECONDARY_CANCER_CONDITION] + for cc in cancer_conditions_profiles: if cc in resource_profiles: cancer_condition = condition_to_cancer_condition(resource) for key, value in MCODE_PROFILES_MAPPING["cancer_condition"]["profile"].items(): if cc == value: cancer_condition["condition_type"] = key - mcodepacket["cancer_condition"] = cancer_condition + cancer_conditions.append(cancer_condition) + # mcodepacket["cancer_condition"] = cancer_condition # get TNM staging stage category if resource["resourceType"] == "Observation" and "meta" in resource: @@ -262,6 +264,8 @@ def parse_bundle(bundle): if member["id"] in staging_to_members[tnm_staging_item["id"]]: tnm_staging_item[member["category_type"]] = member["tnm_staging_value"] + if cancer_conditions: + mcodepacket["cancer_condition"] = cancer_conditions # mcodepacket["tnm_staging"] = tnm_stagings mcodepacket["tumor_marker"] = tumor_markers mcodepacket["cancer_related_procedures"] = cancer_related_procedures From 153d3f96c1f163451c5050002794b7a78b4f177f Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Thu, 18 Jun 2020 19:01:47 -0400 Subject: [PATCH 154/190] cleaning --- chord_metadata_service/mcode/mcode_ingest.py | 11 ----------- chord_metadata_service/mcode/models.py | 2 -- chord_metadata_service/mcode/parse_fhir_mcode.py | 3 +-- 3 files changed, 1 insertion(+), 15 deletions(-) diff --git a/chord_metadata_service/mcode/mcode_ingest.py b/chord_metadata_service/mcode/mcode_ingest.py index ae185a4cb..6ee35b3c1 100644 --- a/chord_metadata_service/mcode/mcode_ingest.py +++ b/chord_metadata_service/mcode/mcode_ingest.py @@ -1,11 +1,5 @@ -import uuid -import jsonschema import logging - -from chord_metadata_service.restapi.schemas import FHIR_BUNDLE_SCHEMA from chord_metadata_service.patients.models import Individual - -from .parse_fhir_mcode import parse_bundle from .models import * @@ -113,11 +107,6 @@ def ingest_mcodepacket(mcodepacket_data, table_id): tumor_marker_data_value=tm.get("tumor_marker_data_value", None), individual=Individual.objects.get(id=tm["individual"]) ) - # if cancer_conditions: - # new_mcodepacket["cancer_condition"] = cancer_conditions - # - # if crprocedures: - # new_mcodepacket["cancer_related_procedures"] = crprocedures mcodepacket = MCodePacket( id=new_mcodepacket["id"], diff --git a/chord_metadata_service/mcode/models.py b/chord_metadata_service/mcode/models.py index 2d863dd5e..a35a33816 100644 --- a/chord_metadata_service/mcode/models.py +++ b/chord_metadata_service/mcode/models.py @@ -316,8 +316,6 @@ class MCodePacket(models.Model, IndexableMixin): help_text=rec_help(d.MCODEPACKET, "subject")) genomics_report = models.ForeignKey(GenomicsReport, blank=True, null=True, on_delete=models.SET_NULL, help_text=rec_help(d.MCODEPACKET, "genomics_report")) - # cancer_condition = models.ForeignKey(CancerCondition, blank=True, null=True, on_delete=models.SET_NULL, - # help_text=rec_help(d.MCODEPACKET, "cancer_condition")) cancer_condition = models.ManyToManyField(CancerCondition, blank=True, help_text=rec_help(d.MCODEPACKET, "cancer_condition")) cancer_related_procedures = models.ManyToManyField(CancerRelatedProcedure, blank=True, diff --git a/chord_metadata_service/mcode/parse_fhir_mcode.py b/chord_metadata_service/mcode/parse_fhir_mcode.py index cf1dcc1f7..e7bcf723e 100644 --- a/chord_metadata_service/mcode/parse_fhir_mcode.py +++ b/chord_metadata_service/mcode/parse_fhir_mcode.py @@ -166,6 +166,7 @@ def parse_bundle(bundle): tnm_staging_members = [] # all procedure cancer_related_procedures = [] + # all cancer conditions cancer_conditions = [] for item in bundle["entry"]: resource = item["resource"] @@ -186,7 +187,6 @@ def parse_bundle(bundle): if cc == value: cancer_condition["condition_type"] = key cancer_conditions.append(cancer_condition) - # mcodepacket["cancer_condition"] = cancer_condition # get TNM staging stage category if resource["resourceType"] == "Observation" and "meta" in resource: @@ -276,4 +276,3 @@ def parse_bundle(bundle): mcodepacket["cancer_condition"]["tnm_staging"] = tnm_stagings return mcodepacket - From e308c2833ffd0eef1f9f56cc6ba47a5ad201e586 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Fri, 19 Jun 2020 11:49:54 -0400 Subject: [PATCH 155/190] fix a bug in mcodepacket serializer --- chord_metadata_service/mcode/serializers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/chord_metadata_service/mcode/serializers.py b/chord_metadata_service/mcode/serializers.py index 188577d2e..dff6ec516 100644 --- a/chord_metadata_service/mcode/serializers.py +++ b/chord_metadata_service/mcode/serializers.py @@ -99,7 +99,8 @@ def to_representation(self, instance): response = super().to_representation(instance) response['subject'] = IndividualSerializer(instance.subject).data response['genomics_report'] = GenomicsReportSerializer(instance.genomics_report, required=False).data - response['cancer_condition'] = CancerConditionSerializer(instance.cancer_condition, required=False).data + response['cancer_condition'] = CancerConditionSerializer(instance.cancer_condition, many=True, + required=False).data response['cancer_related_procedures'] = CancerRelatedProcedureSerializer(instance.cancer_related_procedures, many=True, required=False).data response['medication_statement'] = MedicationStatementSerializer(instance.medication_statement, From 4dfad8652cfca576b421ca4ef2fb03e9ee7bdab1 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Fri, 19 Jun 2020 15:23:03 -0400 Subject: [PATCH 156/190] add defaults to get_or_create() since we don't have auto-created pk --- chord_metadata_service/mcode/mcode_ingest.py | 81 +++++++++++++------- 1 file changed, 55 insertions(+), 26 deletions(-) diff --git a/chord_metadata_service/mcode/mcode_ingest.py b/chord_metadata_service/mcode/mcode_ingest.py index 6ee35b3c1..dd9a4be45 100644 --- a/chord_metadata_service/mcode/mcode_ingest.py +++ b/chord_metadata_service/mcode/mcode_ingest.py @@ -23,6 +23,7 @@ def ingest_mcodepacket(mcodepacket_data, table_id): # get and create Patient if subject: subject, _ = Individual.objects.get_or_create(**subject) + logger.info(f"Individual {subject.id} created") new_mcodepacket["subject"] = subject.id if genomics_report_data: @@ -34,22 +35,27 @@ def ingest_mcodepacket(mcodepacket_data, table_id): if cancer_condition_data: for cc in cancer_condition_data: - cancer_condition, _ = CancerCondition.objects.get_or_create( + cancer_condition, cc_created = CancerCondition.objects.get_or_create( id=cc["id"], - code=cc["code"], - condition_type=cc["condition_type"], - clinical_status=cc.get("clinical_status", None), - verification_status=cc.get("verification_status", None), - date_of_diagnosis=cc.get("date_of_diagnosis", None), - body_site=cc.get("body_site", None), - laterality=cc.get("laterality", None), - histology_morphology_behavior=cc.get("histology_morphology_behavior", None) + defaults={ + "code": cc["code"], + "condition_type": cc["condition_type"], + "clinical_status": cc.get("clinical_status", None), + "verification_status": cc.get("verification_status", None), + "date_of_diagnosis": cc.get("date_of_diagnosis", None), + "body_site": cc.get("body_site", None), + "laterality": cc.get("laterality", None), + "histology_morphology_behavior": cc.get("histology_morphology_behavior", None) + } ) - print(f"CANCER CONDITION {cancer_condition}") + if cc_created: + logger.info(f"New Cancer Condition {cancer_condition.id} created") + else: + logger.info(f"Existing Cancer Condition {cancer_condition.id} retrieved") cancer_conditions.append(cancer_condition.id) if "tnm_staging" in cc: for tnms in cc["tnm_staging"]: - tnm_staging, _ = TNMStaging.objects.get_or_create( + tnm_staging, tnms_created = TNMStaging.objects.get_or_create( id=tnms["id"], cancer_condition=cancer_condition, stage_group=tnms["stage_group"], @@ -58,21 +64,31 @@ def ingest_mcodepacket(mcodepacket_data, table_id): regional_nodes_category=tnms.get("regional_nodes_category", None), distant_metastases_category=tnms.get("distant_metastases_category", None) ) + if tnms_created: + logger.info(f"New TNM Staging {tnm_staging.id} created") + else: + logger.info(f"Existing TNM Staging {tnm_staging.id} retrieved") # get and create Cancer Related Procedure crprocedures = [] if cancer_related_procedures: for crp in cancer_related_procedures: - cancer_related_procedure, _ = CancerRelatedProcedure.objects.get_or_create( + cancer_related_procedure, crp_created = CancerRelatedProcedure.objects.get_or_create( id=crp["id"], - code=crp["code"], - procedure_type=crp["procedure_type"], - body_site= crp.get("body_site", None), - laterality=crp.get("laterality", None), - treatment_intent=crp.get("treatment_intent", None), - reason_code=crp.get("reason_code", None), - extra_properties=crp.get("extra_properties", None) + defaults={ + "code": crp["code"], + "procedure_type": crp["procedure_type"], + "body_site": crp.get("body_site", None), + "laterality": crp.get("laterality", None), + "treatment_intent": crp.get("treatment_intent", None), + "reason_code": crp.get("reason_code", None), + "extra_properties": crp.get("extra_properties", None) + } ) + if crp_created: + logger.info(f'New Cancer Related Procedure {cancer_related_procedure.id} created') + else: + logger.info(f'Existing Cancer Related Procedure {cancer_related_procedure.id} retrieved') crprocedures.append(cancer_related_procedure.id) if "reason_reference" in crp: related_cancer_conditions = [] @@ -82,12 +98,18 @@ def ingest_mcodepacket(mcodepacket_data, table_id): related_cancer_conditions.append(condition) cancer_related_procedure.reason_reference.set(related_cancer_conditions) - # get and create CancerCondition + # get and create MedicationStatement if medication_statement_data: - medication_statement, _ = MedicationStatement.objects.get_or_create( + medication_statement, ms_created = MedicationStatement.objects.get_or_create( id=medication_statement_data["id"], - medication_code=medication_statement_data["medication_code"] + defaults={ + "medication_code": medication_statement_data["medication_code"] + } ) + if ms_created: + logger.info(f"New Medication Statement {medication_statement.id} created") + else: + logger.info(f"Existing Medication Statement {medication_statement.id} retrieved") new_mcodepacket["medication_statement"] = medication_statement # get date of death @@ -101,12 +123,18 @@ def ingest_mcodepacket(mcodepacket_data, table_id): # get tumor marker if tumor_markers: for tm in tumor_markers: - tumor_marker, _ = LabsVital.objects.get_or_create( + tumor_marker, tm_created = LabsVital.objects.get_or_create( id=tm["id"], - tumor_marker_code=tm["tumor_marker_code"], - tumor_marker_data_value=tm.get("tumor_marker_data_value", None), - individual=Individual.objects.get(id=tm["individual"]) + defaults={ + "tumor_marker_code": tm["tumor_marker_code"], + "tumor_marker_data_value": tm.get("tumor_marker_data_value", None), + "individual": Individual.objects.get(id=tm["individual"]) + } ) + if tm_created: + logger.info(f"New LabsVital with tumor marker {tumor_marker.id} created") + else: + logger.info(f"Existing LabsVital with tumor marker {tumor_marker.id} retrieved") mcodepacket = MCodePacket( id=new_mcodepacket["id"], @@ -118,6 +146,7 @@ def ingest_mcodepacket(mcodepacket_data, table_id): table_id=table_id ) mcodepacket.save() + logger.info(f"Mcodepacket {mcodepacket.id} created") if cancer_conditions: mcodepacket.cancer_condition.set(cancer_conditions) if crprocedures: From fa8b0072695909cd5778fe37a58c72641d8e5fbf Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Fri, 19 Jun 2020 16:33:50 -0400 Subject: [PATCH 157/190] add list of cancer conditions to mcodepacket in parse_bundle() --- chord_metadata_service/mcode/mcode_ingest.py | 1 - chord_metadata_service/mcode/parse_fhir_mcode.py | 11 ++++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/chord_metadata_service/mcode/mcode_ingest.py b/chord_metadata_service/mcode/mcode_ingest.py index dd9a4be45..5be6d2315 100644 --- a/chord_metadata_service/mcode/mcode_ingest.py +++ b/chord_metadata_service/mcode/mcode_ingest.py @@ -93,7 +93,6 @@ def ingest_mcodepacket(mcodepacket_data, table_id): if "reason_reference" in crp: related_cancer_conditions = [] for rr_id in crp["reason_reference"]: - print(f"REASON REFERENCE CANCER CONDITION {rr_id}") condition = CancerCondition.objects.get(id=rr_id) related_cancer_conditions.append(condition) cancer_related_procedure.reason_reference.set(related_cancer_conditions) diff --git a/chord_metadata_service/mcode/parse_fhir_mcode.py b/chord_metadata_service/mcode/parse_fhir_mcode.py index e7bcf723e..d60ecf5a0 100644 --- a/chord_metadata_service/mcode/parse_fhir_mcode.py +++ b/chord_metadata_service/mcode/parse_fhir_mcode.py @@ -270,9 +270,14 @@ def parse_bundle(bundle): mcodepacket["tumor_marker"] = tumor_markers mcodepacket["cancer_related_procedures"] = cancer_related_procedures - # TODO add nested tnm_stagings to cancer_condition for tnms in tnm_stagings: - if tnms["cancer_condition"] == mcodepacket["cancer_condition"]["id"]: - mcodepacket["cancer_condition"]["tnm_staging"] = tnm_stagings + if tnms["cancer_condition"] in [cc_id["id"] for cc_id in mcodepacket["cancer_condition"]]: + for cc in mcodepacket["cancer_condition"]: + if cc["id"] == tnms["cancer_condition"]: + if "tnm_staging" in cc: + cc["tnm_staging"].append(tnms) + else: + cc["tnm_staging"] = [] + cc["tnm_staging"].append(tnms) return mcodepacket From 10f0d7dd28c992b46f635d9ace14bc00a5871380 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Fri, 19 Jun 2020 18:29:13 -0400 Subject: [PATCH 158/190] add logger message for mcode ingest --- chord_metadata_service/mcode/mcode_ingest.py | 34 ++++++++------------ 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/chord_metadata_service/mcode/mcode_ingest.py b/chord_metadata_service/mcode/mcode_ingest.py index 5be6d2315..9f4af1038 100644 --- a/chord_metadata_service/mcode/mcode_ingest.py +++ b/chord_metadata_service/mcode/mcode_ingest.py @@ -7,6 +7,13 @@ logger.setLevel(logging.INFO) +def _logger_message(created, obj, obj_id): + if created: + logger.info(f"New {obj.__class__.__name__} {obj_id} created") + else: + logger.info(f"Existing {obj.__class__.__name__} {obj_id} retrieved") + + def ingest_mcodepacket(mcodepacket_data, table_id): """ Ingests a single mcodepacket in mcode app and patients' metadata int patients app.""" @@ -48,10 +55,7 @@ def ingest_mcodepacket(mcodepacket_data, table_id): "histology_morphology_behavior": cc.get("histology_morphology_behavior", None) } ) - if cc_created: - logger.info(f"New Cancer Condition {cancer_condition.id} created") - else: - logger.info(f"Existing Cancer Condition {cancer_condition.id} retrieved") + _logger_message(cc_created, cancer_condition, cancer_condition.id) cancer_conditions.append(cancer_condition.id) if "tnm_staging" in cc: for tnms in cc["tnm_staging"]: @@ -64,10 +68,7 @@ def ingest_mcodepacket(mcodepacket_data, table_id): regional_nodes_category=tnms.get("regional_nodes_category", None), distant_metastases_category=tnms.get("distant_metastases_category", None) ) - if tnms_created: - logger.info(f"New TNM Staging {tnm_staging.id} created") - else: - logger.info(f"Existing TNM Staging {tnm_staging.id} retrieved") + _logger_message(tnms_created, tnm_staging, tnm_staging.id) # get and create Cancer Related Procedure crprocedures = [] @@ -85,10 +86,7 @@ def ingest_mcodepacket(mcodepacket_data, table_id): "extra_properties": crp.get("extra_properties", None) } ) - if crp_created: - logger.info(f'New Cancer Related Procedure {cancer_related_procedure.id} created') - else: - logger.info(f'Existing Cancer Related Procedure {cancer_related_procedure.id} retrieved') + _logger_message(crp_created, cancer_related_procedure, cancer_related_procedure.id) crprocedures.append(cancer_related_procedure.id) if "reason_reference" in crp: related_cancer_conditions = [] @@ -105,10 +103,7 @@ def ingest_mcodepacket(mcodepacket_data, table_id): "medication_code": medication_statement_data["medication_code"] } ) - if ms_created: - logger.info(f"New Medication Statement {medication_statement.id} created") - else: - logger.info(f"Existing Medication Statement {medication_statement.id} retrieved") + _logger_message(ms_created, medication_statement, medication_statement.id) new_mcodepacket["medication_statement"] = medication_statement # get date of death @@ -130,10 +125,7 @@ def ingest_mcodepacket(mcodepacket_data, table_id): "individual": Individual.objects.get(id=tm["individual"]) } ) - if tm_created: - logger.info(f"New LabsVital with tumor marker {tumor_marker.id} created") - else: - logger.info(f"Existing LabsVital with tumor marker {tumor_marker.id} retrieved") + _logger_message(tm_created, tumor_marker, tumor_marker.id) mcodepacket = MCodePacket( id=new_mcodepacket["id"], @@ -145,7 +137,7 @@ def ingest_mcodepacket(mcodepacket_data, table_id): table_id=table_id ) mcodepacket.save() - logger.info(f"Mcodepacket {mcodepacket.id} created") + logger.info(f"New Mcodepacket {mcodepacket.id} created") if cancer_conditions: mcodepacket.cancer_condition.set(cancer_conditions) if crprocedures: From 1a521e40a8c657ba74cb8ca06f112c4ae8748179 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 22 Jun 2020 09:37:11 -0400 Subject: [PATCH 159/190] Add mcode mappings init file --- chord_metadata_service/mcode/mappings/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 chord_metadata_service/mcode/mappings/__init__.py diff --git a/chord_metadata_service/mcode/mappings/__init__.py b/chord_metadata_service/mcode/mappings/__init__.py new file mode 100644 index 000000000..e69de29bb From a5aab724ea2bcc44a107d517526b54fa32390e0e Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Mon, 22 Jun 2020 21:23:22 -0400 Subject: [PATCH 160/190] save patients data to patients app small fixes --- chord_metadata_service/mcode/mcode_ingest.py | 46 ++++++++++++------- .../mcode/parse_fhir_mcode.py | 39 ++++++++++++---- chord_metadata_service/restapi/fhir_ingest.py | 10 ++-- 3 files changed, 65 insertions(+), 30 deletions(-) diff --git a/chord_metadata_service/mcode/mcode_ingest.py b/chord_metadata_service/mcode/mcode_ingest.py index 9f4af1038..efb620598 100644 --- a/chord_metadata_service/mcode/mcode_ingest.py +++ b/chord_metadata_service/mcode/mcode_ingest.py @@ -7,15 +7,15 @@ logger.setLevel(logging.INFO) -def _logger_message(created, obj, obj_id): +def _logger_message(created, obj): if created: - logger.info(f"New {obj.__class__.__name__} {obj_id} created") + logger.info(f"New {obj.__class__.__name__} {obj.id} created") else: - logger.info(f"Existing {obj.__class__.__name__} {obj_id} retrieved") + logger.info(f"Existing {obj.__class__.__name__} {obj.id} retrieved") def ingest_mcodepacket(mcodepacket_data, table_id): - """ Ingests a single mcodepacket in mcode app and patients' metadata int patients app.""" + """ Ingests a single mcodepacket in mcode app and patients' metadata into patients app.""" new_mcodepacket = {"id": mcodepacket_data["id"]} subject = mcodepacket_data["subject"] @@ -29,8 +29,17 @@ def ingest_mcodepacket(mcodepacket_data, table_id): # get and create Patient if subject: - subject, _ = Individual.objects.get_or_create(**subject) - logger.info(f"Individual {subject.id} created") + subject, s_created = Individual.objects.get_or_create( + id=subject["id"], + defaults={ + "alternate_ids": subject.get("alternate_ids", None), + "sex": subject.get("sex", ""), + "date_of_birth": subject.get("date_of_birth", None), + "active": subject.get("active", None), + "deceased": subject.get("deceased", None) + } + ) + _logger_message(s_created, subject) new_mcodepacket["subject"] = subject.id if genomics_report_data: @@ -55,20 +64,23 @@ def ingest_mcodepacket(mcodepacket_data, table_id): "histology_morphology_behavior": cc.get("histology_morphology_behavior", None) } ) - _logger_message(cc_created, cancer_condition, cancer_condition.id) + _logger_message(cc_created, cancer_condition) cancer_conditions.append(cancer_condition.id) if "tnm_staging" in cc: for tnms in cc["tnm_staging"]: tnm_staging, tnms_created = TNMStaging.objects.get_or_create( id=tnms["id"], - cancer_condition=cancer_condition, - stage_group=tnms["stage_group"], - tnm_type=tnms["tnm_type"], - primary_tumor_category=tnms.get("primary_tumor_category", None), - regional_nodes_category=tnms.get("regional_nodes_category", None), - distant_metastases_category=tnms.get("distant_metastases_category", None) + defaults={ + "cancer_condition": cancer_condition, + "stage_group": tnms["stage_group"], + "tnm_type": tnms["tnm_type"], + "primary_tumor_category": tnms.get("primary_tumor_category", None), + "regional_nodes_category": tnms.get("regional_nodes_category", None), + "distant_metastases_category": tnms.get("distant_metastases_category", None) + + } ) - _logger_message(tnms_created, tnm_staging, tnm_staging.id) + _logger_message(tnms_created, tnm_staging) # get and create Cancer Related Procedure crprocedures = [] @@ -86,7 +98,7 @@ def ingest_mcodepacket(mcodepacket_data, table_id): "extra_properties": crp.get("extra_properties", None) } ) - _logger_message(crp_created, cancer_related_procedure, cancer_related_procedure.id) + _logger_message(crp_created, cancer_related_procedure) crprocedures.append(cancer_related_procedure.id) if "reason_reference" in crp: related_cancer_conditions = [] @@ -103,7 +115,7 @@ def ingest_mcodepacket(mcodepacket_data, table_id): "medication_code": medication_statement_data["medication_code"] } ) - _logger_message(ms_created, medication_statement, medication_statement.id) + _logger_message(ms_created, medication_statement) new_mcodepacket["medication_statement"] = medication_statement # get date of death @@ -125,7 +137,7 @@ def ingest_mcodepacket(mcodepacket_data, table_id): "individual": Individual.objects.get(id=tm["individual"]) } ) - _logger_message(tm_created, tumor_marker, tumor_marker.id) + _logger_message(tm_created, tumor_marker) mcodepacket = MCodePacket( id=new_mcodepacket["id"], diff --git a/chord_metadata_service/mcode/parse_fhir_mcode.py b/chord_metadata_service/mcode/parse_fhir_mcode.py index d60ecf5a0..a44e5c7ef 100644 --- a/chord_metadata_service/mcode/parse_fhir_mcode.py +++ b/chord_metadata_service/mcode/parse_fhir_mcode.py @@ -1,11 +1,9 @@ import uuid -import json from .mappings.mappings import * from .mappings.mcode_profiles import * from chord_metadata_service.restapi.schemas import FHIR_BUNDLE_SCHEMA -from chord_metadata_service.restapi.fhir_ingest import _check_schema -from chord_metadata_service.restapi.fhir_utils import patient_to_individual +from chord_metadata_service.restapi.fhir_ingest import check_schema def get_ontology_value(resource, codeable_concept_property): @@ -30,6 +28,33 @@ def get_ontology_value(resource, codeable_concept_property): raise KeyError(e) +def patient_to_individual(resource): + """ Patient to Individual. """ + + # TODO when smart-on-fhir for fhir 4.0 is issued change it for function form fhir-utils + individual = { + "id": resource["id"] + } + if "identifier" in resource: + individual["alternate_ids"] = [alternate_id["value"] for alternate_id in resource["identifier"]] + gender_to_sex = { + "male": "MALE", + "female": "FEMALE", + "other": "OTHER_SEX", + "unknown": "UNKNOWN_SEX" + } + if "gender" in resource: + individual["sex"] = gender_to_sex[resource["gender"]] + if "birthDate" in resource: + individual["date_of_birth"] = resource["birthDate"] + if "active" in resource: + if resource["active"]: + individual["active"] = True + if "deceasedBoolean" in resource: + individual["deceased"] = True + return individual + + def observation_to_labs_vital(resource): """ Observation with tumor marker to LabsVital. """ labs_vital = { @@ -153,7 +178,7 @@ def parse_bundle(bundle): :param bundle: FHIR resourceType Bundle object :return: mcodepacket object """ - _check_schema(FHIR_BUNDLE_SCHEMA, bundle, 'bundle') + check_schema(FHIR_BUNDLE_SCHEMA, bundle, 'bundle') mcodepacket = { "id": str(uuid.uuid4()) } @@ -172,10 +197,8 @@ def parse_bundle(bundle): resource = item["resource"] # get Patient data if resource["resourceType"] == "Patient": - # patient = patient_to_individual(resource) - mcodepacket["subject"] = { - "id": resource["id"] - } + mcodepacket["subject"] = patient_to_individual(resource) + # get Patient's Cancer Condition if resource["resourceType"] == "Condition": resource_profiles = resource["meta"]["profile"] diff --git a/chord_metadata_service/restapi/fhir_ingest.py b/chord_metadata_service/restapi/fhir_ingest.py index 12206d8a2..58b89db08 100644 --- a/chord_metadata_service/restapi/fhir_ingest.py +++ b/chord_metadata_service/restapi/fhir_ingest.py @@ -24,7 +24,7 @@ def _parse_reference(ref): return ref.split('/')[-1] -def _check_schema(schema, obj, additional_info=None): +def check_schema(schema, obj, additional_info=None): """ Validates schema and catches errors. """ try: jsonschema.validate(obj, schema) @@ -41,7 +41,7 @@ def _check_schema(schema, obj, additional_info=None): def ingest_patients(patients_data, table_id, created_by): """ Takes FHIR Bundle containing Patient resources. """ # check if Patients data follows FHIR Bundle schema - _check_schema(FHIR_BUNDLE_SCHEMA, patients_data, 'patients data') + check_schema(FHIR_BUNDLE_SCHEMA, patients_data, 'patients data') phenopacket_ids = {} for item in patients_data["entry"]: @@ -69,7 +69,7 @@ def ingest_patients(patients_data, table_id, created_by): def ingest_observations(phenopacket_ids: Dict[str, str], observations_data): """ Takes FHIR Bundle containing Observation resources. """ # check if Observations data follows FHIR Bundle schema - _check_schema(FHIR_BUNDLE_SCHEMA, observations_data, 'observations data') + check_schema(FHIR_BUNDLE_SCHEMA, observations_data, 'observations data') for item in observations_data["entry"]: phenotypic_feature_data = observation_to_phenotypic_feature(item["resource"]) @@ -92,7 +92,7 @@ def ingest_observations(phenopacket_ids: Dict[str, str], observations_data): def ingest_conditions(phenopacket_ids: Dict[str, str], conditions_data): """ Takes FHIR Bundle containing Condition resources. """ # check if Conditions data follows FHIR Bundle schema - _check_schema(FHIR_BUNDLE_SCHEMA, conditions_data, 'conditions data') + check_schema(FHIR_BUNDLE_SCHEMA, conditions_data, 'conditions data') for item in conditions_data["entry"]: disease_data = condition_to_disease(item["resource"]) @@ -115,7 +115,7 @@ def ingest_conditions(phenopacket_ids: Dict[str, str], conditions_data): def ingest_specimens(phenopacket_ids: Dict[str, str], specimens_data): """ Takes FHIR Bundle containing Specimen resources. """ # check if Specimens data follows FHIR Bundle schema - _check_schema(FHIR_BUNDLE_SCHEMA, specimens_data, 'specimens data') + check_schema(FHIR_BUNDLE_SCHEMA, specimens_data, 'specimens data') for item in specimens_data["entry"]: biosample_data = specimen_to_biosample(item["resource"]) From 03f2ea652b9404ac215e203f5923631704a990fc Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Mon, 22 Jun 2020 22:51:11 -0400 Subject: [PATCH 161/190] fix boolean default --- chord_metadata_service/mcode/mcode_ingest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chord_metadata_service/mcode/mcode_ingest.py b/chord_metadata_service/mcode/mcode_ingest.py index efb620598..7063a4fe0 100644 --- a/chord_metadata_service/mcode/mcode_ingest.py +++ b/chord_metadata_service/mcode/mcode_ingest.py @@ -35,8 +35,8 @@ def ingest_mcodepacket(mcodepacket_data, table_id): "alternate_ids": subject.get("alternate_ids", None), "sex": subject.get("sex", ""), "date_of_birth": subject.get("date_of_birth", None), - "active": subject.get("active", None), - "deceased": subject.get("deceased", None) + "active": subject.get("active", False), + "deceased": subject.get("deceased", False) } ) _logger_message(s_created, subject) From f2cc834a03c54ced283a547943f1efee528c3f92 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Tue, 23 Jun 2020 10:58:39 -0400 Subject: [PATCH 162/190] adjust schemas --- chord_metadata_service/mcode/schemas.py | 63 ++++++++++++++----------- 1 file changed, 36 insertions(+), 27 deletions(-) diff --git a/chord_metadata_service/mcode/schemas.py b/chord_metadata_service/mcode/schemas.py index 895a41281..98da89a87 100644 --- a/chord_metadata_service/mcode/schemas.py +++ b/chord_metadata_service/mcode/schemas.py @@ -258,33 +258,6 @@ "required": ["id", "individual", "tumor_marker_code"] }, LABS_VITAL) -# TODO check required inb data dictionary -MCODE_CANCER_CONDITION_SCHEMA = describe_schema({ - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "condition_type": { - "type": "string", - "enum": [ - "primary", - "secondary" - ] - }, - "body_site": ONTOLOGY_CLASS_LIST, - "clinical_status": ONTOLOGY_CLASS, - "code": ONTOLOGY_CLASS, - "date_of_diagnosis": { - "type": "string", - "format": "date-time" - }, - "histology_morphology_behavior": ONTOLOGY_CLASS, - "extra_properties": EXTRA_PROPERTIES_SCHEMA - }, - "required": ["id", "condition_type", "code"] -}, LABS_VITAL) - MCODE_TNM_STAGING_SCHEMA = describe_schema({ "type": "object", "properties": { @@ -318,6 +291,38 @@ ] }, TNM_STAGING) +# TODO check required inb data dictionary +MCODE_CANCER_CONDITION_SCHEMA = describe_schema({ + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "condition_type": { + "type": "string", + "enum": [ + "primary", + "secondary" + ] + }, + "body_site": ONTOLOGY_CLASS_LIST, + "clinical_status": ONTOLOGY_CLASS, + "code": ONTOLOGY_CLASS, + "date_of_diagnosis": { + "type": "string", + "format": "date-time" + }, + "histology_morphology_behavior": ONTOLOGY_CLASS, + "tnm_staging": { + "type": "array", + "items": MCODE_TNM_STAGING_SCHEMA + }, + "extra_properties": EXTRA_PROPERTIES_SCHEMA + }, + "required": ["id", "condition_type", "code"] +}, LABS_VITAL) + + MCODE_CANCER_RELATED_PROCEDURE_SCHEMA = describe_schema({ "type": "object", "properties": { @@ -388,6 +393,10 @@ "date_of_death": { "type": "string" }, + "tumor_marker": { + "type": "array", + "items": MCODE_LABS_VITAL_SCHEMA + }, "cancer_disease_status": ONTOLOGY_CLASS, "extra_properties": EXTRA_PROPERTIES_SCHEMA } From 45ba018f6f1f964b7d89dad761c7aa6bfa6b3f81 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Tue, 23 Jun 2020 11:00:12 -0400 Subject: [PATCH 163/190] add mcode example for mcode v1.0 --- examples/mcode_example.json | 645 +++++++++++++++++++++++++++++------- 1 file changed, 526 insertions(+), 119 deletions(-) diff --git a/examples/mcode_example.json b/examples/mcode_example.json index db163d729..1dd889636 100644 --- a/examples/mcode_example.json +++ b/examples/mcode_example.json @@ -1,124 +1,531 @@ { - "id": "mcodepacket:01", - "subject": { - "id": "ind:HG00096", - "taxonomy": { - "id": "NCBITaxon:9606", - "label": "Homo sapiens" - }, - "date_of_birth": "1923-12-07", - "sex": "MALE", - "karyotypic_sex": "XY", - "created": "2020-03-25T18:10:14.017215Z", - "updated": "2020-03-25T18:10:14.017215Z" - }, - "genomics_report": { - "id": "genomics_resport:01", - "code": { - "id": "GTR000511179.13", - "label": "Clinical Exome" - }, - "performing_organization_name": "Test organization" - }, - "cancer_condition": { - "id": "cancer_condition:01", - "tnm_staging": [ - { - "id": "tnmstaging:01", - "tnm_type": "clinical", - "stage_group": { - "data_value": { - "id": "test", - "label": "t12" - } - }, - "primary_tumor_category": { - "data_value": { - "id": "test", - "label": "t12" - } - }, - "regional_nodes_category": { - "data_value": { - "id": "test", - "label": "t12" - } - }, - "distant_metastases_category": { - "data_value": { - "id": "test", - "label": "t12" - } - }, - "cancer_condition": "cancer_condition:01" + "id":"8670db4d-77ad-4bee-b38c-599453510c6d", + "subject":{ + "id":"6fcf56ed-8ad8-4395-a966-9ebee3822656" + }, + "cancer_condition":{ + "id":"5f2395f3-3271-441d-841a-af276c238eb0", + "clinical_status":{ + "id":"http://terminology.hl7.org/CodeSystem/condition-clinical:active", + "label":"active" + }, + "verification_status":{ + "id":"http://terminology.hl7.org/CodeSystem/condition-ver-status:confirmed", + "label":"confirmed" + }, + "code":{ + "id":"http://snomed.info/sct:254837009", + "label":"Malignant neoplasm of breast (disorder)" + }, + "date_of_diagnosis":"2019-03-02T22:02:57-05:00", + "condition_type":"primary", + "tnm_staging":[ + { + "id":"63a13799-00cb-4c7c-afa9-d997df3dbcf8", + "cancer_condition":"5f2395f3-3271-441d-841a-af276c238eb0", + "stage_group":{ + "data_value":{ + "id":"http://snomed.info/sct:261614003", + "label":"Stage 2A (qualifier value)" + } + }, + "tnm_type":"clinical", + "primary_tumor_category":{ + "data_value":{ + "id":"http://snomed.info/sct:67673008", + "label":"T2 category (finding)" + } + }, + "regional_nodes_category":{ + "data_value":{ + "id":"http://snomed.info/sct:62455006", + "label":"N0 category (finding)" + } + }, + "distant_metastases_category":{ + "data_value":{ + "id":"http://snomed.info/sct:62455006", + "label":"N0 category (finding)" + } + } + }, + { + "id":"b19e1d7c-3f01-4933-b0f1-36f8ca2d5bbc", + "cancer_condition":"5f2395f3-3271-441d-841a-af276c238eb0", + "stage_group":{ + "data_value":{ + "id":"http://snomed.info/sct:258219007", + "label":"Stage 2 (qualifier value)" + } + }, + "tnm_type":"clinical", + "primary_tumor_category":{ + "data_value":{ + "id":"http://snomed.info/sct:67673008", + "label":"T2 category (finding)" + } + }, + "regional_nodes_category":{ + "data_value":{ + "id":"http://snomed.info/sct:62455006", + "label":"N0 category (finding)" + } + }, + "distant_metastases_category":{ + "data_value":{ + "id":"http://snomed.info/sct:62455006", + "label":"N0 category (finding)" + } + } + } + ] + }, + "cancer_disease_status":{ + "id":"http://snomed.info/sct:268910001", + "label":"Patient's condition improved" + }, + "medication_statement":{ + "id":"fd5416ee-78ad-41ea-b847-dcc3e975dec9", + "medication_code":{ + "id":"http://www.nlm.nih.gov/research/umls/rxnorm:198240", + "label":"tamoxifen citrate 10 MG Oral Tablet" } - ], - "condition_type": "primary", - "body_site": [ + }, + "tumor_marker":[ { - "id": "442083009", - "label": "Anatomical or acquired body structure (body structure)" - } - ], - "clinical_status": { - "id": "active", - "label": "Active" - }, - "code": { - "id": "404087009", - "label": "Carcinosarcoma of skin (disorder)" - }, - "date_of_diagnosis": "2018-11-13T20:20:39Z", - "histology_morphology_behavior": { - "id": "372147008", - "label": "Kaposi's sarcoma - category (morphologic abnormality)" - }, - "verification_status": { - "id": "unconfirmed", - "label": "Unconfirmed" - } - }, - "medication_statement": { - "id": "medication_statement:02", - "medication_code": { - "id": "92052", - "label": "Verapamil Oral Tablet [Calan]" - }, - "termination_reason": [ - { - "id": "182992009", - "label": "Treatment completed" - }, - { - "id": "1821212992009", - "label": "Treatment completed" + "id":"9f785640-bdd2-4afe-93e2-3959b8994567", + "tumor_marker_code":{ + "id":"http://loinc.org:48676-1", + "label":"HER2 [Interpretation] in Tissue" + }, + "tumor_marker_data_value":{ + "id":"http://snomed.info/sct:260385009", + "label":"Negative (qualifier value)" + }, + "individual":"6fcf56ed-8ad8-4395-a966-9ebee3822656" + }, + { + "id":"deeb6699-867b-4260-9d52-18b302bc42da", + "tumor_marker_code":{ + "id":"http://loinc.org:48676-1", + "label":"HER2 [Interpretation] in Tissue" + }, + "tumor_marker_data_value":{ + "id":"http://snomed.info/sct:260385009", + "label":"Negative (qualifier value)" + }, + "individual":"6fcf56ed-8ad8-4395-a966-9ebee3822656" + }, + { + "id":"61f262ea-d3e9-4657-99c3-420d4b0f5280", + "tumor_marker_code":{ + "id":"http://loinc.org:16112-5", + "label":"Estrogen receptor [Interpretation] in Tissue" + }, + "tumor_marker_data_value":{ + "id":"http://snomed.info/sct:10828004", + "label":"Positive (qualifier value)" + }, + "individual":"6fcf56ed-8ad8-4395-a966-9ebee3822656" + }, + { + "id":"6fa53768-cd9e-4633-b9e9-e6a3b264f145", + "tumor_marker_code":{ + "id":"http://loinc.org:16113-3", + "label":"Progesterone receptor [Interpretation] in Tissue" + }, + "tumor_marker_data_value":{ + "id":"http://snomed.info/sct:260385009", + "label":"Negative (qualifier value)" + }, + "individual":"6fcf56ed-8ad8-4395-a966-9ebee3822656" } - ], - "treatment_intent": { - "id": "373808002", - "label": "Curative - procedure intent" - }, - "start_date": "2018-11-13T20:20:39Z", - "end_date": "2019-04-13T20:20:39Z" - }, - "cancer_related_procedures": [ - { - "id": "cancer_related_procedure:03", - "procedure_type": "radiation", - "code": { - "id": "33356009", - "label": "Betatron teleradiotherapy (procedure)" - }, - "body_site": [ - { - "id": "74805009", - "label": "Mammary gland sinus" - } - ], - "treatment_intent": { - "id": "373808002", - "label": "Curative - procedure intent" + ], + "cancer_related_procedures":[ + { + "id":"910dba95-2870-4e83-9f0f-b16da9c39297", + "code":{ + "id":"http://snomed.info/sct:69031006", + "label":"Excision of breast tissue (procedure)" + }, + "reason_reference":[ + "5f2395f3-3271-441d-841a-af276c238eb0" + ], + "procedure_type":"surgical" + }, + { + "id":"2676fcea-a788-4dbd-97b2-57e30df8cc0f", + "code":{ + "id":"http://snomed.info/sct:448385000", + "label":"Megavoltage radiation therapy using photons (procedure)" + }, + "reason_reference":[ + "5f2395f3-3271-441d-841a-af276c238eb0" + ], + "procedure_type":"radiation" + }, + { + "id":"9b3fef9e-8860-4721-8f74-609fd173c10a", + "code":{ + "id":"http://snomed.info/sct:448385000", + "label":"Megavoltage radiation therapy using photons (procedure)" + }, + "reason_reference":[ + "5f2395f3-3271-441d-841a-af276c238eb0" + ], + "procedure_type":"radiation" + }, + { + "id":"2d5367df-a102-4b18-b254-62fbdb1586ae", + "code":{ + "id":"http://snomed.info/sct:448385000", + "label":"Megavoltage radiation therapy using photons (procedure)" + }, + "reason_reference":[ + "5f2395f3-3271-441d-841a-af276c238eb0" + ], + "procedure_type":"radiation" + }, + { + "id":"a7609255-0482-4b36-ad90-c821e863404d", + "code":{ + "id":"http://snomed.info/sct:448385000", + "label":"Megavoltage radiation therapy using photons (procedure)" + }, + "reason_reference":[ + "5f2395f3-3271-441d-841a-af276c238eb0" + ], + "procedure_type":"radiation" + }, + { + "id":"83fdc3c6-7be8-422a-aa12-6252bcc54739", + "code":{ + "id":"http://snomed.info/sct:448385000", + "label":"Megavoltage radiation therapy using photons (procedure)" + }, + "reason_reference":[ + "5f2395f3-3271-441d-841a-af276c238eb0" + ], + "procedure_type":"radiation" + }, + { + "id":"51f0d170-cfbf-4ffa-8d05-3f4565cca150", + "code":{ + "id":"http://snomed.info/sct:448385000", + "label":"Megavoltage radiation therapy using photons (procedure)" + }, + "reason_reference":[ + "5f2395f3-3271-441d-841a-af276c238eb0" + ], + "procedure_type":"radiation" + }, + { + "id":"df19b727-fff0-4d5d-82bd-b9ddd7337dba", + "code":{ + "id":"http://snomed.info/sct:448385000", + "label":"Megavoltage radiation therapy using photons (procedure)" + }, + "reason_reference":[ + "5f2395f3-3271-441d-841a-af276c238eb0" + ], + "procedure_type":"radiation" + }, + { + "id":"5ccc627a-aa39-418b-842c-23fc200d7510", + "code":{ + "id":"http://snomed.info/sct:448385000", + "label":"Megavoltage radiation therapy using photons (procedure)" + }, + "reason_reference":[ + "5f2395f3-3271-441d-841a-af276c238eb0" + ], + "procedure_type":"radiation" + }, + { + "id":"93e6ed86-474b-4033-aae1-8547a0a63d23", + "code":{ + "id":"http://snomed.info/sct:448385000", + "label":"Megavoltage radiation therapy using photons (procedure)" + }, + "reason_reference":[ + "5f2395f3-3271-441d-841a-af276c238eb0" + ], + "procedure_type":"radiation" + }, + { + "id":"5d4ade2d-53ad-489a-8336-a53c8693672c", + "code":{ + "id":"http://snomed.info/sct:448385000", + "label":"Megavoltage radiation therapy using photons (procedure)" + }, + "reason_reference":[ + "5f2395f3-3271-441d-841a-af276c238eb0" + ], + "procedure_type":"radiation" + }, + { + "id":"03c7d064-63bc-46ef-adf3-fe64c5981d4e", + "code":{ + "id":"http://snomed.info/sct:448385000", + "label":"Megavoltage radiation therapy using photons (procedure)" + }, + "reason_reference":[ + "5f2395f3-3271-441d-841a-af276c238eb0" + ], + "procedure_type":"radiation" + }, + { + "id":"d722684b-f0d9-4931-b3de-7f3638a6551e", + "code":{ + "id":"http://snomed.info/sct:448385000", + "label":"Megavoltage radiation therapy using photons (procedure)" + }, + "reason_reference":[ + "5f2395f3-3271-441d-841a-af276c238eb0" + ], + "procedure_type":"radiation" + }, + { + "id":"168785f3-e7db-41c8-bb1d-a1aaef0e53e4", + "code":{ + "id":"http://snomed.info/sct:448385000", + "label":"Megavoltage radiation therapy using photons (procedure)" + }, + "reason_reference":[ + "5f2395f3-3271-441d-841a-af276c238eb0" + ], + "procedure_type":"radiation" + }, + { + "id":"5f7736ab-86b8-43d0-88cf-243260d1cc01", + "code":{ + "id":"http://snomed.info/sct:448385000", + "label":"Megavoltage radiation therapy using photons (procedure)" + }, + "reason_reference":[ + "5f2395f3-3271-441d-841a-af276c238eb0" + ], + "procedure_type":"radiation" + }, + { + "id":"b40ed30c-3ad7-468a-846a-1ab8ce682b64", + "code":{ + "id":"http://snomed.info/sct:448385000", + "label":"Megavoltage radiation therapy using photons (procedure)" + }, + "reason_reference":[ + "5f2395f3-3271-441d-841a-af276c238eb0" + ], + "procedure_type":"radiation" + }, + { + "id":"1c645d66-1bb0-4e89-9322-34aefb59f0b8", + "code":{ + "id":"http://snomed.info/sct:448385000", + "label":"Megavoltage radiation therapy using photons (procedure)" + }, + "reason_reference":[ + "5f2395f3-3271-441d-841a-af276c238eb0" + ], + "procedure_type":"radiation" + }, + { + "id":"a8889bc8-1523-4e79-aa4d-6026eeaa02bd", + "code":{ + "id":"http://snomed.info/sct:448385000", + "label":"Megavoltage radiation therapy using photons (procedure)" + }, + "reason_reference":[ + "5f2395f3-3271-441d-841a-af276c238eb0" + ], + "procedure_type":"radiation" + }, + { + "id":"37cd7a3f-d4e2-48b1-be07-7126491e3aef", + "code":{ + "id":"http://snomed.info/sct:448385000", + "label":"Megavoltage radiation therapy using photons (procedure)" + }, + "reason_reference":[ + "5f2395f3-3271-441d-841a-af276c238eb0" + ], + "procedure_type":"radiation" + }, + { + "id":"29e8e15f-9835-43fa-b76d-33d58a4b239c", + "code":{ + "id":"http://snomed.info/sct:448385000", + "label":"Megavoltage radiation therapy using photons (procedure)" + }, + "reason_reference":[ + "5f2395f3-3271-441d-841a-af276c238eb0" + ], + "procedure_type":"radiation" + }, + { + "id":"3ebbde3e-6a23-4c13-9c65-ef7b899c1473", + "code":{ + "id":"http://snomed.info/sct:448385000", + "label":"Megavoltage radiation therapy using photons (procedure)" + }, + "reason_reference":[ + "5f2395f3-3271-441d-841a-af276c238eb0" + ], + "procedure_type":"radiation" + }, + { + "id":"22265f09-ad9a-4c17-a450-27a0652ebba6", + "code":{ + "id":"http://snomed.info/sct:448385000", + "label":"Megavoltage radiation therapy using photons (procedure)" + }, + "reason_reference":[ + "5f2395f3-3271-441d-841a-af276c238eb0" + ], + "procedure_type":"radiation" + }, + { + "id":"ff832c55-e38a-4b07-b40a-53d1767a0f0b", + "code":{ + "id":"http://snomed.info/sct:448385000", + "label":"Megavoltage radiation therapy using photons (procedure)" + }, + "reason_reference":[ + "5f2395f3-3271-441d-841a-af276c238eb0" + ], + "procedure_type":"radiation" + }, + { + "id":"db38f007-6676-4449-a885-727bc358894a", + "code":{ + "id":"http://snomed.info/sct:448385000", + "label":"Megavoltage radiation therapy using photons (procedure)" + }, + "reason_reference":[ + "5f2395f3-3271-441d-841a-af276c238eb0" + ], + "procedure_type":"radiation" + }, + { + "id":"74dd61a3-cd90-4f5b-a6f7-9ac485e7d43e", + "code":{ + "id":"http://snomed.info/sct:448385000", + "label":"Megavoltage radiation therapy using photons (procedure)" + }, + "reason_reference":[ + "5f2395f3-3271-441d-841a-af276c238eb0" + ], + "procedure_type":"radiation" + }, + { + "id":"f1044644-8494-49e2-b6f8-2414467fe551", + "code":{ + "id":"http://snomed.info/sct:448385000", + "label":"Megavoltage radiation therapy using photons (procedure)" + }, + "reason_reference":[ + "5f2395f3-3271-441d-841a-af276c238eb0" + ], + "procedure_type":"radiation" + }, + { + "id":"156be3f2-f228-448d-b54f-59fc671e05e2", + "code":{ + "id":"http://snomed.info/sct:448385000", + "label":"Megavoltage radiation therapy using photons (procedure)" + }, + "reason_reference":[ + "5f2395f3-3271-441d-841a-af276c238eb0" + ], + "procedure_type":"radiation" + }, + { + "id":"424f3afb-3884-4a7d-af02-afdca441a644", + "code":{ + "id":"http://snomed.info/sct:448385000", + "label":"Megavoltage radiation therapy using photons (procedure)" + }, + "reason_reference":[ + "5f2395f3-3271-441d-841a-af276c238eb0" + ], + "procedure_type":"radiation" + }, + { + "id":"46c576a4-196d-47e2-ad25-3d4333c8d1ea", + "code":{ + "id":"http://snomed.info/sct:448385000", + "label":"Megavoltage radiation therapy using photons (procedure)" + }, + "reason_reference":[ + "5f2395f3-3271-441d-841a-af276c238eb0" + ], + "procedure_type":"radiation" + }, + { + "id":"11995568-c20e-45e4-9a03-64e6b17cacd7", + "code":{ + "id":"http://snomed.info/sct:448385000", + "label":"Megavoltage radiation therapy using photons (procedure)" + }, + "reason_reference":[ + "5f2395f3-3271-441d-841a-af276c238eb0" + ], + "procedure_type":"radiation" + }, + { + "id":"12de0ba3-ccff-483d-b493-178b3d6f12ee", + "code":{ + "id":"http://snomed.info/sct:448385000", + "label":"Megavoltage radiation therapy using photons (procedure)" + }, + "reason_reference":[ + "5f2395f3-3271-441d-841a-af276c238eb0" + ], + "procedure_type":"radiation" + }, + { + "id":"36cf5f93-1454-496c-b4d0-6a2166b188be", + "code":{ + "id":"http://snomed.info/sct:448385000", + "label":"Megavoltage radiation therapy using photons (procedure)" + }, + "reason_reference":[ + "5f2395f3-3271-441d-841a-af276c238eb0" + ], + "procedure_type":"radiation" + }, + { + "id":"6d86fdb3-ef23-46b6-b478-552da5e12504", + "code":{ + "id":"http://snomed.info/sct:448385000", + "label":"Megavoltage radiation therapy using photons (procedure)" + }, + "reason_reference":[ + "5f2395f3-3271-441d-841a-af276c238eb0" + ], + "procedure_type":"radiation" + }, + { + "id":"bc483d87-9735-4d70-ae6f-3433202943c1", + "code":{ + "id":"http://snomed.info/sct:448385000", + "label":"Megavoltage radiation therapy using photons (procedure)" + }, + "reason_reference":[ + "5f2395f3-3271-441d-841a-af276c238eb0" + ], + "procedure_type":"radiation" + }, + { + "id":"833b7ae7-3111-4f0b-bc84-f57bad2cb6c0", + "code":{ + "id":"http://snomed.info/sct:448385000", + "label":"Megavoltage radiation therapy using photons (procedure)" + }, + "reason_reference":[ + "5f2395f3-3271-441d-841a-af276c238eb0" + ], + "procedure_type":"radiation" } - } - ] -} + ] +} \ No newline at end of file From 1ed271477ab0c57eba7856a62c614024e3e4cd0a Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Tue, 23 Jun 2020 11:21:31 -0400 Subject: [PATCH 164/190] cleaning --- chord_metadata_service/mcode/descriptions.py | 5 +- .../mcode/mappings/mcode_profiles.py | 1 - chord_metadata_service/mcode/schemas.py | 10 +- .../mcode/tests/constants.py | 93 ------------------- .../mcode/tests/test_models.py | 31 +++---- 5 files changed, 25 insertions(+), 115 deletions(-) diff --git a/chord_metadata_service/mcode/descriptions.py b/chord_metadata_service/mcode/descriptions.py index 5c77e59cc..3f4d9b447 100644 --- a/chord_metadata_service/mcode/descriptions.py +++ b/chord_metadata_service/mcode/descriptions.py @@ -7,7 +7,6 @@ from chord_metadata_service.restapi.description_utils import EXTRA_PROPERTIES - GENETIC_SPECIMEN = { "description": "Class to describe a biosample used for genomics testing or analysis.", "properties": { @@ -63,7 +62,7 @@ "properties": { "id": "An arbitrary identifier for the genetics report.", "code": "An ontology or controlled vocabulary term to identify the laboratory test. " - "Accepted value sets: LOINC, GTR.", + "Accepted value sets: LOINC, GTR.", "performing_organization_name": "The name of the organization producing the genomics report.", "issued": "The date/time this report was issued.", "genetic_specimen": "List of related genetic specimens.", @@ -90,7 +89,7 @@ "id": "An arbitrary identifier for the cancer condition.", "condition_type": "Cancer condition type: primary or secondary.", "body_site": "Code for the body location, optionally pre-coordinating laterality or direction. " - "Accepted ontologies: SNOMED CT, ICD-O-3 and others.", + "Accepted ontologies: SNOMED CT, ICD-O-3 and others.", "laterality": "Body side of the body location, if needed to distinguish from a similar location " "on the other side of the body.", "clinical_status": "A flag indicating whether the condition is active or inactive, recurring, in remission, " diff --git a/chord_metadata_service/mcode/mappings/mcode_profiles.py b/chord_metadata_service/mcode/mappings/mcode_profiles.py index b252af8df..2c5859577 100644 --- a/chord_metadata_service/mcode/mappings/mcode_profiles.py +++ b/chord_metadata_service/mcode/mappings/mcode_profiles.py @@ -1,5 +1,4 @@ # Individual -# TODO there are two version of mcode profile URLs, both are not resolvable, monitor when stable URLs are being release MCODE_PATIENT = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-cancer-patient" MCODE_COMORBID_CONDITION = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-comorbid-condition" MCODE_ECOG_PERFORMANCE_STATUS = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-ecog-performance-status" diff --git a/chord_metadata_service/mcode/schemas.py b/chord_metadata_service/mcode/schemas.py index 98da89a87..499651212 100644 --- a/chord_metadata_service/mcode/schemas.py +++ b/chord_metadata_service/mcode/schemas.py @@ -165,6 +165,7 @@ "required": ["id", "specimen_type"] }, GENETIC_SPECIMEN) + MCODE_CANCER_GENETIC_VARIANT_SCHEMA = describe_schema({ "type": "object", "properties": { @@ -193,6 +194,7 @@ "required": ["id", "specimen_type"] }, CANCER_GENETIC_VARIANT) + MCODE_GENOMIC_REGION_STUDIED_SCHEMA = describe_schema({ "type": "object", "properties": { @@ -217,6 +219,7 @@ "required": ["id", "specimen_type"] }, GENOMIC_REGION_STUDIED) + MCODE_GENOMICS_REPORT_SCHEMA = describe_schema({ "type": "object", "properties": { @@ -242,6 +245,7 @@ "required": ["id", "code", "issued"] }, GENOMICS_REPORT) + MCODE_LABS_VITAL_SCHEMA = describe_schema({ "type": "object", "properties": { @@ -258,6 +262,7 @@ "required": ["id", "individual", "tumor_marker_code"] }, LABS_VITAL) + MCODE_TNM_STAGING_SCHEMA = describe_schema({ "type": "object", "properties": { @@ -291,7 +296,7 @@ ] }, TNM_STAGING) -# TODO check required inb data dictionary + MCODE_CANCER_CONDITION_SCHEMA = describe_schema({ "type": "object", "properties": { @@ -352,6 +357,7 @@ "required": ["id", "procedure_type", "code"] }, CANCER_RELATED_PROCEDURE) + MCODE_MEDICATION_STATEMENT_SCHEMA = describe_schema({ "type": "object", "properties": { @@ -374,7 +380,7 @@ "required": ["id", "medication_code"] }, MEDICATION_STATEMENT) -# TODO add subject fk to individual + MCODE_SCHEMA = describe_schema({ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "chord_metadata_service:mcode_schema", diff --git a/chord_metadata_service/mcode/tests/constants.py b/chord_metadata_service/mcode/tests/constants.py index 46ab4ce22..33ec199c1 100644 --- a/chord_metadata_service/mcode/tests/constants.py +++ b/chord_metadata_service/mcode/tests/constants.py @@ -19,99 +19,6 @@ "active": True } -# VALID_GENETIC_VARIANT_TESTED = { -# "id": "variant_tested:01", -# "method": { -# "id": "C17003", -# "label": "Polymerase Chain Reaction" -# }, -# "variant_tested_identifier": { -# "id": "360448", -# "label": "360448", -# }, -# "variant_tested_hgvs_name": [ -# "NC_000007.13:g.55086734A>G", -# "NC_000007.14:g.55019041A>G", -# "NM_001346897.2:c.-237A>G", -# "NM_001346898.2:c.-237A>G", -# "NM_001346899.1:c.-237A>G", -# "NM_001346941.2:c.-237A>G", -# "NM_005228.5:c.-237A>G", -# "NM_201282.2:c.-237A>G", -# "NM_201283.1:c.-237A>G", -# "NM_201284.2:c.-237A>G", -# "LRG_304t1:c.-237A>G", -# "LRG_304:g.5010A>G", -# "NG_007726.3:g.5010A>G" -# ], -# "variant_tested_description": "single nucleotide variant", -# "data_value": { -# "id": "LA6576-8", -# "label": "Positive", -# } -# } - -# INVALID_GENETIC_VARIANT_TESTED = { -# "id": "variant_tested:02", -# "method": ["invalid_value"], -# "variant_tested_identifier": { -# "id": "360448", -# "label": "360448", -# }, -# "variant_tested_hgvs_name": [ -# "NC_000007.13:g.55086734A>G", -# "NC_000007.14:g.55019041A>G", -# "NM_001346897.2:c.-237A>G", -# "NM_001346898.2:c.-237A>G", -# "NM_001346899.1:c.-237A>G", -# "NM_001346941.2:c.-237A>G", -# "NM_005228.5:c.-237A>G", -# "NM_201282.2:c.-237A>G", -# "NM_201283.1:c.-237A>G", -# "NM_201284.2:c.-237A>G", -# "LRG_304t1:c.-237A>G", -# "LRG_304:g.5010A>G", -# "NG_007726.3:g.5010A>G" -# ], -# "variant_tested_description": "single nucleotide variant", -# "data_value": { -# "id": "LA6576-8", -# "label": "Positive", -# } -# } -# -# VALID_GENETIC_VARIANT_FOUND = { -# "id": "variant_found:01", -# "method": { -# "id": "C17003", -# "label": "Polymerase Chain Reaction" -# }, -# "variant_found_identifier": { -# "id": "360448", -# "label": "360448", -# }, -# "variant_found_hgvs_name": [ -# "NC_000007.13:g.55086734A>G", -# "NC_000007.14:g.55019041A>G", -# "NM_001346897.2:c.-237A>G", -# "NM_001346898.2:c.-237A>G", -# "NM_001346899.1:c.-237A>G", -# "NM_001346941.2:c.-237A>G", -# "NM_005228.5:c.-237A>G", -# "NM_201282.2:c.-237A>G", -# "NM_201283.1:c.-237A>G", -# "NM_201284.2:c.-237A>G", -# "LRG_304t1:c.-237A>G", -# "LRG_304:g.5010A>G", -# "NG_007726.3:g.5010A>G" -# ], -# "variant_found_description": "single nucleotide variant", -# "genomic_source_class": { -# "id": "LA6684-0", -# "label": "Somatic", -# } -# } - def valid_genetic_report(): return { diff --git a/chord_metadata_service/mcode/tests/test_models.py b/chord_metadata_service/mcode/tests/test_models.py index 01c83c540..90d3944f1 100644 --- a/chord_metadata_service/mcode/tests/test_models.py +++ b/chord_metadata_service/mcode/tests/test_models.py @@ -4,7 +4,6 @@ from ..models import * from .constants import * from rest_framework import serializers -from django.core.validators import ValidationError class GenomicsReportTest(TestCase): @@ -29,21 +28,21 @@ def test_labs_vital(self): labs_vital = LabsVital.objects.get(id='labs_vital:01') self.assertIsInstance(labs_vital.tumor_marker_code, dict) - # def test_validation(self): - # invalid_obj = valid_labs_vital(self.individual) - # invalid_obj["id"] = "labs_vital:02" - # invalid_obj["tumor_marker_test"]["code"] = { - # "coding": [ - # { - # "code": "50610-5", - # "display": "Alpha-1-Fetoprotein", - # "system": "loinc.org" - # } - # ] - # } - # invalid = LabsVital.objects.create(**invalid_obj) - # with self.assertRaises(serializers.ValidationError): - # invalid.full_clean() + def test_validation(self): + invalid_obj = valid_labs_vital(self.individual) + invalid_obj["id"] = "labs_vital:02" + invalid_obj["tumor_marker_code"] = { + "coding": [ + { + "code": "50610-5", + "display": "Alpha-1-Fetoprotein", + "system": "loinc.org" + } + ] + } + invalid = LabsVital.objects.create(**invalid_obj) + with self.assertRaises(serializers.ValidationError): + invalid.full_clean() class CancerConditionTest(TestCase): From d154b037420df9a039c3085b8f7b9ef99377558f Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Tue, 23 Jun 2020 20:46:11 -0400 Subject: [PATCH 165/190] code review changes --- chord_metadata_service/mcode/mcode_ingest.py | 20 +++---- chord_metadata_service/mcode/models.py | 25 ++++----- .../mcode/parse_fhir_mcode.py | 54 +++++++++---------- chord_metadata_service/mcode/serializers.py | 1 + 4 files changed, 48 insertions(+), 52 deletions(-) diff --git a/chord_metadata_service/mcode/mcode_ingest.py b/chord_metadata_service/mcode/mcode_ingest.py index 7063a4fe0..e62b9bce0 100644 --- a/chord_metadata_service/mcode/mcode_ingest.py +++ b/chord_metadata_service/mcode/mcode_ingest.py @@ -1,6 +1,6 @@ import logging from chord_metadata_service.patients.models import Individual -from .models import * +from . import models as m logger = logging.getLogger("mcode_ingest") @@ -29,7 +29,7 @@ def ingest_mcodepacket(mcodepacket_data, table_id): # get and create Patient if subject: - subject, s_created = Individual.objects.get_or_create( + subject, s_created = m.Individual.objects.get_or_create( id=subject["id"], defaults={ "alternate_ids": subject.get("alternate_ids", None), @@ -51,7 +51,7 @@ def ingest_mcodepacket(mcodepacket_data, table_id): if cancer_condition_data: for cc in cancer_condition_data: - cancer_condition, cc_created = CancerCondition.objects.get_or_create( + cancer_condition, cc_created = m.CancerCondition.objects.get_or_create( id=cc["id"], defaults={ "code": cc["code"], @@ -68,7 +68,7 @@ def ingest_mcodepacket(mcodepacket_data, table_id): cancer_conditions.append(cancer_condition.id) if "tnm_staging" in cc: for tnms in cc["tnm_staging"]: - tnm_staging, tnms_created = TNMStaging.objects.get_or_create( + tnm_staging, tnms_created = m.TNMStaging.objects.get_or_create( id=tnms["id"], defaults={ "cancer_condition": cancer_condition, @@ -86,7 +86,7 @@ def ingest_mcodepacket(mcodepacket_data, table_id): crprocedures = [] if cancer_related_procedures: for crp in cancer_related_procedures: - cancer_related_procedure, crp_created = CancerRelatedProcedure.objects.get_or_create( + cancer_related_procedure, crp_created = m.CancerRelatedProcedure.objects.get_or_create( id=crp["id"], defaults={ "code": crp["code"], @@ -103,13 +103,13 @@ def ingest_mcodepacket(mcodepacket_data, table_id): if "reason_reference" in crp: related_cancer_conditions = [] for rr_id in crp["reason_reference"]: - condition = CancerCondition.objects.get(id=rr_id) + condition = m.CancerCondition.objects.get(id=rr_id) related_cancer_conditions.append(condition) cancer_related_procedure.reason_reference.set(related_cancer_conditions) # get and create MedicationStatement if medication_statement_data: - medication_statement, ms_created = MedicationStatement.objects.get_or_create( + medication_statement, ms_created = m.MedicationStatement.objects.get_or_create( id=medication_statement_data["id"], defaults={ "medication_code": medication_statement_data["medication_code"] @@ -129,17 +129,17 @@ def ingest_mcodepacket(mcodepacket_data, table_id): # get tumor marker if tumor_markers: for tm in tumor_markers: - tumor_marker, tm_created = LabsVital.objects.get_or_create( + tumor_marker, tm_created = m.LabsVital.objects.get_or_create( id=tm["id"], defaults={ "tumor_marker_code": tm["tumor_marker_code"], "tumor_marker_data_value": tm.get("tumor_marker_data_value", None), - "individual": Individual.objects.get(id=tm["individual"]) + "individual": m.Individual.objects.get(id=tm["individual"]) } ) _logger_message(tm_created, tumor_marker) - mcodepacket = MCodePacket( + mcodepacket = m.MCodePacket( id=new_mcodepacket["id"], subject=Individual.objects.get(id=new_mcodepacket["subject"]), genomics_report=new_mcodepacket.get("genomics_report", None), diff --git a/chord_metadata_service/mcode/models.py b/chord_metadata_service/mcode/models.py index a35a33816..90b34d9da 100644 --- a/chord_metadata_service/mcode/models.py +++ b/chord_metadata_service/mcode/models.py @@ -47,7 +47,7 @@ class CancerGeneticVariant(models.Model, IndexableMixin): amino_acid_change = JSONField(blank=True, null=True, validators=[ontology_validator], help_text=rec_help(d.CANCER_GENETIC_VARIANT, "amino_acid_change")) amino_acid_change_type = JSONField(blank=True, null=True, validators=[ontology_validator], - help_text=rec_help(d.CANCER_GENETIC_VARIANT, "amino_acid_change_type")) + help_text=rec_help(d.CANCER_GENETIC_VARIANT, "amino_acid_change_type")) cytogenetic_location = JSONField(blank=True, null=True, help_text=rec_help(d.CANCER_GENETIC_VARIANT, "cytogenetic_location")) cytogenetic_nomenclature = JSONField(blank=True, null=True, validators=[ontology_validator], @@ -57,9 +57,9 @@ class CancerGeneticVariant(models.Model, IndexableMixin): genomic_dna_change = JSONField(blank=True, null=True, validators=[ontology_validator], help_text=rec_help(d.CANCER_GENETIC_VARIANT, "genomic_dna_change")) genomic_source_class = JSONField(blank=True, null=True, validators=[ontology_validator], - help_text=rec_help(d.CANCER_GENETIC_VARIANT, "genomic_source_class")) + help_text=rec_help(d.CANCER_GENETIC_VARIANT, "genomic_source_class")) variation_code = JSONField(blank=True, null=True, validators=[ontology_list_validator], - help_text=rec_help(d.CANCER_GENETIC_VARIANT, "variation_code")) + help_text=rec_help(d.CANCER_GENETIC_VARIANT, "variation_code")) extra_properties = JSONField(blank=True, null=True, help_text=rec_help(d.CANCER_GENETIC_VARIANT, "extra_properties")) created = models.DateTimeField(auto_now=True) @@ -79,15 +79,16 @@ class GenomicRegionStudied(models.Model, IndexableMixin): id = models.CharField(primary_key=True, max_length=200, help_text=rec_help(d.GENOMIC_REGION_STUDIED, "id")) # TODO schema Range list dna_ranges_examined = JSONField(blank=True, null=True, validators=[ontology_list_validator], - help_text=rec_help(d.GENOMIC_REGION_STUDIED, "dna_ranges_examined")) + help_text=rec_help(d.GENOMIC_REGION_STUDIED, "dna_ranges_examined")) dna_region_description = ArrayField(models.CharField(max_length=100, - help_text=rec_help(d.GENOMIC_REGION_STUDIED, 'dna_region_description')), + help_text=rec_help(d.GENOMIC_REGION_STUDIED, + 'dna_region_description')), blank=True, default=list) gene_mutation = JSONField(blank=True, null=True, validators=[ontology_list_validator], help_text=rec_help(d.GENOMIC_REGION_STUDIED, "gene_mutation")) # TODO check: thisis not a Reference in mcode data dictionary why not? gene_studied = JSONField(blank=True, null=True, validators=[ontology_list_validator], - help_text=rec_help(d.GENOMIC_REGION_STUDIED, "gene_studied")) + help_text=rec_help(d.GENOMIC_REGION_STUDIED, "gene_studied")) genomic_reference_sequence_id = JSONField(blank=True, null=True, help_text=rec_help(d.GENOMIC_REGION_STUDIED, "genomic_reference_sequence_id")) @@ -150,7 +151,7 @@ class LabsVital(models.Model, IndexableMixin): tumor_marker_code = JSONField(validators=[ontology_validator], help_text=rec_help(d.LABS_VITAL, "tumor_marker_code")) tumor_marker_data_value = JSONField(blank=True, null=True, validators=[tumor_marker_data_value_validator], - help_text=rec_help(d.LABS_VITAL, "tumor_marker_data_value")) + help_text=rec_help(d.LABS_VITAL, "tumor_marker_data_value")) extra_properties = JSONField(blank=True, null=True, help_text=rec_help(d.LABS_VITAL, "extra_properties")) created = models.DateTimeField(auto_now=True) @@ -177,19 +178,19 @@ class CancerCondition(models.Model, IndexableMixin): condition_type = models.CharField(choices=CANCER_CONDITION_TYPE, max_length=200, help_text=rec_help(d.CANCER_CONDITION, "condition_type")) body_site = JSONField(null=True, validators=[ontology_list_validator], - help_text=rec_help(d.CANCER_CONDITION, 'body_site')) + help_text=rec_help(d.CANCER_CONDITION, 'body_site')) laterality = JSONField(blank=True, null=True, validators=[ontology_validator], - help_text=rec_help(d.CANCER_CONDITION, "laterality")) + help_text=rec_help(d.CANCER_CONDITION, "laterality")) clinical_status = JSONField(blank=True, null=True, validators=[ontology_validator], help_text=rec_help(d.CANCER_CONDITION, "clinical_status")) code = JSONField(validators=[ontology_validator], - help_text=rec_help(d.CANCER_CONDITION, "code")) + help_text=rec_help(d.CANCER_CONDITION, "code")) date_of_diagnosis = models.DateTimeField(blank=True, null=True, help_text=rec_help(d.CANCER_CONDITION, "date_of_diagnosis")) histology_morphology_behavior = JSONField(blank=True, null=True, validators=[ontology_validator], help_text=rec_help(d.CANCER_CONDITION, "histology_morphology_behavior")) verification_status = JSONField(blank=True, null=True, validators=[ontology_validator], - help_text=rec_help(d.CANCER_CONDITION, "verification_status")) + help_text=rec_help(d.CANCER_CONDITION, "verification_status")) extra_properties = JSONField(blank=True, null=True, help_text=rec_help(d.CANCER_CONDITION, "extra_properties")) created = models.DateTimeField(auto_now=True) @@ -253,7 +254,7 @@ class CancerRelatedProcedure(models.Model, IndexableMixin): help_text=rec_help(d.CANCER_RELATED_PROCEDURE, "procedure_type")) code = JSONField(validators=[ontology_validator], help_text=rec_help(d.CANCER_RELATED_PROCEDURE, "code")) body_site = JSONField(null=True, validators=[ontology_list_validator], - help_text=rec_help(d.CANCER_RELATED_PROCEDURE, 'body_site')) + help_text=rec_help(d.CANCER_RELATED_PROCEDURE, 'body_site')) laterality = JSONField(blank=True, null=True, validators=[ontology_validator], help_text=rec_help(d.CANCER_RELATED_PROCEDURE, "laterality")) treatment_intent = JSONField(blank=True, null=True, validators=[ontology_validator], diff --git a/chord_metadata_service/mcode/parse_fhir_mcode.py b/chord_metadata_service/mcode/parse_fhir_mcode.py index a44e5c7ef..f25340ed4 100644 --- a/chord_metadata_service/mcode/parse_fhir_mcode.py +++ b/chord_metadata_service/mcode/parse_fhir_mcode.py @@ -1,7 +1,7 @@ import uuid -from .mappings.mappings import * -from .mappings.mcode_profiles import * +from .mappings.mappings import MCODE_PROFILES_MAPPING +from .mappings import mcode_profiles as p from chord_metadata_service.restapi.schemas import FHIR_BUNDLE_SCHEMA from chord_metadata_service.restapi.fhir_ingest import check_schema @@ -25,7 +25,7 @@ def get_ontology_value(resource, codeable_concept_property): return ontology_value # will be raised if there is no "code" in Coding element except KeyError as e: - raise KeyError(e) + raise e def patient_to_individual(resource): @@ -96,18 +96,16 @@ def procedure_to_crprocedure(resource): if "bodySite" in resource: cancer_related_procedure["body_site"] = get_ontology_value(resource, "bodySite") if "reasonCode" in resource: - codes = [] - for code in resource["reasonCode"]["coding"]: - reason_code = { - "id": f"{code['system']}:{code['code']}", - "label": f"{code['display']}" - } - codes.append(reason_code) + codes = [{"id": f"{code['system']}:{code['code']}", "label": f"{code['display']}"} + for code in resource["reasonCode"]["coding"]] cancer_related_procedure["reason_code"] = codes if "reasonReference" in resource: cancer_conditions = [cc["reference"].split("uuid:")[-1] for cc in resource["reasonReference"]] cancer_related_procedure["reason_reference"] = cancer_conditions # TODO add laterality + # TODO add performed_period + if "performedPeriod" in resource: + cancer_related_procedure["extra_properties"] = resource["performedPeriod"] return cancer_related_procedure @@ -139,7 +137,7 @@ def _get_profiles(resource: dict, profile_urls: list): if p in resource_profiles: return True except KeyError as e: - raise KeyError(e) + raise e def condition_to_cancer_condition(resource): @@ -202,7 +200,7 @@ def parse_bundle(bundle): # get Patient's Cancer Condition if resource["resourceType"] == "Condition": resource_profiles = resource["meta"]["profile"] - cancer_conditions_profiles = [MCODE_PRIMARY_CANCER_CONDITION, MCODE_SECONDARY_CANCER_CONDITION] + cancer_conditions_profiles = [p.MCODE_PRIMARY_CANCER_CONDITION, p.MCODE_SECONDARY_CANCER_CONDITION] for cc in cancer_conditions_profiles: if cc in resource_profiles: cancer_condition = condition_to_cancer_condition(resource) @@ -214,7 +212,7 @@ def parse_bundle(bundle): # get TNM staging stage category if resource["resourceType"] == "Observation" and "meta" in resource: resource_profiles = resource["meta"]["profile"] - stage_groups = [MCODE_TNM_CLINICAL_STAGE_GROUP, MCODE_TNM_PATHOLOGIC_STAGE_GROUP] + stage_groups = [p.MCODE_TNM_CLINICAL_STAGE_GROUP, p.MCODE_TNM_PATHOLOGIC_STAGE_GROUP] for sg in stage_groups: if sg in resource_profiles: tnm_staging = {"id": resource["id"]} @@ -225,10 +223,7 @@ def parse_bundle(bundle): if sg == value: tnm_staging["tnm_type"] = key if "hasMember" in resource: - members = [] - for member in resource["hasMember"]: - member_observation_id = member["reference"].split('/')[-1] - members.append(member_observation_id) + members = [member["reference"].split('/')[-1] for member in resource["hasMember"]] # collect all members of this staging in a dict staging_to_members[resource["id"]] = members tnm_stagings.append(tnm_staging) @@ -236,16 +231,16 @@ def parse_bundle(bundle): # get all TNM staging members if resource["resourceType"] == "Observation" and "meta" in resource: primary_tumor_category = _get_tnm_staging_property(resource, - [MCODE_TNM_CLINICAL_PRIMARY_TUMOR_CATEGORY, - MCODE_TNM_PATHOLOGIC_PRIMARY_TUMOR_CATEGORY], + [p.MCODE_TNM_CLINICAL_PRIMARY_TUMOR_CATEGORY, + p.MCODE_TNM_PATHOLOGIC_PRIMARY_TUMOR_CATEGORY], 'primary_tumor_category') regional_nodes_category = _get_tnm_staging_property(resource, - [MCODE_TNM_CLINICAL_REGIONAL_NODES_CATEGORY, - MCODE_TNM_PATHOLOGIC_REGIONAL_NODES_CATEGORY], + [p.MCODE_TNM_CLINICAL_REGIONAL_NODES_CATEGORY, + p.MCODE_TNM_PATHOLOGIC_REGIONAL_NODES_CATEGORY], 'regional_nodes_category') distant_metastases_category = _get_tnm_staging_property(resource, - [MCODE_TNM_CLINICAL_REGIONAL_NODES_CATEGORY, - MCODE_TNM_PATHOLOGIC_REGIONAL_NODES_CATEGORY], + [p.MCODE_TNM_CLINICAL_REGIONAL_NODES_CATEGORY, + p.MCODE_TNM_PATHOLOGIC_REGIONAL_NODES_CATEGORY], 'distant_metastases_category') for category in [primary_tumor_category, regional_nodes_category, distant_metastases_category]: @@ -255,7 +250,7 @@ def parse_bundle(bundle): # get Cancer Related Procedure if resource["resourceType"] == "Procedure" and "meta" in resource: resource_profiles = resource["meta"]["profile"] - procedure_profiles = [MCODE_CANCER_RELATED_RADIATION_PROCEDURE, MCODE_CANCER_RELATED_SURGICAL_PROCEDURE] + procedure_profiles = [p.MCODE_CANCER_RELATED_RADIATION_PROCEDURE, p.MCODE_CANCER_RELATED_SURGICAL_PROCEDURE] for pp in procedure_profiles: if pp in resource_profiles: procedure = procedure_to_crprocedure(resource) @@ -266,18 +261,18 @@ def parse_bundle(bundle): # get tumor marker if resource["resourceType"] == "Observation" and "meta" in resource: - if MCODE_TUMOR_MARKER in resource["meta"]["profile"]: + if p.MCODE_TUMOR_MARKER in resource["meta"]["profile"]: labs_vital = observation_to_labs_vital(resource) tumor_markers.append(labs_vital) # get Medication Statement if resource["resourceType"] == "MedicationStatement" and "meta" in resource: - if MCODE_MEDICATION_STATEMENT in resource["meta"]["profile"]: + if p.MCODE_MEDICATION_STATEMENT in resource["meta"]["profile"]: mcodepacket["medication_statement"] = get_medication_statement(resource) # get Cancer Disease Status if resource["resourceType"] == "Observation" and "meta" in resource: - if MCODE_CANCER_DISEASE_STATUS in resource["meta"]["profile"]: + if p.MCODE_CANCER_DISEASE_STATUS in resource["meta"]["profile"]: # TODO change refactor observation conversion mcodepacket["cancer_disease_status"] = observation_to_labs_vital(resource)["tumor_marker_data_value"] @@ -289,7 +284,7 @@ def parse_bundle(bundle): if cancer_conditions: mcodepacket["cancer_condition"] = cancer_conditions - # mcodepacket["tnm_staging"] = tnm_stagings + mcodepacket["tumor_marker"] = tumor_markers mcodepacket["cancer_related_procedures"] = cancer_related_procedures @@ -300,7 +295,6 @@ def parse_bundle(bundle): if "tnm_staging" in cc: cc["tnm_staging"].append(tnms) else: - cc["tnm_staging"] = [] - cc["tnm_staging"].append(tnms) + cc["tnm_staging"] = [tnms] return mcodepacket diff --git a/chord_metadata_service/mcode/serializers.py b/chord_metadata_service/mcode/serializers.py index dff6ec516..2584ea951 100644 --- a/chord_metadata_service/mcode/serializers.py +++ b/chord_metadata_service/mcode/serializers.py @@ -105,6 +105,7 @@ def to_representation(self, instance): many=True, required=False).data response['medication_statement'] = MedicationStatementSerializer(instance.medication_statement, required=False).data + # TODO add tumor marker return response class Meta: From 672e2940a3ad2df6f5cf18fb7a71c50ff109b5a6 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Thu, 25 Jun 2020 11:27:27 -0400 Subject: [PATCH 166/190] Mcode lint --- chord_metadata_service/mcode/admin.py | 22 +++---- chord_metadata_service/mcode/api_views.py | 44 +++++++------- .../mcode/mappings/mappings.py | 56 +++++++++--------- .../mcode/mappings/mcode_profiles.py | 58 ++++++++++--------- chord_metadata_service/mcode/models.py | 10 ++-- .../mcode/parse_fhir_mcode.py | 18 +++--- chord_metadata_service/mcode/schemas.py | 30 +++++----- chord_metadata_service/mcode/serializers.py | 22 +++---- .../mcode/tests/test_models.py | 44 +++++++------- chord_metadata_service/mcode/validators.py | 10 ++-- chord_metadata_service/mcode/views.py | 3 - 11 files changed, 157 insertions(+), 160 deletions(-) delete mode 100644 chord_metadata_service/mcode/views.py diff --git a/chord_metadata_service/mcode/admin.py b/chord_metadata_service/mcode/admin.py index 5e52ada17..78355dfcb 100644 --- a/chord_metadata_service/mcode/admin.py +++ b/chord_metadata_service/mcode/admin.py @@ -1,52 +1,52 @@ from django.contrib import admin -from .models import * +from . import models as m -@admin.register(GeneticSpecimen) +@admin.register(m.GeneticSpecimen) class GeneticSpecimenAdmin(admin.ModelAdmin): pass -@admin.register(CancerGeneticVariant) +@admin.register(m.CancerGeneticVariant) class CancerGeneticVariantAdmin(admin.ModelAdmin): pass -@admin.register(GenomicRegionStudied) +@admin.register(m.GenomicRegionStudied) class GenomicRegionStudiedAdmin(admin.ModelAdmin): pass -@admin.register(GenomicsReport) +@admin.register(m.GenomicsReport) class GenomicsReportAdmin(admin.ModelAdmin): pass -@admin.register(LabsVital) +@admin.register(m.LabsVital) class LabsVitalAdmin(admin.ModelAdmin): pass -@admin.register(CancerCondition) +@admin.register(m.CancerCondition) class CancerConditionAdmin(admin.ModelAdmin): pass -@admin.register(TNMStaging) +@admin.register(m.TNMStaging) class TNMStagingAdmin(admin.ModelAdmin): pass -@admin.register(CancerRelatedProcedure) +@admin.register(m.CancerRelatedProcedure) class CancerRelatedProcedureAdmin(admin.ModelAdmin): pass -@admin.register(MedicationStatement) +@admin.register(m.MedicationStatement) class MedicationStatementAdmin(admin.ModelAdmin): pass -@admin.register(MCodePacket) +@admin.register(m.MCodePacket) class MCodePacketAdmin(admin.ModelAdmin): pass diff --git a/chord_metadata_service/mcode/api_views.py b/chord_metadata_service/mcode/api_views.py index 38a167c85..67fb3df1d 100644 --- a/chord_metadata_service/mcode/api_views.py +++ b/chord_metadata_service/mcode/api_views.py @@ -3,10 +3,8 @@ from rest_framework.decorators import api_view, permission_classes from rest_framework.permissions import AllowAny from rest_framework.response import Response -from .serializers import * from .schemas import MCODE_SCHEMA -from .models import * -from .serializers import * +from . import models as m, serializers as s from chord_metadata_service.restapi.api_renderers import PhenopacketsRenderer from chord_metadata_service.restapi.pagination import LargeResultsSetPagination @@ -17,53 +15,53 @@ class McodeModelViewSet(viewsets.ModelViewSet): class GeneticSpecimenViewSet(McodeModelViewSet): - queryset = GeneticSpecimen.objects.all() - serializer_class = GeneticSpecimenSerializer + queryset = m.GeneticSpecimen.objects.all() + serializer_class = s.GeneticSpecimenSerializer class CancerGeneticVariantViewSet(McodeModelViewSet): - queryset = CancerGeneticVariant.objects.all() - serializer_class = CancerGeneticVariantSerializer + queryset = m.CancerGeneticVariant.objects.all() + serializer_class = s.CancerGeneticVariantSerializer class GenomicRegionStudiedViewSet(McodeModelViewSet): - queryset = GenomicRegionStudied.objects.all() - serializer_class = GenomicRegionStudiedSerializer + queryset = m.GenomicRegionStudied.objects.all() + serializer_class = s.GenomicRegionStudiedSerializer class GenomicsReportViewSet(McodeModelViewSet): - queryset = GenomicsReport.objects.all() - serializer_class = GenomicsReportSerializer + queryset = m.GenomicsReport.objects.all() + serializer_class = s.GenomicsReportSerializer class LabsVitalViewSet(McodeModelViewSet): - queryset = LabsVital.objects.all() - serializer_class = LabsVitalSerializer + queryset = m.LabsVital.objects.all() + serializer_class = s.LabsVitalSerializer class CancerConditionViewSet(McodeModelViewSet): - queryset = CancerCondition.objects.all() - serializer_class = CancerConditionSerializer + queryset = m.CancerCondition.objects.all() + serializer_class = s.CancerConditionSerializer class TNMStagingViewSet(McodeModelViewSet): - queryset = TNMStaging.objects.all() - serializer_class = TNMStagingSerializer + queryset = m.TNMStaging.objects.all() + serializer_class = s.TNMStagingSerializer class CancerRelatedProcedureViewSet(McodeModelViewSet): - queryset = CancerRelatedProcedure.objects.all() - serializer_class = CancerRelatedProcedureSerializer + queryset = m.CancerRelatedProcedure.objects.all() + serializer_class = s.CancerRelatedProcedureSerializer class MedicationStatementViewSet(McodeModelViewSet): - queryset = MedicationStatement.objects.all() - serializer_class = MedicationStatementSerializer + queryset = m.MedicationStatement.objects.all() + serializer_class = s.MedicationStatementSerializer class MCodePacketViewSet(McodeModelViewSet): - queryset = MCodePacket.objects.all() - serializer_class = MCodePacketSerializer + queryset = m.MCodePacket.objects.all() + serializer_class = s.MCodePacketSerializer @api_view(["GET"]) diff --git a/chord_metadata_service/mcode/mappings/mappings.py b/chord_metadata_service/mcode/mappings/mappings.py index 4149d704b..8e9ef3d3c 100644 --- a/chord_metadata_service/mcode/mappings/mappings.py +++ b/chord_metadata_service/mcode/mappings/mappings.py @@ -1,77 +1,77 @@ -from .mcode_profiles import * +from . import mcode_profiles as mp MCODE_PROFILES_MAPPING = { "patient": { - "profile": MCODE_PATIENT, + "profile": mp.MCODE_PATIENT, "properties_profile": { - "comorbid_condition": MCODE_COMORBID_CONDITION, - "ecog_performance_status": MCODE_ECOG_PERFORMANCE_STATUS, - "karnofsky": MCODE_KARNOFSKY + "comorbid_condition": mp.MCODE_COMORBID_CONDITION, + "ecog_performance_status": mp.MCODE_ECOG_PERFORMANCE_STATUS, + "karnofsky": mp.MCODE_KARNOFSKY } }, "genetic_specimen": { - "profile": MCODE_GENETIC_SPECIMEN, + "profile": mp.MCODE_GENETIC_SPECIMEN, "properties_profile": { - "laterality": MCODE_LATERALITY + "laterality": mp.MCODE_LATERALITY } }, "cancer_genetic_variant": { - "profile": MCODE_CANCER_GENETIC_VARIANT + "profile": mp.MCODE_CANCER_GENETIC_VARIANT }, "genomic_region_studied": { - "profile": MCODE_GENOMIC_REGION_STUDIED + "profile": mp.MCODE_GENOMIC_REGION_STUDIED }, "genomics_report": { - "profile": MCODE_GENOMICS_REPORT + "profile": mp.MCODE_GENOMICS_REPORT }, "labs_vital": { - "profile": MCODE_TUMOR_MARKER + "profile": mp.MCODE_TUMOR_MARKER }, "cancer_condition": { "profile": { - "primary": MCODE_PRIMARY_CANCER_CONDITION, - "secondary": MCODE_SECONDARY_CANCER_CONDITION + "primary": mp.MCODE_PRIMARY_CANCER_CONDITION, + "secondary": mp.MCODE_SECONDARY_CANCER_CONDITION }, "properties_profile": { - "histology_morphology_behavior": MCODE_HISTOLOGY_MORPHOLOGY_BEHAVIOR + "histology_morphology_behavior": mp.MCODE_HISTOLOGY_MORPHOLOGY_BEHAVIOR } }, "tnm_staging": { "properties_profile": { "stage_group": { - "clinical": MCODE_TNM_CLINICAL_STAGE_GROUP, - "pathologic": MCODE_TNM_PATHOLOGIC_STAGE_GROUP + "clinical": mp.MCODE_TNM_CLINICAL_STAGE_GROUP, + "pathologic": mp.MCODE_TNM_PATHOLOGIC_STAGE_GROUP }, "primary_tumor_category": { - "clinical": MCODE_TNM_CLINICAL_PRIMARY_TUMOR_CATEGORY, - "pathologic": MCODE_TNM_PATHOLOGIC_PRIMARY_TUMOR_CATEGORY + "clinical": mp.MCODE_TNM_CLINICAL_PRIMARY_TUMOR_CATEGORY, + "pathologic": mp.MCODE_TNM_PATHOLOGIC_PRIMARY_TUMOR_CATEGORY }, "regional_nodes_category": { - "clinical": MCODE_TNM_CLINICAL_REGIONAL_NODES_CATEGORY, - "pathologic": MCODE_TNM_PATHOLOGIC_REGIONAL_NODES_CATEGORY + "clinical": mp.MCODE_TNM_CLINICAL_REGIONAL_NODES_CATEGORY, + "pathologic": mp.MCODE_TNM_PATHOLOGIC_REGIONAL_NODES_CATEGORY }, "distant_metastases_category": { - "clinical": MCODE_TNM_CLINICAL_DISTANT_METASTASES_CATEGORY, - "pathologic": MCODE_TNM_PATHOLOGIC_DISTANT_METASTASES_CATEGORY + "clinical": mp.MCODE_TNM_CLINICAL_DISTANT_METASTASES_CATEGORY, + "pathologic": mp.MCODE_TNM_PATHOLOGIC_DISTANT_METASTASES_CATEGORY } } }, "cancer_related_procedure": { "profile": { - "radiation": MCODE_CANCER_RELATED_RADIATION_PROCEDURE, - "surgical": MCODE_CANCER_RELATED_SURGICAL_PROCEDURE + "radiation": mp.MCODE_CANCER_RELATED_RADIATION_PROCEDURE, + "surgical": mp.MCODE_CANCER_RELATED_SURGICAL_PROCEDURE } }, "medication_statement": { - "profile": MCODE_MEDICATION_STATEMENT, + "profile": mp.MCODE_MEDICATION_STATEMENT, "properties_profile": { - "termination_reason": MCODE_TERMINATION_REASON, - "treatment_intent": MCODE_TREATMENT_INTENT + "termination_reason": mp.MCODE_TERMINATION_REASON, + "treatment_intent": mp.MCODE_TREATMENT_INTENT } }, "mcodepacket": { "properties_profile": { - "cancer_disease_status": MCODE_CANCER_DISEASE_STATUS + "cancer_disease_status": mp.MCODE_CANCER_DISEASE_STATUS } } } diff --git a/chord_metadata_service/mcode/mappings/mcode_profiles.py b/chord_metadata_service/mcode/mappings/mcode_profiles.py index 2c5859577..24a09522a 100644 --- a/chord_metadata_service/mcode/mappings/mcode_profiles.py +++ b/chord_metadata_service/mcode/mappings/mcode_profiles.py @@ -1,60 +1,64 @@ +def mcode_structure(structure: str): + return f"http://hl7.org/fhir/us/mcode/StructureDefinition/{structure}" + + # Individual -MCODE_PATIENT = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-cancer-patient" -MCODE_COMORBID_CONDITION = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-comorbid-condition" -MCODE_ECOG_PERFORMANCE_STATUS = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-ecog-performance-status" -MCODE_KARNOFSKY = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-karnofsky-performance-status" +MCODE_PATIENT = mcode_structure("mcode-cancer-patient") +MCODE_COMORBID_CONDITION = mcode_structure("mcode-comorbid-condition") +MCODE_ECOG_PERFORMANCE_STATUS = mcode_structure("mcode-ecog-performance-status") +MCODE_KARNOFSKY = mcode_structure("mcode-karnofsky-performance-status") # GeneticSpecimen -MCODE_GENETIC_SPECIMEN = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-genetic-specimen" +MCODE_GENETIC_SPECIMEN = mcode_structure("mcode-genetic-specimen") # GeneticVariant -MCODE_CANCER_GENETIC_VARIANT = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-cancer-genetic-variant" +MCODE_CANCER_GENETIC_VARIANT = mcode_structure("mcode-cancer-genetic-variant") # GenomicRegionStudied -MCODE_GENOMIC_REGION_STUDIED = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-genomic-region-studied" +MCODE_GENOMIC_REGION_STUDIED = mcode_structure("mcode-genomic-region-studied") # GenomicsReport -MCODE_GENOMICS_REPORT = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-cancer-genomics-report" +MCODE_GENOMICS_REPORT = mcode_structure("mcode-cancer-genomics-report") # LabsVital # the following are present in Ballout 1 version but not in 1.0.0 version -MCODE_TUMOR_MARKER = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-tumor-marker" +MCODE_TUMOR_MARKER = mcode_structure("mcode-tumor-marker") # CancerCondition -MCODE_PRIMARY_CANCER_CONDITION = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-primary-cancer-condition" -MCODE_SECONDARY_CANCER_CONDITION = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-secondary-cancer-condition" +MCODE_PRIMARY_CANCER_CONDITION = mcode_structure("mcode-primary-cancer-condition") +MCODE_SECONDARY_CANCER_CONDITION = mcode_structure("mcode-secondary-cancer-condition") # TNMStaging # CLINICAL -MCODE_TNM_CLINICAL_STAGE_GROUP = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-tnm-clinical-stage-group" -MCODE_TNM_CLINICAL_PRIMARY_TUMOR_CATEGORY = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-tnm-clinical-primary-tumor-category" -MCODE_TNM_CLINICAL_REGIONAL_NODES_CATEGORY = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-tnm-clinical-regional-nodes-category" -MCODE_TNM_CLINICAL_DISTANT_METASTASES_CATEGORY = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-tnm-clinical-distant-metastases-category" +MCODE_TNM_CLINICAL_STAGE_GROUP = mcode_structure("mcode-tnm-clinical-stage-group") +MCODE_TNM_CLINICAL_PRIMARY_TUMOR_CATEGORY = mcode_structure("mcode-tnm-clinical-primary-tumor-category") +MCODE_TNM_CLINICAL_REGIONAL_NODES_CATEGORY = mcode_structure("mcode-tnm-clinical-regional-nodes-category") +MCODE_TNM_CLINICAL_DISTANT_METASTASES_CATEGORY = mcode_structure("mcode-tnm-clinical-distant-metastases-category") # PATHOLOGIC -MCODE_TNM_PATHOLOGIC_STAGE_GROUP = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-tnm-pathological-stage-group" -MCODE_TNM_PATHOLOGIC_PRIMARY_TUMOR_CATEGORY = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-tnm-pathological-primary-tumor-category" -MCODE_TNM_PATHOLOGIC_REGIONAL_NODES_CATEGORY = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-tnm-pathological-regional-nodes-category" -MCODE_TNM_PATHOLOGIC_DISTANT_METASTASES_CATEGORY = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-tnm-pathological-distant-metastases-category" +MCODE_TNM_PATHOLOGIC_STAGE_GROUP = mcode_structure("mcode-tnm-pathological-stage-group") +MCODE_TNM_PATHOLOGIC_PRIMARY_TUMOR_CATEGORY = mcode_structure("mcode-tnm-pathological-primary-tumor-category") +MCODE_TNM_PATHOLOGIC_REGIONAL_NODES_CATEGORY = mcode_structure("mcode-tnm-pathological-regional-nodes-category") +MCODE_TNM_PATHOLOGIC_DISTANT_METASTASES_CATEGORY = mcode_structure("mcode-tnm-pathological-distant-metastases-category") # CancerRelatedProcedure # CancerRelatedRadiationProcedure -MCODE_CANCER_RELATED_RADIATION_PROCEDURE = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-cancer-related-radiation-procedure" +MCODE_CANCER_RELATED_RADIATION_PROCEDURE = mcode_structure("mcode-cancer-related-radiation-procedure") # CancerRelatedSurgicalProcedure -MCODE_CANCER_RELATED_SURGICAL_PROCEDURE = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-cancer-related-surgical-procedure" +MCODE_CANCER_RELATED_SURGICAL_PROCEDURE = mcode_structure("mcode-cancer-related-surgical-procedure") # MedicationStatement -MCODE_MEDICATION_STATEMENT = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-cancer-related-medication-statement" +MCODE_MEDICATION_STATEMENT = mcode_structure("mcode-cancer-related-medication-statement") # mCodePacket -MCODE_CANCER_DISEASE_STATUS = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-cancer-disease-status" +MCODE_CANCER_DISEASE_STATUS = mcode_structure("mcode-cancer-disease-status") # Extension definitions -MCODE_LATERALITY = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-laterality" +MCODE_LATERALITY = mcode_structure("mcode-laterality") # CancerCondition histology_morphology_behavior -MCODE_HISTOLOGY_MORPHOLOGY_BEHAVIOR = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-histology-morphology-behavior" +MCODE_HISTOLOGY_MORPHOLOGY_BEHAVIOR = mcode_structure("mcode-histology-morphology-behavior") # MedicationStatement -MCODE_TERMINATION_REASON = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-termination-reason" -MCODE_TREATMENT_INTENT = "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-treatment-intent" +MCODE_TERMINATION_REASON = mcode_structure("mcode-termination-reason") +MCODE_TREATMENT_INTENT = mcode_structure("mcode-treatment-intent") diff --git a/chord_metadata_service/mcode/models.py b/chord_metadata_service/mcode/models.py index 90b34d9da..11734968c 100644 --- a/chord_metadata_service/mcode/models.py +++ b/chord_metadata_service/mcode/models.py @@ -135,7 +135,7 @@ def __str__(self): return str(self.id) -################################# Labs/Vital ################################# +# ================================ Labs/Vital ================================ class LabsVital(models.Model, IndexableMixin): @@ -164,7 +164,7 @@ def __str__(self): return str(self.id) -################################# Disease ################################# +# ================================== Disease ================================== class CancerCondition(models.Model, IndexableMixin): """ @@ -236,9 +236,9 @@ def __str__(self): return str(self.id) -################################# Treatment ################################# +# ================================= Treatment ================================= -###### Procedure ###### +# ==== Procedure ==== class CancerRelatedProcedure(models.Model, IndexableMixin): """ @@ -277,7 +277,7 @@ def __str__(self): return str(self.id) -###### Medication Statement ###### +# ==== Medication Statement ==== class MedicationStatement(models.Model, IndexableMixin): diff --git a/chord_metadata_service/mcode/parse_fhir_mcode.py b/chord_metadata_service/mcode/parse_fhir_mcode.py index f25340ed4..221e11096 100644 --- a/chord_metadata_service/mcode/parse_fhir_mcode.py +++ b/chord_metadata_service/mcode/parse_fhir_mcode.py @@ -131,13 +131,11 @@ def _get_tnm_staging_property(resource: dict, profile_urls: list, category_type= def _get_profiles(resource: dict, profile_urls: list): - try: - resource_profiles = resource["meta"]["profile"] - for p in profile_urls: - if p in resource_profiles: - return True - except KeyError as e: - raise e + # Can raise a KeyError in some cases + resource_profiles = resource["meta"]["profile"] + for p_url in profile_urls: + if p_url in resource_profiles: + return True def condition_to_cancer_condition(resource): @@ -219,9 +217,9 @@ def parse_bundle(bundle): tnm_stage_group = observation_to_tnm_staging(resource) tnm_staging["cancer_condition"] = tnm_stage_group["cancer_condition"] tnm_staging["stage_group"] = tnm_stage_group["tnm_staging_value"] - for key, value in MCODE_PROFILES_MAPPING["tnm_staging"]["properties_profile"]["stage_group"].items(): - if sg == value: - tnm_staging["tnm_type"] = key + for k, v in MCODE_PROFILES_MAPPING["tnm_staging"]["properties_profile"]["stage_group"].items(): + if sg == v: + tnm_staging["tnm_type"] = k if "hasMember" in resource: members = [member["reference"].split('/')[-1] for member in resource["hasMember"]] # collect all members of this staging in a dict diff --git a/chord_metadata_service/mcode/schemas.py b/chord_metadata_service/mcode/schemas.py index 499651212..1037d1575 100644 --- a/chord_metadata_service/mcode/schemas.py +++ b/chord_metadata_service/mcode/schemas.py @@ -2,11 +2,11 @@ from chord_metadata_service.restapi.schemas import ONTOLOGY_CLASS, ONTOLOGY_CLASS_LIST, EXTRA_PROPERTIES_SCHEMA from chord_metadata_service.restapi.description_utils import describe_schema from chord_metadata_service.patients.schemas import INDIVIDUAL_SCHEMA -from .descriptions import * +from . import descriptions as d -################################## mCode/FHIR based schemas ################################## +# ========================= mCode/FHIR based schemas ========================= -### FHIR datatypes +# === FHIR datatypes === # FHIR Quantity https://www.hl7.org/fhir/datatypes.html#Quantity QUANTITY = { @@ -98,7 +98,7 @@ "additionalProperties": False } -### FHIR based mCode elements +# === FHIR based mCode elements === TIME_OR_PERIOD = { "$schema": "http://json-schema.org/draft-07/schema#", @@ -148,7 +148,7 @@ ) -############################## Metadata service mCode based schemas ############################## +# =================== Metadata service mCode based schemas =================== MCODE_GENETIC_SPECIMEN_SCHEMA = describe_schema({ @@ -163,7 +163,7 @@ "extra_properties": EXTRA_PROPERTIES_SCHEMA }, "required": ["id", "specimen_type"] -}, GENETIC_SPECIMEN) +}, d.GENETIC_SPECIMEN) MCODE_CANCER_GENETIC_VARIANT_SCHEMA = describe_schema({ @@ -192,7 +192,7 @@ "extra_properties": EXTRA_PROPERTIES_SCHEMA }, "required": ["id", "specimen_type"] -}, CANCER_GENETIC_VARIANT) +}, d.CANCER_GENETIC_VARIANT) MCODE_GENOMIC_REGION_STUDIED_SCHEMA = describe_schema({ @@ -217,7 +217,7 @@ "extra_properties": EXTRA_PROPERTIES_SCHEMA }, "required": ["id", "specimen_type"] -}, GENOMIC_REGION_STUDIED) +}, d.GENOMIC_REGION_STUDIED) MCODE_GENOMICS_REPORT_SCHEMA = describe_schema({ @@ -243,7 +243,7 @@ "extra_properties": EXTRA_PROPERTIES_SCHEMA }, "required": ["id", "code", "issued"] -}, GENOMICS_REPORT) +}, d.GENOMICS_REPORT) MCODE_LABS_VITAL_SCHEMA = describe_schema({ @@ -260,7 +260,7 @@ "extra_properties": EXTRA_PROPERTIES_SCHEMA }, "required": ["id", "individual", "tumor_marker_code"] -}, LABS_VITAL) +}, d.LABS_VITAL) MCODE_TNM_STAGING_SCHEMA = describe_schema({ @@ -294,7 +294,7 @@ "distant_metastases_category", "cancer_condition" ] -}, TNM_STAGING) +}, d.TNM_STAGING) MCODE_CANCER_CONDITION_SCHEMA = describe_schema({ @@ -325,7 +325,7 @@ "extra_properties": EXTRA_PROPERTIES_SCHEMA }, "required": ["id", "condition_type", "code"] -}, LABS_VITAL) +}, d.LABS_VITAL) MCODE_CANCER_RELATED_PROCEDURE_SCHEMA = describe_schema({ @@ -355,7 +355,7 @@ "extra_properties": EXTRA_PROPERTIES_SCHEMA }, "required": ["id", "procedure_type", "code"] -}, CANCER_RELATED_PROCEDURE) +}, d.CANCER_RELATED_PROCEDURE) MCODE_MEDICATION_STATEMENT_SCHEMA = describe_schema({ @@ -378,7 +378,7 @@ "extra_properties": EXTRA_PROPERTIES_SCHEMA }, "required": ["id", "medication_code"] -}, MEDICATION_STATEMENT) +}, d.MEDICATION_STATEMENT) MCODE_SCHEMA = describe_schema({ @@ -406,4 +406,4 @@ "cancer_disease_status": ONTOLOGY_CLASS, "extra_properties": EXTRA_PROPERTIES_SCHEMA } -}, MCODEPACKET) +}, d.MCODEPACKET) diff --git a/chord_metadata_service/mcode/serializers.py b/chord_metadata_service/mcode/serializers.py index 2584ea951..f3c02a8ed 100644 --- a/chord_metadata_service/mcode/serializers.py +++ b/chord_metadata_service/mcode/serializers.py @@ -1,6 +1,6 @@ from chord_metadata_service.restapi.serializers import GenericSerializer from chord_metadata_service.patients.serializers import IndividualSerializer -from .models import * +from . import models as m __all__ = [ @@ -20,28 +20,28 @@ class GeneticSpecimenSerializer(GenericSerializer): class Meta: - model = GeneticSpecimen + model = m.GeneticSpecimen fields = '__all__' class CancerGeneticVariantSerializer(GenericSerializer): class Meta: - model = CancerGeneticVariant + model = m.CancerGeneticVariant fields = '__all__' class GenomicRegionStudiedSerializer(GenericSerializer): class Meta: - model = GenomicRegionStudied + model = m.GenomicRegionStudied fields = '__all__' class GenomicsReportSerializer(GenericSerializer): class Meta: - model = GenomicsReport + model = m.GenomicsReport fields = '__all__' def to_representation(self, instance): @@ -56,14 +56,14 @@ def to_representation(self, instance): class LabsVitalSerializer(GenericSerializer): class Meta: - model = LabsVital + model = m.LabsVital fields = '__all__' class TNMStagingSerializer(GenericSerializer): class Meta: - model = TNMStaging + model = m.TNMStaging fields = '__all__' @@ -71,21 +71,21 @@ class CancerConditionSerializer(GenericSerializer): tnm_staging = TNMStagingSerializer(source='tnmstaging_set', read_only=True, many=True) class Meta: - model = CancerCondition + model = m.CancerCondition fields = '__all__' class CancerRelatedProcedureSerializer(GenericSerializer): class Meta: - model = CancerRelatedProcedure + model = m.CancerRelatedProcedure fields = '__all__' class MedicationStatementSerializer(GenericSerializer): class Meta: - model = MedicationStatement + model = m.MedicationStatement fields = '__all__' @@ -109,5 +109,5 @@ def to_representation(self, instance): return response class Meta: - model = MCodePacket + model = m.MCodePacket fields = '__all__' diff --git a/chord_metadata_service/mcode/tests/test_models.py b/chord_metadata_service/mcode/tests/test_models.py index 90d3944f1..ede3ac6a0 100644 --- a/chord_metadata_service/mcode/tests/test_models.py +++ b/chord_metadata_service/mcode/tests/test_models.py @@ -1,8 +1,8 @@ import datetime from django.test import TestCase from chord_metadata_service.patients.models import Individual -from ..models import * -from .constants import * +from .. import models as m +from . import constants as c from rest_framework import serializers @@ -10,10 +10,10 @@ class GenomicsReportTest(TestCase): """ Test module for Genomics Report model """ def setUp(self): - self.genomics_report = GenomicsReport.objects.create(**valid_genetic_report()) + self.genomics_report = m.GenomicsReport.objects.create(**c.valid_genetic_report()) def test_genomics_report(self): - genomics_report = GenomicsReport.objects.get(id='genomics_report:01') + genomics_report = m.GenomicsReport.objects.get(id='genomics_report:01') self.assertEqual(genomics_report.code['id'], 'GTR000567625.2') @@ -21,15 +21,15 @@ class LabsVitalTest(TestCase): """ Test module for LabsVital model """ def setUp(self): - self.individual = Individual.objects.create(**VALID_INDIVIDUAL) - self.labs_vital = LabsVital.objects.create(**valid_labs_vital(self.individual)) + self.individual = Individual.objects.create(**c.VALID_INDIVIDUAL) + self.labs_vital = m.LabsVital.objects.create(**c.valid_labs_vital(self.individual)) def test_labs_vital(self): - labs_vital = LabsVital.objects.get(id='labs_vital:01') + labs_vital = m.LabsVital.objects.get(id='labs_vital:01') self.assertIsInstance(labs_vital.tumor_marker_code, dict) def test_validation(self): - invalid_obj = valid_labs_vital(self.individual) + invalid_obj = c.valid_labs_vital(self.individual) invalid_obj["id"] = "labs_vital:02" invalid_obj["tumor_marker_code"] = { "coding": [ @@ -40,7 +40,7 @@ def test_validation(self): } ] } - invalid = LabsVital.objects.create(**invalid_obj) + invalid = m.LabsVital.objects.create(**invalid_obj) with self.assertRaises(serializers.ValidationError): invalid.full_clean() @@ -49,10 +49,10 @@ class CancerConditionTest(TestCase): """ Test module for CancerCondition model """ def setUp(self): - self.cancer_condition = CancerCondition.objects.create(**valid_cancer_condition()) + self.cancer_condition = m.CancerCondition.objects.create(**c.valid_cancer_condition()) def test_cancer_condition(self): - cancer_condition = CancerCondition.objects.get(id='cancer_condition:01') + cancer_condition = m.CancerCondition.objects.get(id='cancer_condition:01') self.assertEqual(cancer_condition.condition_type, 'primary') self.assertIsInstance(cancer_condition.body_site, list) self.assertEqual(cancer_condition.body_site[0]['id'], '442083009') @@ -68,19 +68,19 @@ class TNMStagingTest(TestCase): # TODO URI syntax examples for tests https://tools.ietf.org/html/rfc3986 def setUp(self): - self.cancer_condition = CancerCondition.objects.create(**valid_cancer_condition()) - self.tnm_staging = TNMStaging.objects.create(**invalid_tnm_staging(self.cancer_condition)) + self.cancer_condition = m.CancerCondition.objects.create(**c.valid_cancer_condition()) + self.tnm_staging = m.TNMStaging.objects.create(**c.invalid_tnm_staging(self.cancer_condition)) def test_tnm_staging(self): - tnm_staging = TNMStaging.objects.get(id='tnm_staging:01') + tnm_staging = m.TNMStaging.objects.get(id='tnm_staging:01') self.assertEqual(tnm_staging.tnm_type, 'clinical') # this should fails in validation below self.assertIsInstance(tnm_staging.stage_group['data_value']['coding'], list) def test_validation(self): - invalid_obj = invalid_tnm_staging(self.cancer_condition) + invalid_obj = c.invalid_tnm_staging(self.cancer_condition) invalid_obj["id"] = "tnm_staging:02" - invalid = TNMStaging.objects.create(**invalid_obj) + invalid = m.TNMStaging.objects.create(**invalid_obj) with self.assertRaises(serializers.ValidationError): invalid.full_clean() @@ -89,12 +89,12 @@ class CancerRelatedProcedureTest(TestCase): """ Test module for CancerRelatedProcedure model """ def setUp(self): - self.cancer_related_procedure = CancerRelatedProcedure.objects.create( - **valid_cancer_related_procedure() + self.cancer_related_procedure = m.CancerRelatedProcedure.objects.create( + **c.valid_cancer_related_procedure() ) def test_cancer_related_procedure(self): - cancer_related_procedure = CancerRelatedProcedure.objects.get(id='cancer_related_procedure:01') + cancer_related_procedure = m.CancerRelatedProcedure.objects.get(id='cancer_related_procedure:01') self.assertEqual(cancer_related_procedure.procedure_type, 'radiation') self.assertEqual(cancer_related_procedure.code['id'], '33356009') self.assertIsInstance(cancer_related_procedure.body_site, list) @@ -105,12 +105,12 @@ class MedicationStatementTest(TestCase): """ Test module for MedicationStatement model """ def setUp(self): - self.cancer_related_procedure = MedicationStatement.objects.create( - **valid_medication_statement() + self.cancer_related_procedure = m.MedicationStatement.objects.create( + **c.valid_medication_statement() ) def test_cancer_related_procedure(self): - medication_statement = MedicationStatement.objects.get(id='medication_statement:01') + medication_statement = m.MedicationStatement.objects.get(id='medication_statement:01') self.assertEqual(medication_statement.medication_code['id'], '92052') self.assertIsInstance(medication_statement.termination_reason, list) self.assertEqual(medication_statement.treatment_intent['label'], 'Curative - procedure intent') diff --git a/chord_metadata_service/mcode/validators.py b/chord_metadata_service/mcode/validators.py index 43374ff18..9f1d26b12 100644 --- a/chord_metadata_service/mcode/validators.py +++ b/chord_metadata_service/mcode/validators.py @@ -1,9 +1,9 @@ from chord_metadata_service.restapi.validators import JsonSchemaValidator -from .schemas import * +from . import schemas as s -quantity_validator = JsonSchemaValidator(schema=QUANTITY, formats=['uri']) -tumor_marker_data_value_validator = JsonSchemaValidator(schema=TUMOR_MARKER_DATA_VALUE) -complex_ontology_validator = JsonSchemaValidator(schema=COMPLEX_ONTOLOGY, formats=['uri']) +quantity_validator = JsonSchemaValidator(schema=s.QUANTITY, formats=['uri']) +tumor_marker_data_value_validator = JsonSchemaValidator(schema=s.TUMOR_MARKER_DATA_VALUE) +complex_ontology_validator = JsonSchemaValidator(schema=s.COMPLEX_ONTOLOGY, formats=['uri']) # TODO delete? -time_or_period_validator = JsonSchemaValidator(schema=TIME_OR_PERIOD, formats=['date-time']) +time_or_period_validator = JsonSchemaValidator(schema=s.TIME_OR_PERIOD, formats=['date-time']) diff --git a/chord_metadata_service/mcode/views.py b/chord_metadata_service/mcode/views.py deleted file mode 100644 index 91ea44a21..000000000 --- a/chord_metadata_service/mcode/views.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.shortcuts import render - -# Create your views here. From 3806649603d6fe7f8682cb0327a3a1dc1cce99fa Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Thu, 25 Jun 2020 11:36:46 -0400 Subject: [PATCH 167/190] change mcodepacket and medication statement rel to m2m (migrations) --- chord_metadata_service/mcode/mcode_ingest.py | 23 +++++++++++-------- .../migrations/0011_auto_20200625_1117.py | 22 ++++++++++++++++++ chord_metadata_service/mcode/models.py | 4 ++-- .../mcode/parse_fhir_mcode.py | 12 +++++++--- chord_metadata_service/mcode/schemas.py | 5 +++- chord_metadata_service/mcode/serializers.py | 2 +- examples/mcode_example.json | 6 +++-- 7 files changed, 55 insertions(+), 19 deletions(-) create mode 100644 chord_metadata_service/mcode/migrations/0011_auto_20200625_1117.py diff --git a/chord_metadata_service/mcode/mcode_ingest.py b/chord_metadata_service/mcode/mcode_ingest.py index e62b9bce0..ee1cab1bd 100644 --- a/chord_metadata_service/mcode/mcode_ingest.py +++ b/chord_metadata_service/mcode/mcode_ingest.py @@ -107,16 +107,18 @@ def ingest_mcodepacket(mcodepacket_data, table_id): related_cancer_conditions.append(condition) cancer_related_procedure.reason_reference.set(related_cancer_conditions) - # get and create MedicationStatement + # get and create MedicationStatements + medication_statements = [] if medication_statement_data: - medication_statement, ms_created = m.MedicationStatement.objects.get_or_create( - id=medication_statement_data["id"], - defaults={ - "medication_code": medication_statement_data["medication_code"] - } - ) - _logger_message(ms_created, medication_statement) - new_mcodepacket["medication_statement"] = medication_statement + for ms in medication_statement_data: + medication_statement, ms_created = m.MedicationStatement.objects.get_or_create( + id=ms["id"], + defaults={ + "medication_code": ms["medication_code"] + } + ) + _logger_message(ms_created, medication_statement) + medication_statements.append(medication_statement.id) # get date of death if date_of_death_data: @@ -143,7 +145,6 @@ def ingest_mcodepacket(mcodepacket_data, table_id): id=new_mcodepacket["id"], subject=Individual.objects.get(id=new_mcodepacket["subject"]), genomics_report=new_mcodepacket.get("genomics_report", None), - medication_statement=new_mcodepacket.get("medication_statement", None), date_of_death=new_mcodepacket.get("date_of_death", ""), cancer_disease_status=new_mcodepacket.get("cancer_disease_status", None), table_id=table_id @@ -154,5 +155,7 @@ def ingest_mcodepacket(mcodepacket_data, table_id): mcodepacket.cancer_condition.set(cancer_conditions) if crprocedures: mcodepacket.cancer_related_procedures.set(crprocedures) + if medication_statements: + mcodepacket.medication_statement.set(medication_statements) return mcodepacket diff --git a/chord_metadata_service/mcode/migrations/0011_auto_20200625_1117.py b/chord_metadata_service/mcode/migrations/0011_auto_20200625_1117.py new file mode 100644 index 000000000..050ddab04 --- /dev/null +++ b/chord_metadata_service/mcode/migrations/0011_auto_20200625_1117.py @@ -0,0 +1,22 @@ +# Generated by Django 2.2.13 on 2020-06-25 15:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mcode', '0010_auto_20200618_1825'), + ] + + operations = [ + migrations.RemoveField( + model_name='mcodepacket', + name='medication_statement', + ), + migrations.AddField( + model_name='mcodepacket', + name='medication_statement', + field=models.ManyToManyField(blank=True, help_text='Medication treatment addressed to an Individual.', to='mcode.MedicationStatement'), + ), + ] diff --git a/chord_metadata_service/mcode/models.py b/chord_metadata_service/mcode/models.py index 90b34d9da..bf735b3f1 100644 --- a/chord_metadata_service/mcode/models.py +++ b/chord_metadata_service/mcode/models.py @@ -321,8 +321,8 @@ class MCodePacket(models.Model, IndexableMixin): help_text=rec_help(d.MCODEPACKET, "cancer_condition")) cancer_related_procedures = models.ManyToManyField(CancerRelatedProcedure, blank=True, help_text=rec_help(d.MCODEPACKET, "cancer_related_procedures")) - medication_statement = models.ForeignKey(MedicationStatement, blank=True, null=True, on_delete=models.SET_NULL, - help_text=rec_help(d.MCODEPACKET, "medication_statement")) + medication_statement = models.ManyToManyField(MedicationStatement, blank=True, + help_text=rec_help(d.MCODEPACKET, "medication_statement")) date_of_death = models.CharField(max_length=200, blank=True, help_text=rec_help(d.MCODEPACKET, "date_of_death")) cancer_disease_status = JSONField(blank=True, null=True, validators=[ontology_validator], help_text=rec_help(d.MCODEPACKET, "cancer_disease_status")) diff --git a/chord_metadata_service/mcode/parse_fhir_mcode.py b/chord_metadata_service/mcode/parse_fhir_mcode.py index f25340ed4..6ae832aa7 100644 --- a/chord_metadata_service/mcode/parse_fhir_mcode.py +++ b/chord_metadata_service/mcode/parse_fhir_mcode.py @@ -103,9 +103,10 @@ def procedure_to_crprocedure(resource): cancer_conditions = [cc["reference"].split("uuid:")[-1] for cc in resource["reasonReference"]] cancer_related_procedure["reason_reference"] = cancer_conditions # TODO add laterality - # TODO add performed_period if "performedPeriod" in resource: - cancer_related_procedure["extra_properties"] = resource["performedPeriod"] + cancer_related_procedure["extra_properties"] = { + "performed_period": resource["performedPeriod"] + } return cancer_related_procedure @@ -191,6 +192,8 @@ def parse_bundle(bundle): cancer_related_procedures = [] # all cancer conditions cancer_conditions = [] + # all medication statements + medication_statements = [] for item in bundle["entry"]: resource = item["resource"] # get Patient data @@ -268,7 +271,7 @@ def parse_bundle(bundle): # get Medication Statement if resource["resourceType"] == "MedicationStatement" and "meta" in resource: if p.MCODE_MEDICATION_STATEMENT in resource["meta"]["profile"]: - mcodepacket["medication_statement"] = get_medication_statement(resource) + medication_statements.append(get_medication_statement(resource)) # get Cancer Disease Status if resource["resourceType"] == "Observation" and "meta" in resource: @@ -285,6 +288,9 @@ def parse_bundle(bundle): if cancer_conditions: mcodepacket["cancer_condition"] = cancer_conditions + if medication_statements: + mcodepacket["medication_statement"] = medication_statements + mcodepacket["tumor_marker"] = tumor_markers mcodepacket["cancer_related_procedures"] = cancer_related_procedures diff --git a/chord_metadata_service/mcode/schemas.py b/chord_metadata_service/mcode/schemas.py index 499651212..81101c9b3 100644 --- a/chord_metadata_service/mcode/schemas.py +++ b/chord_metadata_service/mcode/schemas.py @@ -395,7 +395,10 @@ "genomics_report": MCODE_GENOMICS_REPORT_SCHEMA, "cancer_condition": MCODE_CANCER_CONDITION_SCHEMA, "cancer_related_procedures": MCODE_CANCER_RELATED_PROCEDURE_SCHEMA, - "medication_statement": MCODE_MEDICATION_STATEMENT_SCHEMA, + "medication_statement": { + "type": "array", + "items": MCODE_MEDICATION_STATEMENT_SCHEMA + }, "date_of_death": { "type": "string" }, diff --git a/chord_metadata_service/mcode/serializers.py b/chord_metadata_service/mcode/serializers.py index 2584ea951..58b89d62a 100644 --- a/chord_metadata_service/mcode/serializers.py +++ b/chord_metadata_service/mcode/serializers.py @@ -104,7 +104,7 @@ def to_representation(self, instance): response['cancer_related_procedures'] = CancerRelatedProcedureSerializer(instance.cancer_related_procedures, many=True, required=False).data response['medication_statement'] = MedicationStatementSerializer(instance.medication_statement, - required=False).data + many=True, required=False).data # TODO add tumor marker return response diff --git a/examples/mcode_example.json b/examples/mcode_example.json index 1dd889636..c717fd2d7 100644 --- a/examples/mcode_example.json +++ b/examples/mcode_example.json @@ -84,13 +84,15 @@ "id":"http://snomed.info/sct:268910001", "label":"Patient's condition improved" }, - "medication_statement":{ + "medication_statement": [ + { "id":"fd5416ee-78ad-41ea-b847-dcc3e975dec9", "medication_code":{ "id":"http://www.nlm.nih.gov/research/umls/rxnorm:198240", "label":"tamoxifen citrate 10 MG Oral Tablet" } - }, + } + ], "tumor_marker":[ { "id":"9f785640-bdd2-4afe-93e2-3959b8994567", From f59fe288521bdaf8206eda3945f6fb4779b2ffc4 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Mon, 29 Jun 2020 09:49:30 -0400 Subject: [PATCH 168/190] add new simple data model diagram --- .../simple_metadata_service_model_v1.0.png | Bin 0 -> 1200100 bytes docs/modules/introduction.rst | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 docs/_static/simple_metadata_service_model_v1.0.png diff --git a/docs/_static/simple_metadata_service_model_v1.0.png b/docs/_static/simple_metadata_service_model_v1.0.png new file mode 100644 index 0000000000000000000000000000000000000000..13c656531aeb88e1eb476400a72bed3c362c692e GIT binary patch literal 1200100 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 Date: Mon, 29 Jun 2020 10:21:56 -0400 Subject: [PATCH 169/190] update intro --- docs/modules/introduction.rst | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/docs/modules/introduction.rst b/docs/modules/introduction.rst index 5a5bccf00..f6724bfd8 100644 --- a/docs/modules/introduction.rst +++ b/docs/modules/introduction.rst @@ -22,15 +22,27 @@ Architecture The Metadata Service contains several services that share one API. Services depend on each other and are separated based on their scope. -**1. Patients service** handles anonymized individual’s data (e.g. individual id, sex, age or date of birth) +**1. Patients service** handles anonymized individual’s data (e.g. individual id, sex, age or date of birth). - Data model: aggregated profile from GA4GH Phenopackets Individual and FHIR Patient. It contains all fields of Phenopacket Individual and additional fields from FHIR Patient. -**2. Phenopackets service** handles phenotypic and clinical data +**2. Phenopackets service** handles phenotypic and clinical data. - Data model: GA4GH Phenopackets schema. Currently contains only two out of four Phenopackets top elements - Phenopacket and Interpretation. -**3. CHORD service** handles granular metadata about dataset (e.g. description, where the dataset is located, who are the creators of the dataset, licenses applied to the dataset, +**3. mCode service** handles oncology related data. + +- Data model: mCODE data elements. mCODE data elements grouped in a mCodepacket (like Phenopacket) containing patient's cancer related descriptions including genomics data, medication statements and cancer related procedures. + +**4. Experiments service** handles experiment related data. + +- Data model: current data model is derived from IHEC metadata `Experiment specification `_. + +**5. Resources service** handles metadata about ontologies usedf for data annotation. + +- Data model: derived from Phenopacket Resource profile. + +**6. CHORD service** handles granular metadata about dataset (e.g. description, where the dataset is located, who are the creators of the dataset, licenses applied to the dataset, authorization policy, terms of use). The dataset in the current implementation is one or more phenopackets related to each other through their provenance. @@ -40,7 +52,7 @@ The dataset in the current implementation is one or more phenopackets related to - GA4GH DUO is used to capture the terms of use applied to a dataset. -**4. Restapi service** handles all generic functionality shared among other services (e.g. renderers, common serializers, schemas, validators) +**7. Restapi service** handles all generic functionality shared among other services (e.g. renderers, common serializers, schemas, validators) Metadata standards @@ -48,12 +60,15 @@ Metadata standards `Phenopackets schema `_ is used for phenotypic description of patient and/or biosample. +`mCODE data elements `_ is used for oncology related description of patient. + `DATS standard `_ is used for dataset description. `DUO ontology `_ is used for describing terms of use for a dataset. `Phenopackets on FHIR Implementation Guide `_ is used to map Phenopackets elements to `FHIR `_ resources. +`IHEC Metadata Experiment `_ is used for describing an experiment. REST API highlights ------------------- From bc915ad3b1e1eba19f6b011ec52f36faa0864d36 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Mon, 29 Jun 2020 11:50:09 -0400 Subject: [PATCH 170/190] update README.md architecture part add ingestion workflows examples in docs --- README.md | 13 +++++++-- docs/modules/introduction.rst | 54 ++++++++++++++++++++++++++++++++--- 2 files changed, 61 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 766054a0c..97c7b6bb2 100644 --- a/README.md +++ b/README.md @@ -23,10 +23,19 @@ CHORD Metadata Service is a service to store epigenomic metadata. 2. Phenopackets service handles phenotypic and clinical data * Data model: [GA4GH Phenopackets schema](https://github.com/phenopackets/phenopacket-schema) -3. CHORD service handles metadata about dataset, has relation to phenopackets (one dataset can have many phenopackets) +3. mCode service handles patient's oncology related data. + * Data model: [mCODE data elements](https://mcodeinitiative.org/) + +4. Experiments service handles experiment related data. + * Data model: derived from [IHEC Metadata Experiment](https://github.com/IHEC/ihec-ecosystems/blob/master/docs/metadata/2.0/Ihec_metadata_specification.md#experiments) + +5. Resources service handles metadata about ontologies used for data annotation. + * Data model: derived from Phenopackets Resource profile + +6. CHORD service handles metadata about dataset, has relation to phenopackets (one dataset can have many phenopackets) * Data model: [DATS](https://github.com/datatagsuite) + [GA4GH DUO](https://github.com/EBISPOT/DUO) -4. Rest api service handles all generic functionality shared among other services +7. Rest api service handles all generic functionality shared among other services ## REST API highlights diff --git a/docs/modules/introduction.rst b/docs/modules/introduction.rst index f6724bfd8..d7acf62ed 100644 --- a/docs/modules/introduction.rst +++ b/docs/modules/introduction.rst @@ -30,7 +30,7 @@ Services depend on each other and are separated based on their scope. - Data model: GA4GH Phenopackets schema. Currently contains only two out of four Phenopackets top elements - Phenopacket and Interpretation. -**3. mCode service** handles oncology related data. +**3. mCode service** handles patient's oncology related data. - Data model: mCODE data elements. mCODE data elements grouped in a mCodepacket (like Phenopacket) containing patient's cancer related descriptions including genomics data, medication statements and cancer related procedures. @@ -38,7 +38,7 @@ Services depend on each other and are separated based on their scope. - Data model: current data model is derived from IHEC metadata `Experiment specification `_. -**5. Resources service** handles metadata about ontologies usedf for data annotation. +**5. Resources service** handles metadata about ontologies used for data annotation. - Data model: derived from Phenopacket Resource profile. @@ -91,9 +91,12 @@ REST API highlights **Data ingest** -Currently only the data that follow Phenopackets schema can be ingested. +Ingest workflows are implemented for different types of data within the service. Ingest endpoint is :code:`/private/ingest`. -Example of POST request body: + +- Phenopackets data ingest: the data must follow Phenopackets schema in order to be ingested. + +Example of Phenopackets POST request body: .. code-block:: @@ -126,7 +129,50 @@ Example of POST request body: } } +- mCode data ingest: mCODE data elements are based on FHIR datatypes. It's expected that the data is compliant with FHIR Release 4 and provided in FHIR Bundles. + +Only mCode related profiles will be ingested. + +Example of mCode FHIR data POST request body: + +.. code-block:: + + { + "table_id":"table_unique_id", + "workflow_id":"mcode_fhir_json", + "workflow_params":{ + "mcode_fhir_json.json_document":"/path/to/data.json" + }, + "workflow_outputs":{ + "json_document":"/path/to/data.json" + } + } + + +- FHIR data ingest: currently there is no implementation guide from FHIR to Phenopackets. +FHIR data will only be ingested partially. +The ingestion works for the following FHIR resources: Patient, Observation, Condition, Specimen. +It's expected that the data is compliant with FHIR Release 4 and provided in FHIR Bundles. + +.. code-block:: + + { + "table_id": "table_unique_id", + "workflow_id": "fhir_json", + "workflow_params": { + "fhir_json.patients": "/path/to/patients.json", + "fhir_json.observations": "/path/to/observations.json", + "fhir_json.conditions": "/path/to/conditions.json", + "fhir_json.specimens": "/path/to/specimens.json" + }, + "workflow_outputs": { + "patients": "/path/to/patients.json", + "observations": "/path/to/observations.json", + "conditions": "/path/to/conditions.json", + "specimens": "/path/to/specimens.json" + } + } Elasticsearch index (optional) From a4158cbb09c1dc7aae396a78b6dc4cb3be8bcb56 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Mon, 29 Jun 2020 11:52:13 -0400 Subject: [PATCH 171/190] remove old text --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 97c7b6bb2..c2b459905 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,6 @@ Phenopackets model is mapped to [FHIR](https://www.hl7.org/fhir/) using To retrieve data in fhir append `?format=fhir` . * Ingest endpoint: `/private/ingest`. -Example of POST body is in `chord/views_ingest.py` (`METADATA_WORKFLOWS`). ## Install From ad1fa3fe7f78083ecaf57b743bc96a285bbc8d9d Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Mon, 29 Jun 2020 12:48:55 -0400 Subject: [PATCH 172/190] add new models and views to docs --- docs/modules/models.rst | 18 ++++++++++++++++++ docs/modules/views.rst | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/docs/modules/models.rst b/docs/modules/models.rst index 3a06bc47b..dc89aa741 100644 --- a/docs/modules/models.rst +++ b/docs/modules/models.rst @@ -13,6 +13,24 @@ Patients service .. automodule:: chord_metadata_service.patients.models :members: +Mcode service +------------------- + +.. automodule:: chord_metadata_service.mcode.models + :members: + +Experiments service +------------------- + +.. automodule:: chord_metadata_service.experiments.models + :members: + +Resources service +------------------- + +.. automodule:: chord_metadata_service.experiments.models + :members: + CHORD service ------------------- diff --git a/docs/modules/views.rst b/docs/modules/views.rst index 6c564ddf7..74de9630d 100644 --- a/docs/modules/views.rst +++ b/docs/modules/views.rst @@ -13,6 +13,24 @@ Patients service .. automodule:: chord_metadata_service.patients.api_views :members: +Mcode service +------------------- + +.. automodule:: chord_metadata_service.mcode.api_views + :members: + +Experiments service +------------------- + +.. automodule:: chord_metadata_service.experiments.api_views + :members: + +Resources service +------------------- + +.. automodule:: chord_metadata_service.resources.api_views + :members: + CHORD service ------------------- From b02a2a5198585f25446a2dc69293690b5751d46b Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Mon, 29 Jun 2020 13:00:27 -0400 Subject: [PATCH 173/190] attempt to fix failing readthedocs build 1 --- .readthedocs.yml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index 44b0bcde2..024bd2d65 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -15,9 +15,8 @@ formats: - htmlzip # Optionally set the version of Python and requirements required to build your docs -#python: -# version: 3.7 -# install: -# - requirements: requirements.txt -# - requirements: docs/requirements.txt -# - method: pip +python: + version: 3.7 + install: + - requirements: requirements.txt + - method: pip From 7b9611f70ac0d02c879bab15e46879ab0bb864c3 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Mon, 29 Jun 2020 13:03:17 -0400 Subject: [PATCH 174/190] fix 2 --- .readthedocs.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index 024bd2d65..7852b509b 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -18,5 +18,5 @@ formats: python: version: 3.7 install: - - requirements: requirements.txt - - method: pip + - requirements: requirements.txt + - method: pip \ No newline at end of file From f415e3aabd34ddfe9afde6ec42e1374e2510b89e Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Mon, 29 Jun 2020 13:17:37 -0400 Subject: [PATCH 175/190] fix 2 --- .readthedocs.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index 7852b509b..84b019533 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -18,5 +18,5 @@ formats: python: version: 3.7 install: - - requirements: requirements.txt - - method: pip \ No newline at end of file + - requirements: requirements.txt + - method: pip \ No newline at end of file From 379904823ada6778a419c8c272bce28d86e5cc78 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Mon, 29 Jun 2020 13:30:55 -0400 Subject: [PATCH 176/190] try 3 --- .readthedocs.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index 84b019533..ef53f669c 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -15,8 +15,8 @@ formats: - htmlzip # Optionally set the version of Python and requirements required to build your docs -python: - version: 3.7 - install: - - requirements: requirements.txt - - method: pip \ No newline at end of file +#python: +# version: 3.7 +# install: +# - requirements: requirements.txt +# - method: pip \ No newline at end of file From 99b98096c83c0d124e48296c02431b21d3939266 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Mon, 29 Jun 2020 14:56:12 -0400 Subject: [PATCH 177/190] try 4 --- docs/requirements.txt | 76 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 docs/requirements.txt diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 000000000..470d43f41 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,76 @@ +alabaster==0.7.12 +appdirs==1.4.4 +attrs==19.3.0 +Babel==2.8.0 +certifi==2020.4.5.2 +chardet==3.0.4 +chord-lib==0.9.0 +codecov==2.1.7 +colorama==0.4.3 +coreapi==2.3.3 +coreschema==0.0.4 +coverage==5.1 +distlib==0.3.0 +Django==2.2.13 +django-filter==2.2.0 +django-nose==1.4.6 +django-rest-swagger==2.2.0 +djangorestframework==3.11.0 +djangorestframework-camel-case==1.1.2 +docutils==0.16 +elasticsearch==7.6.0 +entrypoints==0.3 +fhirclient==3.2.0 +filelock==3.0.12 +flake8==3.8.3 +idna==2.9 +imagesize==1.2.0 +importlib-metadata==1.6.0 +isodate==0.6.0 +itypes==1.2.0 +Jinja2==2.11.2 +jsonschema==3.2.0 +Markdown==3.2.2 +MarkupSafe==1.1.1 +mccabe==0.6.1 +more-itertools==8.4.0 +nose==1.3.7 +openapi-codec==1.3.2 +packaging==20.4 +pluggy==0.13.1 +psycopg2-binary==2.8.5 +py==1.8.2 +pycodestyle==2.6.0 +pyflakes==2.2.0 +Pygments==2.6.1 +pyparsing==2.4.7 +pyrsistent==0.16.0 +python-dateutil==2.8.1 +pytz==2020.1 +PyYAML==5.3.1 +rdflib==4.2.2 +rdflib-jsonld==0.4.0 +redis==3.5.3 +requests==2.24.0 +rfc3987==1.3.8 +simplejson==3.17.0 +six==1.15.0 +snowballstemmer==2.0.0 +Sphinx==2.4.4 +sphinx-rtd-theme==0.4.3 +sphinxcontrib-applehelp==1.0.2 +sphinxcontrib-devhelp==1.0.2 +sphinxcontrib-htmlhelp==1.0.3 +sphinxcontrib-jsmath==1.0.1 +sphinxcontrib-qthelp==1.0.3 +sphinxcontrib-serializinghtml==1.1.4 +sqlparse==0.3.1 +strict-rfc3339==0.7 +toml==0.10.1 +tox==3.15.2 +uritemplate==3.0.1 +urllib3==1.25.9 +virtualenv==20.0.23 +Werkzeug==1.0.1 +wincertstore==0.2 +zipp==3.1.0 From 7d0d34a1ec21ab7a773adf42e19c45cca2534e4f Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Mon, 29 Jun 2020 14:56:57 -0400 Subject: [PATCH 178/190] try 4 - 2 --- .readthedocs.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index ef53f669c..90ea7c61a 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -15,8 +15,7 @@ formats: - htmlzip # Optionally set the version of Python and requirements required to build your docs -#python: -# version: 3.7 -# install: -# - requirements: requirements.txt -# - method: pip \ No newline at end of file +python: + version: 3.7 + install: + - requirements: docs/requirements.txt \ No newline at end of file From 4df620ee82acbb7f91ff8f694b7485196926fc4e Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Mon, 29 Jun 2020 14:59:36 -0400 Subject: [PATCH 179/190] set main requirements --- .readthedocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index 90ea7c61a..7b78cce75 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -18,4 +18,4 @@ formats: python: version: 3.7 install: - - requirements: docs/requirements.txt \ No newline at end of file + - requirements: requirements.txt \ No newline at end of file From 2c0f383565cebd1f7707c2571b9344b0f7fb4aae Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Mon, 29 Jun 2020 15:01:46 -0400 Subject: [PATCH 180/190] delete docs/requirements.txt --- docs/requirements.txt | 76 ------------------------------------------- 1 file changed, 76 deletions(-) delete mode 100644 docs/requirements.txt diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index 470d43f41..000000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,76 +0,0 @@ -alabaster==0.7.12 -appdirs==1.4.4 -attrs==19.3.0 -Babel==2.8.0 -certifi==2020.4.5.2 -chardet==3.0.4 -chord-lib==0.9.0 -codecov==2.1.7 -colorama==0.4.3 -coreapi==2.3.3 -coreschema==0.0.4 -coverage==5.1 -distlib==0.3.0 -Django==2.2.13 -django-filter==2.2.0 -django-nose==1.4.6 -django-rest-swagger==2.2.0 -djangorestframework==3.11.0 -djangorestframework-camel-case==1.1.2 -docutils==0.16 -elasticsearch==7.6.0 -entrypoints==0.3 -fhirclient==3.2.0 -filelock==3.0.12 -flake8==3.8.3 -idna==2.9 -imagesize==1.2.0 -importlib-metadata==1.6.0 -isodate==0.6.0 -itypes==1.2.0 -Jinja2==2.11.2 -jsonschema==3.2.0 -Markdown==3.2.2 -MarkupSafe==1.1.1 -mccabe==0.6.1 -more-itertools==8.4.0 -nose==1.3.7 -openapi-codec==1.3.2 -packaging==20.4 -pluggy==0.13.1 -psycopg2-binary==2.8.5 -py==1.8.2 -pycodestyle==2.6.0 -pyflakes==2.2.0 -Pygments==2.6.1 -pyparsing==2.4.7 -pyrsistent==0.16.0 -python-dateutil==2.8.1 -pytz==2020.1 -PyYAML==5.3.1 -rdflib==4.2.2 -rdflib-jsonld==0.4.0 -redis==3.5.3 -requests==2.24.0 -rfc3987==1.3.8 -simplejson==3.17.0 -six==1.15.0 -snowballstemmer==2.0.0 -Sphinx==2.4.4 -sphinx-rtd-theme==0.4.3 -sphinxcontrib-applehelp==1.0.2 -sphinxcontrib-devhelp==1.0.2 -sphinxcontrib-htmlhelp==1.0.3 -sphinxcontrib-jsmath==1.0.1 -sphinxcontrib-qthelp==1.0.3 -sphinxcontrib-serializinghtml==1.1.4 -sqlparse==0.3.1 -strict-rfc3339==0.7 -toml==0.10.1 -tox==3.15.2 -uritemplate==3.0.1 -urllib3==1.25.9 -virtualenv==20.0.23 -Werkzeug==1.0.1 -wincertstore==0.2 -zipp==3.1.0 From 771d8c604958798f67c25358653369080c658516 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Mon, 29 Jun 2020 15:53:57 -0400 Subject: [PATCH 181/190] changes to docs and readme --- README.md | 2 +- docs/modules/introduction.rst | 13 +++++++++---- docs/modules/models.rst | 2 +- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index c2b459905..387bc4f13 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ under the BSD 3-clause license. CHORD Metadata Service is a service to store epigenomic metadata. 1. Patients service handles anonymized individual’s data (individual id, sex, age or date of birth) - * Data model: aggregated profile from GA4GH Phenopackets Individual and FHIR Patient + * Data model: aggregated profile from GA4GH Phenopackets Individual, FHIR Patient and mCODE Patient. 2. Phenopackets service handles phenotypic and clinical data * Data model: [GA4GH Phenopackets schema](https://github.com/phenopackets/phenopacket-schema) diff --git a/docs/modules/introduction.rst b/docs/modules/introduction.rst index d7acf62ed..2ec8223f0 100644 --- a/docs/modules/introduction.rst +++ b/docs/modules/introduction.rst @@ -3,6 +3,7 @@ Introduction Metadata service is a service to store phenotypic and clinical metadata about the patient and/or biosample. Data model is partly based on `GA4GH Phenopackets schema `_. +The model is extended to support oncology related metadata and experiments metadata. The simplified data model of the service is below. @@ -24,7 +25,7 @@ Services depend on each other and are separated based on their scope. **1. Patients service** handles anonymized individual’s data (e.g. individual id, sex, age or date of birth). -- Data model: aggregated profile from GA4GH Phenopackets Individual and FHIR Patient. It contains all fields of Phenopacket Individual and additional fields from FHIR Patient. +- Data model: aggregated profile from GA4GH Phenopackets Individual, FHIR Patient and mCODE Patient. It contains all fields of Phenopacket Individual and additional fields from FHIR amd mCODE Patient. **2. Phenopackets service** handles phenotypic and clinical data. @@ -94,7 +95,8 @@ REST API highlights Ingest workflows are implemented for different types of data within the service. Ingest endpoint is :code:`/private/ingest`. -- Phenopackets data ingest: the data must follow Phenopackets schema in order to be ingested. +**1. Phenopackets data ingest** +The data must follow Phenopackets schema in order to be ingested. Example of Phenopackets POST request body: @@ -129,9 +131,11 @@ Example of Phenopackets POST request body: } } -- mCode data ingest: mCODE data elements are based on FHIR datatypes. It's expected that the data is compliant with FHIR Release 4 and provided in FHIR Bundles. +**2. mCode data ingest** +mCODE data elements are based on FHIR datatypes. Only mCode related profiles will be ingested. +It's expected that the data is compliant with FHIR Release 4 and provided in FHIR Bundles. Example of mCode FHIR data POST request body: @@ -149,8 +153,9 @@ Example of mCode FHIR data POST request body: } -- FHIR data ingest: currently there is no implementation guide from FHIR to Phenopackets. +**3. FHIR data ingest** +Currently there is no implementation guide from FHIR to Phenopackets. FHIR data will only be ingested partially. The ingestion works for the following FHIR resources: Patient, Observation, Condition, Specimen. It's expected that the data is compliant with FHIR Release 4 and provided in FHIR Bundles. diff --git a/docs/modules/models.rst b/docs/modules/models.rst index dc89aa741..2171141b4 100644 --- a/docs/modules/models.rst +++ b/docs/modules/models.rst @@ -28,7 +28,7 @@ Experiments service Resources service ------------------- -.. automodule:: chord_metadata_service.experiments.models +.. automodule:: chord_metadata_service.resources.models :members: CHORD service From 07a3015e877598d1c24505a82e216cc1078cc2c5 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Mon, 29 Jun 2020 16:01:58 -0400 Subject: [PATCH 182/190] add new line --- docs/modules/introduction.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/modules/introduction.rst b/docs/modules/introduction.rst index 2ec8223f0..3a758ec07 100644 --- a/docs/modules/introduction.rst +++ b/docs/modules/introduction.rst @@ -96,6 +96,7 @@ Ingest workflows are implemented for different types of data within the service. Ingest endpoint is :code:`/private/ingest`. **1. Phenopackets data ingest** + The data must follow Phenopackets schema in order to be ingested. Example of Phenopackets POST request body: From 7b76f75d2613c7bdb381921f9fd52da42286c454 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Mon, 29 Jun 2020 17:51:22 -0400 Subject: [PATCH 183/190] fix typos --- docs/modules/introduction.rst | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/modules/introduction.rst b/docs/modules/introduction.rst index 3a758ec07..60cf91dc1 100644 --- a/docs/modules/introduction.rst +++ b/docs/modules/introduction.rst @@ -2,8 +2,8 @@ Introduction ============ Metadata service is a service to store phenotypic and clinical metadata about the patient and/or biosample. -Data model is partly based on `GA4GH Phenopackets schema `_. -The model is extended to support oncology related metadata and experiments metadata. +The data model is partly based on `GA4GH Phenopackets schema `_ and +extended to support oncology-related metadata and experiments metadata. The simplified data model of the service is below. @@ -14,7 +14,7 @@ Technical implementation ------------------------ The service is implemented in Python and Django and uses PostgreSQL database to store the data. -Besides PostgreSQL the data can be indexed and queried in Elasticsearch. +Besides PostgreSQL, the data can be indexed and queried in Elasticsearch. Architecture @@ -23,25 +23,25 @@ Architecture The Metadata Service contains several services that share one API. Services depend on each other and are separated based on their scope. -**1. Patients service** handles anonymized individual’s data (e.g. individual id, sex, age or date of birth). +**1. Patients service** handles anonymized individual’s data (e.g. individual id, sex, age, or date of birth). -- Data model: aggregated profile from GA4GH Phenopackets Individual, FHIR Patient and mCODE Patient. It contains all fields of Phenopacket Individual and additional fields from FHIR amd mCODE Patient. +- Data model: aggregated profile from GA4GH Phenopackets Individual, FHIR Patient, and mCODE Patient. It contains all fields of Phenopacket Individual and additional fields from FHIR and mCODE Patient. **2. Phenopackets service** handles phenotypic and clinical data. - Data model: GA4GH Phenopackets schema. Currently contains only two out of four Phenopackets top elements - Phenopacket and Interpretation. -**3. mCode service** handles patient's oncology related data. +**3. mCode service** handles patient's oncology-related data. -- Data model: mCODE data elements. mCODE data elements grouped in a mCodepacket (like Phenopacket) containing patient's cancer related descriptions including genomics data, medication statements and cancer related procedures. +- Data model: mCODE data elements. mCODE data elements grouped in a mCodepacket (like Phenopacket) containing patient's cancer-related descriptions including genomics data, medication statements, and cancer-related procedures. **4. Experiments service** handles experiment related data. -- Data model: current data model is derived from IHEC metadata `Experiment specification `_. +- Data model: derived from IHEC metadata `Experiment specification `_. **5. Resources service** handles metadata about ontologies used for data annotation. -- Data model: derived from Phenopacket Resource profile. +- Data model: derived from the Phenopackets schema Resource profile. **6. CHORD service** handles granular metadata about dataset (e.g. description, where the dataset is located, who are the creators of the dataset, licenses applied to the dataset, authorization policy, terms of use). @@ -61,7 +61,7 @@ Metadata standards `Phenopackets schema `_ is used for phenotypic description of patient and/or biosample. -`mCODE data elements `_ is used for oncology related description of patient. +`mCODE data elements `_ are used for oncology-related description of patient. `DATS standard `_ is used for dataset description. @@ -84,7 +84,7 @@ REST API highlights - Other available renderers: - - Currently the following classes can be retrieved in FHIR format by appending :code:`?format=fhir`: Phenopacket, Individual, Biosample, PhenotypicFeature, HtsFile, Gene, Variant, Disease, Procedure. + - Currently, the following classes can be retrieved in FHIR format by appending :code:`?format=fhir`: Phenopacket, Individual, Biosample, PhenotypicFeature, HtsFile, Gene, Variant, Disease, Procedure. - JSON-LD context to schema.org provided for the Dataset class in order to allow for a Google dataset search for Open Access Data: append :code:`?format=json-ld` when querying dataset endpoint. @@ -156,8 +156,8 @@ Example of mCode FHIR data POST request body: **3. FHIR data ingest** -Currently there is no implementation guide from FHIR to Phenopackets. -FHIR data will only be ingested partially. +At the moment there is no implementation guide from FHIR to Phenopackets. +FHIR data will only be ingested partially where it's possible to establish mapping between FHIR resource and Phenopackets element. The ingestion works for the following FHIR resources: Patient, Observation, Condition, Specimen. It's expected that the data is compliant with FHIR Release 4 and provided in FHIR Bundles. From b83964591860c9af511ae2fc45c504b11de88421 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 6 Jul 2020 10:47:23 -0400 Subject: [PATCH 184/190] Update dependencies --- requirements.txt | 28 ++++++++++++++-------------- setup.py | 10 +++++----- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/requirements.txt b/requirements.txt index 470d43f41..bf1e9ea24 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,30 +2,30 @@ alabaster==0.7.12 appdirs==1.4.4 attrs==19.3.0 Babel==2.8.0 -certifi==2020.4.5.2 +certifi==2020.6.20 chardet==3.0.4 chord-lib==0.9.0 codecov==2.1.7 colorama==0.4.3 coreapi==2.3.3 coreschema==0.0.4 -coverage==5.1 -distlib==0.3.0 -Django==2.2.13 -django-filter==2.2.0 +coverage==5.2 +distlib==0.3.1 +Django==2.2.14 +django-filter==2.3.0 django-nose==1.4.6 django-rest-swagger==2.2.0 djangorestframework==3.11.0 -djangorestframework-camel-case==1.1.2 +djangorestframework-camel-case==1.2.0 docutils==0.16 -elasticsearch==7.6.0 +elasticsearch==7.8.0 entrypoints==0.3 fhirclient==3.2.0 filelock==3.0.12 flake8==3.8.3 -idna==2.9 +idna==2.10 imagesize==1.2.0 -importlib-metadata==1.6.0 +importlib-metadata==1.7.0 isodate==0.6.0 itypes==1.2.0 Jinja2==2.11.2 @@ -39,7 +39,7 @@ openapi-codec==1.3.2 packaging==20.4 pluggy==0.13.1 psycopg2-binary==2.8.5 -py==1.8.2 +py==1.9.0 pycodestyle==2.6.0 pyflakes==2.2.0 Pygments==2.6.1 @@ -48,8 +48,8 @@ pyrsistent==0.16.0 python-dateutil==2.8.1 pytz==2020.1 PyYAML==5.3.1 -rdflib==4.2.2 -rdflib-jsonld==0.4.0 +rdflib==5.0.0 +rdflib-jsonld==0.5.0 redis==3.5.3 requests==2.24.0 rfc3987==1.3.8 @@ -67,10 +67,10 @@ sphinxcontrib-serializinghtml==1.1.4 sqlparse==0.3.1 strict-rfc3339==0.7 toml==0.10.1 -tox==3.15.2 +tox==3.16.1 uritemplate==3.0.1 urllib3==1.25.9 -virtualenv==20.0.23 +virtualenv==20.0.25 Werkzeug==1.0.1 wincertstore==0.2 zipp==3.1.0 diff --git a/setup.py b/setup.py index e3f363dee..41d421971 100644 --- a/setup.py +++ b/setup.py @@ -17,13 +17,13 @@ python_requires=">=3.6", install_requires=[ "chord_lib[django]==0.9.0", - "Django>=2.2.13,<3.0", - "django-filter>=2.2,<3.0", + "Django>=2.2.14,<3.0", + "django-filter>=2.3,<3.0", "django-nose>=1.4,<2.0", "djangorestframework>=3.11,<3.12", - "djangorestframework-camel-case>=1.1,<2.0", + "djangorestframework-camel-case>=1.2.0,<2.0", "django-rest-swagger==2.2.0", - "elasticsearch==7.1.0", + "elasticsearch==7.8.0", "fhirclient>=3.2,<4.0", "jsonschema>=3.2,<4.0", "psycopg2-binary>=2.8,<3.0", @@ -32,7 +32,7 @@ "strict-rfc3339==0.7", "rdflib==4.2.2", "rdflib-jsonld==0.4.0", - "requests>=2.23,<3.0", + "requests>=2.24.0,<3.0", "rfc3987==1.3.8", "uritemplate>=3.0,<4.0", ], From b51f6c03e6dc6371e2e3509e22d4246833eca6b0 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 6 Jul 2020 10:50:15 -0400 Subject: [PATCH 185/190] Add some missing mcode packet-related stuff --- chord_metadata_service/chord/ingest.py | 2 +- .../chord/tests/test_api_search.py | 7 ++++--- chord_metadata_service/chord/views_search.py | 21 ++++++++++++++----- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/chord_metadata_service/chord/ingest.py b/chord_metadata_service/chord/ingest.py index a42579d86..8d0d7205c 100644 --- a/chord_metadata_service/chord/ingest.py +++ b/chord_metadata_service/chord/ingest.py @@ -183,7 +183,7 @@ def create_phenotypic_feature(pf): negated=pf.get("negated", False), severity=pf.get("severity"), modifier=pf.get("modifier", []), # TODO: Validate ontology term in schema... - onset=pf.get("onset", None), + onset=pf.get("onset"), evidence=pf.get("evidence") # TODO: Separate class? ) diff --git a/chord_metadata_service/chord/tests/test_api_search.py b/chord_metadata_service/chord/tests/test_api_search.py index c9b9cfc2f..41fce4f2b 100644 --- a/chord_metadata_service/chord/tests/test_api_search.py +++ b/chord_metadata_service/chord/tests/test_api_search.py @@ -27,7 +27,7 @@ TEST_FHIR_SEARCH_QUERY, ) from ..models import Project, Dataset, TableOwnership, Table -from ..data_types import DATA_TYPE_EXPERIMENT, DATA_TYPE_PHENOPACKET, DATA_TYPES +from ..data_types import DATA_TYPE_EXPERIMENT, DATA_TYPE_MCODEPACKET, DATA_TYPE_PHENOPACKET, DATA_TYPES class DataTypeTest(APITestCase): @@ -35,9 +35,10 @@ def test_data_type_list(self): r = self.client.get(reverse("data-type-list")) self.assertEqual(r.status_code, status.HTTP_200_OK) c = r.json() - self.assertEqual(len(c), 2) - ids = (c[0]["id"], c[1]["id"]) + self.assertEqual(len(c), 3) + ids = (c[0]["id"], c[1]["id"], c[2]["id"]) self.assertIn(DATA_TYPE_EXPERIMENT, ids) + self.assertIn(DATA_TYPE_MCODEPACKET, ids) self.assertIn(DATA_TYPE_PHENOPACKET, ids) def test_data_type_detail(self): diff --git a/chord_metadata_service/chord/views_search.py b/chord_metadata_service/chord/views_search.py index 2b1cb8365..0e6167b28 100644 --- a/chord_metadata_service/chord/views_search.py +++ b/chord_metadata_service/chord/views_search.py @@ -16,6 +16,7 @@ from chord_lib.search import build_search_response, postgres from chord_metadata_service.experiments.models import Experiment from chord_metadata_service.experiments.serializers import ExperimentSerializer +from chord_metadata_service.mcode.models import MCodePacket from chord_metadata_service.metadata.elastic import es from chord_metadata_service.metadata.settings import DEBUG, CHORD_SERVICE_ARTIFACT, CHORD_SERVICE_ID from chord_metadata_service.patients.models import Individual @@ -23,7 +24,7 @@ from chord_metadata_service.phenopackets.models import Phenopacket from chord_metadata_service.phenopackets.serializers import PhenopacketSerializer -from .data_types import DATA_TYPE_EXPERIMENT, DATA_TYPE_PHENOPACKET, DATA_TYPES +from .data_types import DATA_TYPE_EXPERIMENT, DATA_TYPE_MCODEPACKET, DATA_TYPE_PHENOPACKET, DATA_TYPES from .models import Dataset, TableOwnership, Table from .permissions import ReadOnly, OverrideOrSuperUserOnly @@ -31,10 +32,10 @@ @api_view(["GET"]) @permission_classes([AllowAny]) def data_type_list(_request): - return Response([ - {"id": DATA_TYPE_EXPERIMENT, "schema": DATA_TYPES[DATA_TYPE_EXPERIMENT]["schema"]}, - {"id": DATA_TYPE_PHENOPACKET, "schema": DATA_TYPES[DATA_TYPE_PHENOPACKET]["schema"]}, - ]) + return Response(sorted( + ({"id": k, "schema": dt["schema"]} for k, dt in DATA_TYPES.items()), + key=lambda dt: dt["id"] + )) @api_view(["GET"]) @@ -151,6 +152,15 @@ def experiment_table_summary(table): }) +def mcodepacket_table_summary(table): + mcodepackets = MCodePacket.objects.filter(table=table) # TODO + + return Response({ + "count": mcodepackets.count(), + "data_type_specific": {}, # TODO + }) + + def phenopacket_table_summary(table): phenopackets = Phenopacket.objects.filter(table=table) # TODO @@ -220,6 +230,7 @@ def count_individual(ind): SUMMARY_HANDLERS: Dict[str, Callable[[Any], Response]] = { DATA_TYPE_EXPERIMENT: experiment_table_summary, + DATA_TYPE_MCODEPACKET: mcodepacket_table_summary, DATA_TYPE_PHENOPACKET: phenopacket_table_summary, } From 2591d156d409badb7542f157f2241aa882a2bd76 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Mon, 6 Jul 2020 11:10:39 -0400 Subject: [PATCH 186/190] delete all migrations and regenerate new migrations --- .../chord/migrations/0001_initial.py | 70 ---- .../chord/migrations/0001_v1_0_0.py | 90 +++++ .../migrations/0002_auto_20191210_2104.py | 32 -- .../migrations/0003_dataset_field_links.py | 20 -- .../migrations/0004_auto_20200123_2126.py | 18 - .../migrations/0005_auto_20200123_2144.py | 18 - .../migrations/0006_dataset_contact_info.py | 18 - .../migrations/0007_auto_20200129_1537.py | 33 -- .../chord/migrations/0008_dataset_version.py | 18 - .../migrations/0009_auto_20200218_1615.py | 19 - .../migrations/0010_auto_20200309_1945.py | 19 - .../migrations/0011_auto_20200515_1657.py | 69 ---- .../migrations/0012_auto_20200515_1714.py | 84 ----- .../chord/migrations/0013_table.py | 24 -- .../migrations/0014_remove_table_data_type.py | 17 - .../chord/migrations/0015_table_data_type.py | 19 - .../migrations/0016_auto_20200519_2100.py | 18 - .../0017_dataset_additional_resources.py | 19 - .../migrations/0018_auto_20200601_1708.py | 21 -- .../experiments/migrations/0001_initial.py | 33 -- .../experiments/migrations/0001_v1_0_0.py | 38 ++ .../migrations/0002_auto_20200327_1728.py | 40 --- .../migrations/0003_auto_20200331_1841.py | 59 --- .../migrations/0004_auto_20200401_1445.py | 25 -- .../migrations/0005_auto_20200513_1401.py | 36 -- .../migrations/0006_auto_20200514_1541.py | 46 --- .../migrations/0007_auto_20200519_1538.py | 52 --- .../migrations/0008_auto_20200601_1438.py | 31 -- .../mcode/migrations/0001_initial.py | 163 --------- .../mcode/migrations/0001_v1_0_0.py | 215 +++++++++++ .../migrations/0002_auto_20200401_1008.py | 159 --------- .../migrations/0003_auto_20200513_1401.py | 75 ---- .../migrations/0004_auto_20200514_1541.py | 130 ------- .../migrations/0005_auto_20200519_1538.py | 130 ------- .../migrations/0006_auto_20200610_2248.py | 227 ------------ .../migrations/0007_auto_20200610_2254.py | 19 - .../migrations/0008_auto_20200610_2320.py | 20 -- .../migrations/0009_auto_20200618_1318.py | 27 -- .../migrations/0010_auto_20200618_1825.py | 28 -- .../migrations/0011_auto_20200625_1117.py | 22 -- .../patients/migrations/0001_initial.py | 35 -- .../patients/migrations/0001_v1_0_0.py | 41 +++ .../migrations/0002_remove_individual_age.py | 17 - .../migrations/0003_individual_age.py | 19 - .../migrations/0004_auto_20200129_1537.py | 23 -- .../migrations/0005_auto_20200311_1610.py | 29 -- .../migrations/0006_auto_20200401_1504.py | 40 --- .../migrations/0007_auto_20200430_1444.py | 20 -- .../migrations/0008_auto_20200513_1401.py | 20 -- .../migrations/0009_auto_20200514_1541.py | 35 -- .../migrations/0010_auto_20200519_1538.py | 35 -- .../phenopackets/migrations/0001_initial.py | 234 ------------ .../phenopackets/migrations/0001_v1_0_0.py | 224 ++++++++++++ .../migrations/0002_auto_20200121_1659.py | 337 ------------------ .../migrations/0003_auto_20200121_1956.py | 74 ---- .../migrations/0004_auto_20200129_1537.py | 83 ----- .../migrations/0005_auto_20200428_1633.py | 126 ------- .../migrations/0006_auto_20200430_1444.py | 25 -- .../migrations/0007_auto_20200513_1401.py | 41 --- .../migrations/0008_auto_20200514_1541.py | 101 ------ .../migrations/0009_auto_20200515_1613.py | 37 -- .../migrations/0010_auto_20200515_1645.py | 45 --- .../migrations/0011_auto_20200519_1538.py | 112 ------ .../migrations/0012_auto_20200525_2116.py | 26 -- .../{0001_initial.py => 0001_v1_0_0.py} | 2 +- 65 files changed, 609 insertions(+), 3373 deletions(-) delete mode 100644 chord_metadata_service/chord/migrations/0001_initial.py create mode 100644 chord_metadata_service/chord/migrations/0001_v1_0_0.py delete mode 100644 chord_metadata_service/chord/migrations/0002_auto_20191210_2104.py delete mode 100644 chord_metadata_service/chord/migrations/0003_dataset_field_links.py delete mode 100644 chord_metadata_service/chord/migrations/0004_auto_20200123_2126.py delete mode 100644 chord_metadata_service/chord/migrations/0005_auto_20200123_2144.py delete mode 100644 chord_metadata_service/chord/migrations/0006_dataset_contact_info.py delete mode 100644 chord_metadata_service/chord/migrations/0007_auto_20200129_1537.py delete mode 100644 chord_metadata_service/chord/migrations/0008_dataset_version.py delete mode 100644 chord_metadata_service/chord/migrations/0009_auto_20200218_1615.py delete mode 100644 chord_metadata_service/chord/migrations/0010_auto_20200309_1945.py delete mode 100644 chord_metadata_service/chord/migrations/0011_auto_20200515_1657.py delete mode 100644 chord_metadata_service/chord/migrations/0012_auto_20200515_1714.py delete mode 100644 chord_metadata_service/chord/migrations/0013_table.py delete mode 100644 chord_metadata_service/chord/migrations/0014_remove_table_data_type.py delete mode 100644 chord_metadata_service/chord/migrations/0015_table_data_type.py delete mode 100644 chord_metadata_service/chord/migrations/0016_auto_20200519_2100.py delete mode 100644 chord_metadata_service/chord/migrations/0017_dataset_additional_resources.py delete mode 100644 chord_metadata_service/chord/migrations/0018_auto_20200601_1708.py delete mode 100644 chord_metadata_service/experiments/migrations/0001_initial.py create mode 100644 chord_metadata_service/experiments/migrations/0001_v1_0_0.py delete mode 100644 chord_metadata_service/experiments/migrations/0002_auto_20200327_1728.py delete mode 100644 chord_metadata_service/experiments/migrations/0003_auto_20200331_1841.py delete mode 100644 chord_metadata_service/experiments/migrations/0004_auto_20200401_1445.py delete mode 100644 chord_metadata_service/experiments/migrations/0005_auto_20200513_1401.py delete mode 100644 chord_metadata_service/experiments/migrations/0006_auto_20200514_1541.py delete mode 100644 chord_metadata_service/experiments/migrations/0007_auto_20200519_1538.py delete mode 100644 chord_metadata_service/experiments/migrations/0008_auto_20200601_1438.py delete mode 100644 chord_metadata_service/mcode/migrations/0001_initial.py create mode 100644 chord_metadata_service/mcode/migrations/0001_v1_0_0.py delete mode 100644 chord_metadata_service/mcode/migrations/0002_auto_20200401_1008.py delete mode 100644 chord_metadata_service/mcode/migrations/0003_auto_20200513_1401.py delete mode 100644 chord_metadata_service/mcode/migrations/0004_auto_20200514_1541.py delete mode 100644 chord_metadata_service/mcode/migrations/0005_auto_20200519_1538.py delete mode 100644 chord_metadata_service/mcode/migrations/0006_auto_20200610_2248.py delete mode 100644 chord_metadata_service/mcode/migrations/0007_auto_20200610_2254.py delete mode 100644 chord_metadata_service/mcode/migrations/0008_auto_20200610_2320.py delete mode 100644 chord_metadata_service/mcode/migrations/0009_auto_20200618_1318.py delete mode 100644 chord_metadata_service/mcode/migrations/0010_auto_20200618_1825.py delete mode 100644 chord_metadata_service/mcode/migrations/0011_auto_20200625_1117.py delete mode 100644 chord_metadata_service/patients/migrations/0001_initial.py create mode 100644 chord_metadata_service/patients/migrations/0001_v1_0_0.py delete mode 100644 chord_metadata_service/patients/migrations/0002_remove_individual_age.py delete mode 100644 chord_metadata_service/patients/migrations/0003_individual_age.py delete mode 100644 chord_metadata_service/patients/migrations/0004_auto_20200129_1537.py delete mode 100644 chord_metadata_service/patients/migrations/0005_auto_20200311_1610.py delete mode 100644 chord_metadata_service/patients/migrations/0006_auto_20200401_1504.py delete mode 100644 chord_metadata_service/patients/migrations/0007_auto_20200430_1444.py delete mode 100644 chord_metadata_service/patients/migrations/0008_auto_20200513_1401.py delete mode 100644 chord_metadata_service/patients/migrations/0009_auto_20200514_1541.py delete mode 100644 chord_metadata_service/patients/migrations/0010_auto_20200519_1538.py delete mode 100644 chord_metadata_service/phenopackets/migrations/0001_initial.py create mode 100644 chord_metadata_service/phenopackets/migrations/0001_v1_0_0.py delete mode 100644 chord_metadata_service/phenopackets/migrations/0002_auto_20200121_1659.py delete mode 100644 chord_metadata_service/phenopackets/migrations/0003_auto_20200121_1956.py delete mode 100644 chord_metadata_service/phenopackets/migrations/0004_auto_20200129_1537.py delete mode 100644 chord_metadata_service/phenopackets/migrations/0005_auto_20200428_1633.py delete mode 100644 chord_metadata_service/phenopackets/migrations/0006_auto_20200430_1444.py delete mode 100644 chord_metadata_service/phenopackets/migrations/0007_auto_20200513_1401.py delete mode 100644 chord_metadata_service/phenopackets/migrations/0008_auto_20200514_1541.py delete mode 100644 chord_metadata_service/phenopackets/migrations/0009_auto_20200515_1613.py delete mode 100644 chord_metadata_service/phenopackets/migrations/0010_auto_20200515_1645.py delete mode 100644 chord_metadata_service/phenopackets/migrations/0011_auto_20200519_1538.py delete mode 100644 chord_metadata_service/phenopackets/migrations/0012_auto_20200525_2116.py rename chord_metadata_service/resources/migrations/{0001_initial.py => 0001_v1_0_0.py} (97%) diff --git a/chord_metadata_service/chord/migrations/0001_initial.py b/chord_metadata_service/chord/migrations/0001_initial.py deleted file mode 100644 index fadb78c94..000000000 --- a/chord_metadata_service/chord/migrations/0001_initial.py +++ /dev/null @@ -1,70 +0,0 @@ -# Generated by Django 2.2.8 on 2019-12-10 21:04 - -import django.contrib.postgres.fields -import django.contrib.postgres.fields.jsonb -from django.db import migrations, models -import django.db.models.deletion -import uuid - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='Dataset', - fields=[ - ('identifier', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('title', models.CharField(max_length=200, unique=True)), - ('description', models.TextField(blank=True)), - ('data_use', django.contrib.postgres.fields.jsonb.JSONField()), - ('alternate_identifiers', django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True), blank=True, help_text='Alternate identifiers for the dataset.', null=True, size=None)), - ('related_identifiers', django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True), blank=True, help_text='Related identifiers for the dataset.', null=True, size=None)), - ('dates', django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True), blank=True, help_text='Relevant dates for the datasets, a date must be added, e.g. creation date or last modification date should be added.', null=True, size=None)), - ('stored_in', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The data repository hosting the dataset.', null=True)), - ('spatial_coverage', django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True), blank=True, help_text='The geographical extension and span covered by the dataset and its measured dimensions/variables.', null=True, size=None)), - ('types', django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True), blank=True, help_text='A term, ideally from a controlled terminology, identifying the dataset type or nature of the data, placing it in a typology.', null=True, size=None)), - ('availability', models.CharField(blank=True, help_text='A qualifier indicating the different types of availability for a dataset (available, unavailable, embargoed, available with restriction, information not available).', max_length=200)), - ('refinement', models.CharField(blank=True, help_text='A qualifier to describe the level of data processing of the dataset and its distributions.', max_length=200)), - ('aggregation', models.CharField(blank=True, help_text="A qualifier indicating if the entity represents an 'instance of dataset' or a 'collection of datasets'.", max_length=200)), - ('privacy', models.CharField(blank=True, help_text='A qualifier to describe the data protection applied to the dataset. This is relevant for clinical data.', max_length=200)), - ('distributions', django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True), blank=True, help_text='The distribution(s) by which datasets are made available (for example: mySQL dump).', null=True, size=None)), - ('dimensions', django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True), blank=True, help_text='The different dimensions (granular components) making up a dataset.', null=True, size=None)), - ('primary_publications', django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True), blank=True, help_text='The primary publication(s) associated with the dataset, usually describing how the dataset was produced.', null=True, size=None)), - ('citations', django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True), blank=True, help_text='The publication(s) that cite this dataset.', null=True, size=None)), - ('citation_count', models.IntegerField(blank=True, help_text='The number of publications that cite this dataset (enumerated in the citations property).', null=True)), - ('produced_by', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='A study process which generated a given dataset, if any.', null=True)), - ('creators', django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True), blank=True, help_text='The person(s) or organization(s) which contributed to the creation of the dataset.', null=True, size=None)), - ('licenses', django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True), blank=True, help_text='The terms of use of the dataset.', null=True, size=None)), - ('acknowledges', django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True), blank=True, help_text='The grant(s) which funded and supported the work reported by the dataset.', null=True, size=None)), - ('keywords', django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True), blank=True, help_text='Tags associated with the dataset, which will help in its discovery.', null=True, size=None)), - ('extra_properties', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that do not fit in the previous specified attributes.', null=True)), - ('created', models.DateTimeField(auto_now=True)), - ('updated', models.DateTimeField(auto_now_add=True)), - ], - ), - migrations.CreateModel( - name='Project', - fields=[ - ('identifier', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('title', models.CharField(max_length=200, unique=True)), - ('description', models.TextField(blank=True)), - ('created', models.DateTimeField(auto_now=True)), - ('updated', models.DateTimeField(auto_now_add=True)), - ], - ), - migrations.CreateModel( - name='TableOwnership', - fields=[ - ('table_id', models.CharField(max_length=200, primary_key=True, serialize=False)), - ('service_id', models.UUIDField()), - ('service_artifact', models.CharField(default='', max_length=200)), - ('data_type', models.CharField(max_length=200)), - ('dataset', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='table_ownerships', to='chord.Dataset')), - ], - ), - ] diff --git a/chord_metadata_service/chord/migrations/0001_v1_0_0.py b/chord_metadata_service/chord/migrations/0001_v1_0_0.py new file mode 100644 index 000000000..c416d9aac --- /dev/null +++ b/chord_metadata_service/chord/migrations/0001_v1_0_0.py @@ -0,0 +1,90 @@ +# Generated by Django 2.2.13 on 2020-07-06 14:55 + +import chord_metadata_service.chord.models +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('resources', '0001_v1_0_0'), + ] + + operations = [ + migrations.CreateModel( + name='Dataset', + fields=[ + ('identifier', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('title', models.CharField(max_length=200, unique=True)), + ('description', models.TextField(blank=True)), + ('contact_info', models.TextField(blank=True)), + ('data_use', django.contrib.postgres.fields.jsonb.JSONField()), + ('linked_field_sets', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, help_text='Data type fields which are linked together.')), + ('alternate_identifiers', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, help_text='Alternate identifiers for the dataset.')), + ('related_identifiers', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, help_text='Related identifiers for the dataset.')), + ('dates', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, help_text='Relevant dates for the datasets, a date must be added, e.g. creation date or last modification date should be added.')), + ('stored_in', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The data repository hosting the dataset.', null=True)), + ('spatial_coverage', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, help_text='The geographical extension and span covered by the dataset and its measured dimensions/variables.')), + ('types', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, help_text='A term, ideally from a controlled terminology, identifying the dataset type or nature of the data, placing it in a typology.')), + ('availability', models.CharField(blank=True, help_text='A qualifier indicating the different types of availability for a dataset (available, unavailable, embargoed, available with restriction, information not available).', max_length=200)), + ('refinement', models.CharField(blank=True, help_text='A qualifier to describe the level of data processing of the dataset and its distributions.', max_length=200)), + ('aggregation', models.CharField(blank=True, help_text="A qualifier indicating if the entity represents an 'instance of dataset' or a 'collection of datasets'.", max_length=200)), + ('privacy', models.CharField(blank=True, help_text='A qualifier to describe the data protection applied to the dataset. This is relevant for clinical data.', max_length=200)), + ('distributions', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, help_text='The distribution(s) by which datasets are made available (for example: mySQL dump).')), + ('dimensions', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, help_text='The different dimensions (granular components) making up a dataset.')), + ('primary_publications', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, help_text='The primary publication(s) associated with the dataset, usually describing how the dataset was produced.')), + ('citations', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, help_text='The publication(s) that cite this dataset.')), + ('citation_count', models.IntegerField(blank=True, help_text='The number of publications that cite this dataset (enumerated in the citations property).', null=True)), + ('produced_by', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='A study process which generated a given dataset, if any.', null=True)), + ('creators', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, help_text='The person(s) or organization(s) which contributed to the creation of the dataset.')), + ('licenses', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, help_text='The terms of use of the dataset.')), + ('acknowledges', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, help_text='The grant(s) which funded and supported the work reported by the dataset.')), + ('keywords', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, help_text='Tags associated with the dataset, which will help in its discovery.')), + ('version', models.CharField(blank=True, default=chord_metadata_service.chord.models.version_default, help_text='A release point for the dataset when applicable.', max_length=200)), + ('extra_properties', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that do not fit in the previous specified attributes.', null=True)), + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ('additional_resources', models.ManyToManyField(blank=True, help_text="Any resource objects linked to this dataset that aren't specified by a phenopacket in the dataset.", to='resources.Resource')), + ('has_part', models.ManyToManyField(blank=True, help_text="A Dataset that is a subset of this Dataset; Datasets declaring the 'hasPart' relationship are considered a collection of Datasets, the aggregation criteria could be included in the 'description' field.", related_name='_dataset_has_part_+', to='chord.Dataset')), + ], + ), + migrations.CreateModel( + name='Project', + fields=[ + ('identifier', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('title', models.CharField(max_length=200, unique=True)), + ('description', models.TextField(blank=True)), + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ], + ), + migrations.CreateModel( + name='TableOwnership', + fields=[ + ('table_id', models.CharField(max_length=200, primary_key=True, serialize=False)), + ('service_id', models.CharField(max_length=200)), + ('service_artifact', models.CharField(default='', max_length=200)), + ('dataset', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='table_ownership', to='chord.Dataset')), + ], + ), + migrations.CreateModel( + name='Table', + fields=[ + ('ownership_record', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='chord.TableOwnership')), + ('name', models.CharField(max_length=200, unique=True)), + ('data_type', models.CharField(choices=[('experiment', 'experiment'), ('phenopacket', 'phenopacket')], max_length=30)), + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ], + ), + migrations.AddField( + model_name='dataset', + name='project', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='datasets', to='chord.Project'), + ), + ] diff --git a/chord_metadata_service/chord/migrations/0002_auto_20191210_2104.py b/chord_metadata_service/chord/migrations/0002_auto_20191210_2104.py deleted file mode 100644 index 0476f6649..000000000 --- a/chord_metadata_service/chord/migrations/0002_auto_20191210_2104.py +++ /dev/null @@ -1,32 +0,0 @@ -# Generated by Django 2.2.8 on 2019-12-10 21:04 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('phenopackets', '0001_initial'), - ('chord', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='tableownership', - name='sample', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='phenopackets.Biosample'), - ), - migrations.AddField( - model_name='dataset', - name='has_part', - field=models.ManyToManyField(blank=True, help_text="A Dataset that is a subset of this Dataset; Datasets declaring the 'hasPart' relationship are considered a collection of Datasets, the aggregation criteria could be included in the 'description' field.", related_name='_dataset_has_part_+', to='chord.Dataset'), - ), - migrations.AddField( - model_name='dataset', - name='project', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='datasets', to='chord.Project'), - ), - ] diff --git a/chord_metadata_service/chord/migrations/0003_dataset_field_links.py b/chord_metadata_service/chord/migrations/0003_dataset_field_links.py deleted file mode 100644 index c1812a844..000000000 --- a/chord_metadata_service/chord/migrations/0003_dataset_field_links.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 2.2.9 on 2020-01-22 20:38 - -import django.contrib.postgres.fields -import django.contrib.postgres.fields.jsonb -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('chord', '0002_auto_20191210_2104'), - ] - - operations = [ - migrations.AddField( - model_name='dataset', - name='field_links', - field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(), blank=True, default=list, help_text='Data type fields which are linked together.', size=None), - ), - ] diff --git a/chord_metadata_service/chord/migrations/0004_auto_20200123_2126.py b/chord_metadata_service/chord/migrations/0004_auto_20200123_2126.py deleted file mode 100644 index c65aa67b6..000000000 --- a/chord_metadata_service/chord/migrations/0004_auto_20200123_2126.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.2.9 on 2020-01-23 21:26 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('chord', '0003_dataset_field_links'), - ] - - operations = [ - migrations.RenameField( - model_name='dataset', - old_name='field_links', - new_name='field_link_sets', - ), - ] diff --git a/chord_metadata_service/chord/migrations/0005_auto_20200123_2144.py b/chord_metadata_service/chord/migrations/0005_auto_20200123_2144.py deleted file mode 100644 index dd86c9448..000000000 --- a/chord_metadata_service/chord/migrations/0005_auto_20200123_2144.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.2.9 on 2020-01-23 21:44 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('chord', '0004_auto_20200123_2126'), - ] - - operations = [ - migrations.RenameField( - model_name='dataset', - old_name='field_link_sets', - new_name='linked_field_sets', - ), - ] diff --git a/chord_metadata_service/chord/migrations/0006_dataset_contact_info.py b/chord_metadata_service/chord/migrations/0006_dataset_contact_info.py deleted file mode 100644 index dfd86b8f1..000000000 --- a/chord_metadata_service/chord/migrations/0006_dataset_contact_info.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.2.9 on 2020-01-28 21:56 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('chord', '0005_auto_20200123_2144'), - ] - - operations = [ - migrations.AddField( - model_name='dataset', - name='contact_info', - field=models.TextField(blank=True), - ), - ] diff --git a/chord_metadata_service/chord/migrations/0007_auto_20200129_1537.py b/chord_metadata_service/chord/migrations/0007_auto_20200129_1537.py deleted file mode 100644 index df19acc07..000000000 --- a/chord_metadata_service/chord/migrations/0007_auto_20200129_1537.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 2.2.9 on 2020-01-29 15:37 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('chord', '0006_dataset_contact_info'), - ] - - operations = [ - migrations.AlterField( - model_name='dataset', - name='created', - field=models.DateTimeField(auto_now_add=True), - ), - migrations.AlterField( - model_name='dataset', - name='updated', - field=models.DateTimeField(auto_now=True), - ), - migrations.AlterField( - model_name='project', - name='created', - field=models.DateTimeField(auto_now_add=True), - ), - migrations.AlterField( - model_name='project', - name='updated', - field=models.DateTimeField(auto_now=True), - ), - ] diff --git a/chord_metadata_service/chord/migrations/0008_dataset_version.py b/chord_metadata_service/chord/migrations/0008_dataset_version.py deleted file mode 100644 index b8897e133..000000000 --- a/chord_metadata_service/chord/migrations/0008_dataset_version.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.2.9 on 2020-02-17 21:48 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('chord', '0007_auto_20200129_1537'), - ] - - operations = [ - migrations.AddField( - model_name='dataset', - name='version', - field=models.CharField(blank=True, default='version_2020-02-17 16:47:59.425036', help_text='A release point for the dataset when applicable.', max_length=200), - ), - ] diff --git a/chord_metadata_service/chord/migrations/0009_auto_20200218_1615.py b/chord_metadata_service/chord/migrations/0009_auto_20200218_1615.py deleted file mode 100644 index 17aec5acf..000000000 --- a/chord_metadata_service/chord/migrations/0009_auto_20200218_1615.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 2.2.9 on 2020-02-18 21:15 - -import chord_metadata_service.chord.models -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('chord', '0008_dataset_version'), - ] - - operations = [ - migrations.AlterField( - model_name='dataset', - name='version', - field=models.CharField(blank=True, default=chord_metadata_service.chord.models.version_default, help_text='A release point for the dataset when applicable.', max_length=200), - ), - ] diff --git a/chord_metadata_service/chord/migrations/0010_auto_20200309_1945.py b/chord_metadata_service/chord/migrations/0010_auto_20200309_1945.py deleted file mode 100644 index 5292a5273..000000000 --- a/chord_metadata_service/chord/migrations/0010_auto_20200309_1945.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 2.2.11 on 2020-03-09 19:45 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('chord', '0009_auto_20200218_1615'), - ] - - operations = [ - migrations.AlterField( - model_name='tableownership', - name='dataset', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='table_ownership', to='chord.Dataset'), - ), - ] diff --git a/chord_metadata_service/chord/migrations/0011_auto_20200515_1657.py b/chord_metadata_service/chord/migrations/0011_auto_20200515_1657.py deleted file mode 100644 index c806c1665..000000000 --- a/chord_metadata_service/chord/migrations/0011_auto_20200515_1657.py +++ /dev/null @@ -1,69 +0,0 @@ -# Generated by Django 2.2.12 on 2020-05-15 20:57 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('chord', '0010_auto_20200309_1945'), - ] - - operations = [ - migrations.RemoveField( - model_name='dataset', - name='acknowledges', - ), - migrations.RemoveField( - model_name='dataset', - name='alternate_identifiers', - ), - migrations.RemoveField( - model_name='dataset', - name='citations', - ), - migrations.RemoveField( - model_name='dataset', - name='creators', - ), - migrations.RemoveField( - model_name='dataset', - name='dates', - ), - migrations.RemoveField( - model_name='dataset', - name='dimensions', - ), - migrations.RemoveField( - model_name='dataset', - name='distributions', - ), - migrations.RemoveField( - model_name='dataset', - name='keywords', - ), - migrations.RemoveField( - model_name='dataset', - name='licenses', - ), - migrations.RemoveField( - model_name='dataset', - name='linked_field_sets', - ), - migrations.RemoveField( - model_name='dataset', - name='primary_publications', - ), - migrations.RemoveField( - model_name='dataset', - name='related_identifiers', - ), - migrations.RemoveField( - model_name='dataset', - name='spatial_coverage', - ), - migrations.RemoveField( - model_name='dataset', - name='types', - ), - ] diff --git a/chord_metadata_service/chord/migrations/0012_auto_20200515_1714.py b/chord_metadata_service/chord/migrations/0012_auto_20200515_1714.py deleted file mode 100644 index a6b657b29..000000000 --- a/chord_metadata_service/chord/migrations/0012_auto_20200515_1714.py +++ /dev/null @@ -1,84 +0,0 @@ -# Generated by Django 2.2.12 on 2020-05-15 21:14 - -import django.contrib.postgres.fields.jsonb -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('chord', '0011_auto_20200515_1657'), - ] - - operations = [ - migrations.AddField( - model_name='dataset', - name='acknowledges', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, help_text='The grant(s) which funded and supported the work reported by the dataset.'), - ), - migrations.AddField( - model_name='dataset', - name='alternate_identifiers', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, help_text='Alternate identifiers for the dataset.'), - ), - migrations.AddField( - model_name='dataset', - name='citations', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, help_text='The publication(s) that cite this dataset.'), - ), - migrations.AddField( - model_name='dataset', - name='creators', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, help_text='The person(s) or organization(s) which contributed to the creation of the dataset.'), - ), - migrations.AddField( - model_name='dataset', - name='dates', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, help_text='Relevant dates for the datasets, a date must be added, e.g. creation date or last modification date should be added.'), - ), - migrations.AddField( - model_name='dataset', - name='dimensions', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, help_text='The different dimensions (granular components) making up a dataset.'), - ), - migrations.AddField( - model_name='dataset', - name='distributions', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, help_text='The distribution(s) by which datasets are made available (for example: mySQL dump).'), - ), - migrations.AddField( - model_name='dataset', - name='keywords', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, help_text='Tags associated with the dataset, which will help in its discovery.'), - ), - migrations.AddField( - model_name='dataset', - name='licenses', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, help_text='The terms of use of the dataset.'), - ), - migrations.AddField( - model_name='dataset', - name='linked_field_sets', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, help_text='Data type fields which are linked together.'), - ), - migrations.AddField( - model_name='dataset', - name='primary_publications', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, help_text='The primary publication(s) associated with the dataset, usually describing how the dataset was produced.'), - ), - migrations.AddField( - model_name='dataset', - name='related_identifiers', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, help_text='Related identifiers for the dataset.'), - ), - migrations.AddField( - model_name='dataset', - name='spatial_coverage', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, help_text='The geographical extension and span covered by the dataset and its measured dimensions/variables.'), - ), - migrations.AddField( - model_name='dataset', - name='types', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, help_text='A term, ideally from a controlled terminology, identifying the dataset type or nature of the data, placing it in a typology.'), - ), - ] diff --git a/chord_metadata_service/chord/migrations/0013_table.py b/chord_metadata_service/chord/migrations/0013_table.py deleted file mode 100644 index 90e4c95ac..000000000 --- a/chord_metadata_service/chord/migrations/0013_table.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 2.2.12 on 2020-05-19 15:38 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('chord', '0012_auto_20200515_1714'), - ] - - operations = [ - migrations.CreateModel( - name='Table', - fields=[ - ('ownership_record', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='chord.TableOwnership')), - ('name', models.CharField(max_length=200, unique=True)), - ('data_type', models.CharField(choices=[('experiment', 'experiment'), ('phenopacket', 'phenopacket')], max_length=30)), - ('created', models.DateTimeField(auto_now_add=True)), - ('updated', models.DateTimeField(auto_now=True)), - ], - ), - ] diff --git a/chord_metadata_service/chord/migrations/0014_remove_table_data_type.py b/chord_metadata_service/chord/migrations/0014_remove_table_data_type.py deleted file mode 100644 index eac87843f..000000000 --- a/chord_metadata_service/chord/migrations/0014_remove_table_data_type.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 2.2.12 on 2020-05-19 15:54 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('chord', '0013_table'), - ] - - operations = [ - migrations.RemoveField( - model_name='table', - name='data_type', - ), - ] diff --git a/chord_metadata_service/chord/migrations/0015_table_data_type.py b/chord_metadata_service/chord/migrations/0015_table_data_type.py deleted file mode 100644 index b532892f4..000000000 --- a/chord_metadata_service/chord/migrations/0015_table_data_type.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 2.2.12 on 2020-05-19 15:55 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('chord', '0014_remove_table_data_type'), - ] - - operations = [ - migrations.AddField( - model_name='table', - name='data_type', - field=models.CharField(choices=[('experiment', 'experiment'), ('phenopacket', 'phenopacket')], default='phenopacket', max_length=30), - preserve_default=False, - ), - ] diff --git a/chord_metadata_service/chord/migrations/0016_auto_20200519_2100.py b/chord_metadata_service/chord/migrations/0016_auto_20200519_2100.py deleted file mode 100644 index f4e4459b0..000000000 --- a/chord_metadata_service/chord/migrations/0016_auto_20200519_2100.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.2.12 on 2020-05-19 21:00 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('chord', '0015_table_data_type'), - ] - - operations = [ - migrations.AlterField( - model_name='tableownership', - name='service_id', - field=models.CharField(max_length=200), - ), - ] diff --git a/chord_metadata_service/chord/migrations/0017_dataset_additional_resources.py b/chord_metadata_service/chord/migrations/0017_dataset_additional_resources.py deleted file mode 100644 index bd388cfeb..000000000 --- a/chord_metadata_service/chord/migrations/0017_dataset_additional_resources.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 2.2.12 on 2020-06-01 14:44 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('resources', '0001_initial'), - ('chord', '0016_auto_20200519_2100'), - ] - - operations = [ - migrations.AddField( - model_name='dataset', - name='additional_resources', - field=models.ManyToManyField(blank=True, help_text="Any resource objects linked to this dataset that aren't specified by a phenopacket in the dataset.", to='resources.Resource'), - ), - ] diff --git a/chord_metadata_service/chord/migrations/0018_auto_20200601_1708.py b/chord_metadata_service/chord/migrations/0018_auto_20200601_1708.py deleted file mode 100644 index a356b04ff..000000000 --- a/chord_metadata_service/chord/migrations/0018_auto_20200601_1708.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 2.2.12 on 2020-06-01 17:08 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('chord', '0017_dataset_additional_resources'), - ] - - operations = [ - migrations.RemoveField( - model_name='tableownership', - name='data_type', - ), - migrations.RemoveField( - model_name='tableownership', - name='sample', - ), - ] diff --git a/chord_metadata_service/experiments/migrations/0001_initial.py b/chord_metadata_service/experiments/migrations/0001_initial.py deleted file mode 100644 index b854ba022..000000000 --- a/chord_metadata_service/experiments/migrations/0001_initial.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 2.2.11 on 2020-03-25 19:50 - -import chord_metadata_service.restapi.models -import chord_metadata_service.restapi.validators -import django.contrib.postgres.fields -import django.contrib.postgres.fields.jsonb -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='Experiment', - fields=[ - ('id', models.CharField(help_text='An arbitrary identifier for the experiment.', max_length=200, primary_key=True, serialize=False)), - ('reference_registry_id', models.CharField(blank=True, help_text='The IHEC EpiRR ID for this dataset, only for IHEC Reference Epigenome datasets. Otherwise leave empty.', max_length=30, null=True)), - ('qc_flags', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(help_text='Any quanlity control observations can be noted here. This field can be omitted if empty', max_length=100), default=list, null=True, size=None)), - ('experiment_type', models.CharField(help_text="(Controlled Vocabulary) The assay target (e.g. ‘DNA Methylation’, ‘mRNA-Seq’, ‘smRNA-Seq’, 'Histone H3K4me1').", max_length=30)), - ('experiment_ontology', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='(Ontology: OBI) links to experiment ontology information.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'ONTOLOGY_CLASS_LIST', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'ONTOLOGY_CLASS', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'})])), - ('molecule_ontology', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='(Ontology: SO) links to molecule ontology information.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'ONTOLOGY_CLASS_LIST', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'ONTOLOGY_CLASS', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'})])), - ('molecule', models.CharField(blank=True, choices=[('total RNA', 'total RNA'), ('polyA RNA', 'polyA RNA'), ('cytoplasmic RNA', 'cytoplasmic RNA'), ('nuclear RNA', 'nuclear RNA'), ('small RNA', 'small RNA'), ('genomic DNA', 'genomic DNA'), ('protein', 'protein'), ('other', 'other')], help_text='(Controlled Vocabulary) The type of molecule that was extracted from the biological material. Include one of the following: total RNA, polyA RNA, cytoplasmic RNA, nuclear RNA, small RNA, genomic DNA, protein, or other.', max_length=20, null=True)), - ('library_strategy', models.CharField(choices=[('DNase-Hypersensitivity', 'DNase-Hypersensitivity'), ('ATAC-seq', 'ATAC-seq'), ('NOME-Seq', 'NOME-Seq'), ('Bisulfite-Seq', 'Bisulfite-Seq'), ('MeDIP-Seq', 'MeDIP-Seq'), ('MRE-Seq', 'MRE-Seq'), ('ChIP-Seq', 'ChIP-Seq'), ('RNA-Seq', 'RNA-Seq'), ('miRNA-Seq', 'miRNA-Seq'), ('WGS', 'WGS')], help_text='(Controlled Vocabulary) The assay used. These are defined within the SRA metadata specifications with a controlled vocabulary (e.g. ‘Bisulfite-Seq’, ‘RNA-Seq’, ‘ChIP-Seq’). For a complete list, see https://www.ebi.ac.uk/ena/submit/reads-library-strategy.', max_length=25)), - ('other_fields', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The other fields for the experiment', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'KEY_VALUE_OBJECT', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'The schema represents a key-value object.', 'patternProperties': {'^.*$': {'type': 'string'}}, 'title': 'Key-value object', 'type': 'object'})])), - ], - bases=(models.Model, chord_metadata_service.restapi.models.IndexableMixin), - ), - ] diff --git a/chord_metadata_service/experiments/migrations/0001_v1_0_0.py b/chord_metadata_service/experiments/migrations/0001_v1_0_0.py new file mode 100644 index 000000000..3bf230ccc --- /dev/null +++ b/chord_metadata_service/experiments/migrations/0001_v1_0_0.py @@ -0,0 +1,38 @@ +# Generated by Django 2.2.13 on 2020-07-06 14:55 + +import chord_metadata_service.restapi.models +import chord_metadata_service.restapi.validators +import django.contrib.postgres.fields +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('chord', '0001_v1_0_0'), + ('phenopackets', '0001_v1_0_0'), + ] + + operations = [ + migrations.CreateModel( + name='Experiment', + fields=[ + ('id', models.CharField(help_text='An arbitrary identifier for the experiment.', max_length=200, primary_key=True, serialize=False)), + ('reference_registry_id', models.CharField(blank=True, help_text='The IHEC EpiRR ID for this dataset, only for IHEC Reference Epigenome datasets. Otherwise leave empty.', max_length=30, null=True)), + ('qc_flags', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(help_text='Any quality control observations can be noted here. This field can be omitted if empty', max_length=100), blank=True, default=list, size=None)), + ('experiment_type', models.CharField(help_text="(Controlled Vocabulary) The assay target (e.g. ‘DNA Methylation’, ‘mRNA-Seq’, ‘smRNA-Seq’, 'Histone H3K4me1').", max_length=30)), + ('experiment_ontology', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, help_text='Links to experiment ontology information (e.g. via the OBI ontology.)', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)])), + ('molecule_ontology', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, help_text='Links to molecule ontology information (e.g. via the SO ontology.)', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)])), + ('molecule', models.CharField(blank=True, choices=[('total RNA', 'total RNA'), ('polyA RNA', 'polyA RNA'), ('cytoplasmic RNA', 'cytoplasmic RNA'), ('nuclear RNA', 'nuclear RNA'), ('small RNA', 'small RNA'), ('genomic DNA', 'genomic DNA'), ('protein', 'protein'), ('other', 'other')], help_text='(Controlled Vocabulary) The type of molecule that was extracted from the biological material.Include one of the following: total RNA, polyA RNA, cytoplasmic RNA, nuclear RNA, small RNA, genomic DNA, protein, or other.', max_length=20, null=True)), + ('library_strategy', models.CharField(choices=[('DNase-Hypersensitivity', 'DNase-Hypersensitivity'), ('ATAC-seq', 'ATAC-seq'), ('NOME-Seq', 'NOME-Seq'), ('Bisulfite-Seq', 'Bisulfite-Seq'), ('MeDIP-Seq', 'MeDIP-Seq'), ('MRE-Seq', 'MRE-Seq'), ('ChIP-Seq', 'ChIP-Seq'), ('RNA-Seq', 'RNA-Seq'), ('miRNA-Seq', 'miRNA-Seq'), ('WGS', 'WGS')], help_text='(Controlled Vocabulary) The assay used. These are defined within the SRA metadata specifications with a controlled vocabulary (e.g. ‘Bisulfite-Seq’, ‘RNA-Seq’, ‘ChIP-Seq’). For a complete list, see https://www.ebi.ac.uk/ena/submit/reads-library-strategy.', max_length=25)), + ('other_fields', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict, help_text='The other fields for the experiment.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:key_value_object_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'The schema represents a key-value object.', 'patternProperties': {'^.*$': {'type': 'string'}}, 'title': 'Key-value object', 'type': 'object'}, formats=None)])), + ('biosample', models.ForeignKey(help_text='Biosample on which this experiment was done.', on_delete=django.db.models.deletion.CASCADE, to='phenopackets.Biosample')), + ('table', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='chord.Table')), + ], + bases=(models.Model, chord_metadata_service.restapi.models.IndexableMixin), + ), + ] diff --git a/chord_metadata_service/experiments/migrations/0002_auto_20200327_1728.py b/chord_metadata_service/experiments/migrations/0002_auto_20200327_1728.py deleted file mode 100644 index a651e05d5..000000000 --- a/chord_metadata_service/experiments/migrations/0002_auto_20200327_1728.py +++ /dev/null @@ -1,40 +0,0 @@ -# Generated by Django 2.2.11 on 2020-03-27 17:28 - -import chord_metadata_service.restapi.validators -import django.contrib.postgres.fields.jsonb -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('experiments', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='experiment', - name='experiment_ontology', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text='(Ontology: OBI) links to experiment ontology information.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'ONTOLOGY_CLASS_LIST', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'ONTOLOGY_CLASS', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'})]), - ), - migrations.AlterField( - model_name='experiment', - name='molecule', - field=models.CharField(choices=[('total RNA', 'total RNA'), ('polyA RNA', 'polyA RNA'), ('cytoplasmic RNA', 'cytoplasmic RNA'), ('nuclear RNA', 'nuclear RNA'), ('small RNA', 'small RNA'), ('genomic DNA', 'genomic DNA'), ('protein', 'protein'), ('other', 'other')], help_text='(Controlled Vocabulary) The type of molecule that was extracted from the biological material. Include one of the following: total RNA, polyA RNA, cytoplasmic RNA, nuclear RNA, small RNA, genomic DNA, protein, or other.', max_length=20, null=True), - ), - migrations.AlterField( - model_name='experiment', - name='molecule_ontology', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text='(Ontology: SO) links to molecule ontology information.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'ONTOLOGY_CLASS_LIST', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'ONTOLOGY_CLASS', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'})]), - ), - migrations.AlterField( - model_name='experiment', - name='other_fields', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text='The other fields for the experiment', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'KEY_VALUE_OBJECT', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'The schema represents a key-value object.', 'patternProperties': {'^.*$': {'type': 'string'}}, 'title': 'Key-value object', 'type': 'object'})]), - ), - migrations.AlterField( - model_name='experiment', - name='reference_registry_id', - field=models.CharField(help_text='The IHEC EpiRR ID for this dataset, only for IHEC Reference Epigenome datasets. Otherwise leave empty.', max_length=30, null=True), - ), - ] diff --git a/chord_metadata_service/experiments/migrations/0003_auto_20200331_1841.py b/chord_metadata_service/experiments/migrations/0003_auto_20200331_1841.py deleted file mode 100644 index 6a16c6c2b..000000000 --- a/chord_metadata_service/experiments/migrations/0003_auto_20200331_1841.py +++ /dev/null @@ -1,59 +0,0 @@ -# Generated by Django 2.2.11 on 2020-03-31 18:41 - -import chord_metadata_service.restapi.validators -import django.contrib.postgres.fields -import django.contrib.postgres.fields.jsonb -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('patients', '0004_auto_20200129_1537'), - ('phenopackets', '0004_auto_20200129_1537'), - ('experiments', '0002_auto_20200327_1728'), - ] - - operations = [ - migrations.AddField( - model_name='experiment', - name='biosample', - field=models.ForeignKey(blank=True, help_text='Biosamples on which this experiment was done', null=True, on_delete=django.db.models.deletion.SET_NULL, to='phenopackets.Biosample'), - ), - migrations.AddField( - model_name='experiment', - name='individual', - field=models.ForeignKey(blank=True, help_text='Donor on which this experiment was done', null=True, on_delete=django.db.models.deletion.SET_NULL, to='patients.Individual'), - ), - migrations.AlterField( - model_name='experiment', - name='experiment_ontology', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='(Ontology: OBI) links to experiment ontology information.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'ONTOLOGY_CLASS_LIST', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'ONTOLOGY_CLASS', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'})]), - ), - migrations.AlterField( - model_name='experiment', - name='molecule', - field=models.CharField(blank=True, choices=[('total RNA', 'total RNA'), ('polyA RNA', 'polyA RNA'), ('cytoplasmic RNA', 'cytoplasmic RNA'), ('nuclear RNA', 'nuclear RNA'), ('small RNA', 'small RNA'), ('genomic DNA', 'genomic DNA'), ('protein', 'protein'), ('other', 'other')], help_text='(Controlled Vocabulary) The type of molecule that was extracted from the biological material. Include one of the following: total RNA, polyA RNA, cytoplasmic RNA, nuclear RNA, small RNA, genomic DNA, protein, or other.', max_length=20, null=True), - ), - migrations.AlterField( - model_name='experiment', - name='molecule_ontology', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='(Ontology: SO) links to molecule ontology information.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'ONTOLOGY_CLASS_LIST', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'ONTOLOGY_CLASS', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'})]), - ), - migrations.AlterField( - model_name='experiment', - name='other_fields', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The other fields for the experiment', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'KEY_VALUE_OBJECT', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'The schema represents a key-value object.', 'patternProperties': {'^.*$': {'type': 'string'}}, 'title': 'Key-value object', 'type': 'object'})]), - ), - migrations.AlterField( - model_name='experiment', - name='qc_flags', - field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(help_text='Any quanlity control observations can be noted here. This field can be omitted if empty', max_length=100), blank=True, default=list, null=True, size=None), - ), - migrations.AlterField( - model_name='experiment', - name='reference_registry_id', - field=models.CharField(blank=True, help_text='The IHEC EpiRR ID for this dataset, only for IHEC Reference Epigenome datasets. Otherwise leave empty.', max_length=30, null=True), - ), - ] diff --git a/chord_metadata_service/experiments/migrations/0004_auto_20200401_1445.py b/chord_metadata_service/experiments/migrations/0004_auto_20200401_1445.py deleted file mode 100644 index 2acccced4..000000000 --- a/chord_metadata_service/experiments/migrations/0004_auto_20200401_1445.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 2.2.10 on 2020-04-01 18:45 - -import chord_metadata_service.restapi.validators -import django.contrib.postgres.fields.jsonb -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('experiments', '0003_auto_20200331_1841'), - ] - - operations = [ - migrations.AlterField( - model_name='experiment', - name='experiment_ontology', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='(Ontology: OBI) links to experiment ontology information.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'ONTOLOGY_CLASS_LIST', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'})]), - ), - migrations.AlterField( - model_name='experiment', - name='molecule_ontology', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='(Ontology: SO) links to molecule ontology information.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'ONTOLOGY_CLASS_LIST', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'})]), - ), - ] diff --git a/chord_metadata_service/experiments/migrations/0005_auto_20200513_1401.py b/chord_metadata_service/experiments/migrations/0005_auto_20200513_1401.py deleted file mode 100644 index 21f774d30..000000000 --- a/chord_metadata_service/experiments/migrations/0005_auto_20200513_1401.py +++ /dev/null @@ -1,36 +0,0 @@ -# Generated by Django 2.2.12 on 2020-05-13 18:01 - -import chord_metadata_service.restapi.validators -import django.contrib.postgres.fields.jsonb -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('experiments', '0004_auto_20200401_1445'), - ] - - operations = [ - migrations.AlterField( - model_name='experiment', - name='biosample', - field=models.ForeignKey(blank=True, help_text='Biosample on which this experiment was done', null=True, on_delete=django.db.models.deletion.SET_NULL, to='phenopackets.Biosample'), - ), - migrations.AlterField( - model_name='experiment', - name='experiment_ontology', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='(Ontology: OBI) links to experiment ontology information.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)]), - ), - migrations.AlterField( - model_name='experiment', - name='molecule_ontology', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='(Ontology: SO) links to molecule ontology information.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)]), - ), - migrations.AlterField( - model_name='experiment', - name='other_fields', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The other fields for the experiment', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:key_value_object_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'The schema represents a key-value object.', 'patternProperties': {'^.*$': {'type': 'string'}}, 'title': 'Key-value object', 'type': 'object'}, formats=None)]), - ), - ] diff --git a/chord_metadata_service/experiments/migrations/0006_auto_20200514_1541.py b/chord_metadata_service/experiments/migrations/0006_auto_20200514_1541.py deleted file mode 100644 index 9e9b6f967..000000000 --- a/chord_metadata_service/experiments/migrations/0006_auto_20200514_1541.py +++ /dev/null @@ -1,46 +0,0 @@ -# Generated by Django 2.2.12 on 2020-05-14 19:41 - -import chord_metadata_service.restapi.validators -import django.contrib.postgres.fields.jsonb -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('experiments', '0005_auto_20200513_1401'), - ] - - operations = [ - migrations.AlterField( - model_name='experiment', - name='biosample', - field=models.ForeignKey(blank=True, help_text='Biosample on which this experiment was done.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='phenopackets.Biosample'), - ), - migrations.AlterField( - model_name='experiment', - name='experiment_ontology', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='(Ontology: OBI) links to experiment ontology information.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)]), - ), - migrations.AlterField( - model_name='experiment', - name='individual', - field=models.ForeignKey(blank=True, help_text='Donor on which this experiment was done.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='patients.Individual'), - ), - migrations.AlterField( - model_name='experiment', - name='molecule', - field=models.CharField(blank=True, choices=[('total RNA', 'total RNA'), ('polyA RNA', 'polyA RNA'), ('cytoplasmic RNA', 'cytoplasmic RNA'), ('nuclear RNA', 'nuclear RNA'), ('small RNA', 'small RNA'), ('genomic DNA', 'genomic DNA'), ('protein', 'protein'), ('other', 'other')], help_text='(Controlled Vocabulary) The type of molecule that was extracted from the biological material.Include one of the following: total RNA, polyA RNA, cytoplasmic RNA, nuclear RNA, small RNA, genomic DNA, protein, or other.', max_length=20, null=True), - ), - migrations.AlterField( - model_name='experiment', - name='molecule_ontology', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='(Ontology: SO) links to molecule ontology information.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)]), - ), - migrations.AlterField( - model_name='experiment', - name='other_fields', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The other fields for the experiment.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:key_value_object_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'The schema represents a key-value object.', 'patternProperties': {'^.*$': {'type': 'string'}}, 'title': 'Key-value object', 'type': 'object'}, formats=None)]), - ), - ] diff --git a/chord_metadata_service/experiments/migrations/0007_auto_20200519_1538.py b/chord_metadata_service/experiments/migrations/0007_auto_20200519_1538.py deleted file mode 100644 index b41e643fc..000000000 --- a/chord_metadata_service/experiments/migrations/0007_auto_20200519_1538.py +++ /dev/null @@ -1,52 +0,0 @@ -# Generated by Django 2.2.12 on 2020-05-19 15:38 - -import chord_metadata_service.restapi.validators -import django.contrib.postgres.fields -import django.contrib.postgres.fields.jsonb -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('chord', '0013_table'), - ('experiments', '0006_auto_20200514_1541'), - ] - - operations = [ - migrations.RemoveField( - model_name='experiment', - name='individual', - ), - migrations.AddField( - model_name='experiment', - name='table', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='chord.Table'), - ), - migrations.AlterField( - model_name='experiment', - name='biosample', - field=models.ForeignKey(help_text='Biosample on which this experiment was done.', on_delete=django.db.models.deletion.CASCADE, to='phenopackets.Biosample'), - ), - migrations.AlterField( - model_name='experiment', - name='experiment_ontology', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, help_text='(Ontology: OBI) links to experiment ontology information.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)]), - ), - migrations.AlterField( - model_name='experiment', - name='molecule_ontology', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, help_text='(Ontology: SO) links to molecule ontology information.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)]), - ), - migrations.AlterField( - model_name='experiment', - name='other_fields', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict, help_text='The other fields for the experiment.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:key_value_object_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'The schema represents a key-value object.', 'patternProperties': {'^.*$': {'type': 'string'}}, 'title': 'Key-value object', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='experiment', - name='qc_flags', - field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(help_text='Any quanlity control observations can be noted here. This field can be omitted if empty', max_length=100), blank=True, default=list, size=None), - ), - ] diff --git a/chord_metadata_service/experiments/migrations/0008_auto_20200601_1438.py b/chord_metadata_service/experiments/migrations/0008_auto_20200601_1438.py deleted file mode 100644 index 771eab656..000000000 --- a/chord_metadata_service/experiments/migrations/0008_auto_20200601_1438.py +++ /dev/null @@ -1,31 +0,0 @@ -# Generated by Django 2.2.12 on 2020-06-01 14:38 - -import chord_metadata_service.restapi.validators -import django.contrib.postgres.fields -import django.contrib.postgres.fields.jsonb -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('experiments', '0007_auto_20200519_1538'), - ] - - operations = [ - migrations.AlterField( - model_name='experiment', - name='experiment_ontology', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, help_text='Links to experiment ontology information (e.g. via the OBI ontology.)', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)]), - ), - migrations.AlterField( - model_name='experiment', - name='molecule_ontology', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, help_text='Links to molecule ontology information (e.g. via the SO ontology.)', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)]), - ), - migrations.AlterField( - model_name='experiment', - name='qc_flags', - field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(help_text='Any quality control observations can be noted here. This field can be omitted if empty', max_length=100), blank=True, default=list, size=None), - ), - ] diff --git a/chord_metadata_service/mcode/migrations/0001_initial.py b/chord_metadata_service/mcode/migrations/0001_initial.py deleted file mode 100644 index e44b7d689..000000000 --- a/chord_metadata_service/mcode/migrations/0001_initial.py +++ /dev/null @@ -1,163 +0,0 @@ -# Generated by Django 2.2.10 on 2020-03-31 22:39 - -import chord_metadata_service.restapi.models -import chord_metadata_service.restapi.validators -import django.contrib.postgres.fields -import django.contrib.postgres.fields.jsonb -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('phenopackets', '0004_auto_20200129_1537'), - ('patients', '0005_auto_20200311_1610'), - ] - - operations = [ - migrations.CreateModel( - name='CancerCondition', - fields=[ - ('id', models.CharField(help_text='An arbitrary identifier for the cancer condition.', max_length=200, primary_key=True, serialize=False)), - ('condition_type', models.CharField(choices=[('primary', 'primary'), ('secondary', 'secondary')], help_text='Cancer condition type: primary or secondary.', max_length=200)), - ('body_location_code', django.contrib.postgres.fields.jsonb.JSONField(help_text='Code for the body location, optionally pre-coordinating laterality or direction. Accepted ontologies: SNOMED CT, ICD-O-3 and others.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'ONTOLOGY_CLASS_LIST', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'})])), - ('clinical_status', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='A flag indicating whether the condition is active or inactive, recurring, in remission, or resolved (as of the last update of the Condition). Accepted code system: http://terminology.hl7.org/CodeSystem/condition-clinical', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'})])), - ('condition_code', django.contrib.postgres.fields.jsonb.JSONField(help_text='A code describing the type of primary or secondary malignant neoplastic disease.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'})])), - ('date_of_diagnosis', models.DateTimeField(blank=True, help_text='The date the disease was first clinically recognized with sufficient certainty, regardless of whether it was fully characterized at that time.', null=True)), - ('histology_morphology_behavior', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='A description of the morphologic and behavioral characteristics of the cancer. Accepted ontologies: SNOMED CT, ICD-O-3 and others.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'})])), - ], - options={ - 'ordering': ['id'], - }, - bases=(models.Model, chord_metadata_service.restapi.models.IndexableMixin), - ), - migrations.CreateModel( - name='CancerRelatedProcedure', - fields=[ - ('id', models.CharField(help_text='An arbitrary identifier for the procedure.', max_length=200, primary_key=True, serialize=False)), - ('procedure_type', models.CharField(choices=[('radiation', 'radiation'), ('surgical', 'surgical')], help_text='Type of cancer related procedure: radion or surgical.', max_length=200)), - ('code', django.contrib.postgres.fields.jsonb.JSONField(help_text='Code for the procedure performed.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'})])), - ('occurence_time_or_period', django.contrib.postgres.fields.jsonb.JSONField(help_text='The date/time that a procedure was performed.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:time_or_period', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Time of Period schema.', 'properties': {'value': {'anyOf': [{'format': 'date-time', 'type': 'string'}, {'$id': 'chord_metadata_service:period_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Period schema.', 'properties': {'end': {'format': 'date-time', 'type': 'string'}, 'start': {'format': 'date-time', 'type': 'string'}}, 'title': 'Period', 'type': 'object'}]}}, 'title': 'Time of Period', 'type': 'object'})])), - ('target_body_site', django.contrib.postgres.fields.jsonb.JSONField(help_text='The body location(s) where the procedure was performed.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'ONTOLOGY_CLASS_LIST', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'})])), - ('treatment_intent', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The purpose of a treatment.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'})])), - ], - options={ - 'ordering': ['id'], - }, - bases=(models.Model, chord_metadata_service.restapi.models.IndexableMixin), - ), - migrations.CreateModel( - name='GeneticVariantFound', - fields=[ - ('id', models.CharField(help_text='An arbitrary identifier for the genetic variant found.', max_length=200, primary_key=True, serialize=False)), - ('method', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology or controlled vocabulary term to identify the method used to perform the genetic test. Accepted value set: NCIT.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'})])), - ('variant_found_identifier', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The variation ID assigned by HGVS, for example, 360448 is the identifier for NM_005228.4(EGFR):c.-237A>G (single nucleotide variant in EGFR). Accepted value set: ClinVar.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'})])), - ('variant_found_hgvs_name', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=200), blank=True, help_text='Symbolic representation of the variant used in HGVS, for example, NM_005228.4(EGFR):c.-237A>G for HVGS variation ID 360448.', null=True, size=None)), - ('variant_found_description', models.CharField(blank=True, help_text='Description of the variant.', max_length=200)), - ('genomic_source_class', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology or controlled vocabulary term to identify the genomic class of the specimen being analyzed.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'})])), - ], - options={ - 'ordering': ['id'], - }, - bases=(models.Model, chord_metadata_service.restapi.models.IndexableMixin), - ), - migrations.CreateModel( - name='GeneticVariantTested', - fields=[ - ('id', models.CharField(help_text='An arbitrary identifier for the genetic variant tested.', max_length=200, primary_key=True, serialize=False)), - ('method', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology or controlled vocabulary term to identify the method used to perform the genetic test. Accepted value set: NCIT.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'})])), - ('variant_tested_identifier', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The variation ID assigned by HGVS, for example, 360448 is the identifier for NM_005228.4(EGFR):c.-237A>G (single nucleotide variant in EGFR).', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'})])), - ('variant_tested_hgvs_name', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=200), blank=True, help_text='Symbolic representation of the variant used in HGVS, for example, NM_005228.4(EGFR):c.-237A>G for HVGS variation ID 360448.', null=True, size=None)), - ('variant_tested_description', models.CharField(blank=True, help_text='Description of the variant.', max_length=200)), - ('data_value', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology or controlled vocabulary term to identify positive or negative value forthe mutation. Accepted value set: SNOMED CT.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'})])), - ('gene_studied', models.ForeignKey(blank=True, help_text='A gene targeted for mutation analysis, identified in HUGO Gene Nomenclature Committee (HGNC) notation.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='phenopackets.Gene')), - ], - options={ - 'ordering': ['id'], - }, - bases=(models.Model, chord_metadata_service.restapi.models.IndexableMixin), - ), - migrations.CreateModel( - name='GenomicsReport', - fields=[ - ('id', models.CharField(help_text='An arbitrary identifier for the genetics report.', max_length=200, primary_key=True, serialize=False)), - ('test_name', django.contrib.postgres.fields.jsonb.JSONField(help_text='An ontology or controlled vocabulary term to identify the laboratory test. Accepted value sets: LOINC, GTR.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'})])), - ('performing_organization_name', models.CharField(blank=True, help_text='The name of the organization producing the genomics report.', max_length=200)), - ('specimen_type', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology or controlled vocabulary term to identify the type of material the specimen contains or consists of. Accepted value set: HL7 Version 2 and Specimen Type.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'})])), - ('genetic_variant_found', models.ManyToManyField(blank=True, help_text='Records an alteration in the most common DNA nucleotide sequence.', to='mcode.GeneticVariantFound')), - ('genetic_variant_tested', models.ManyToManyField(blank=True, help_text='A test for a specific mutation on a particular gene.', to='mcode.GeneticVariantTested')), - ], - options={ - 'ordering': ['id'], - }, - bases=(models.Model, chord_metadata_service.restapi.models.IndexableMixin), - ), - migrations.CreateModel( - name='MedicationStatement', - fields=[ - ('id', models.CharField(help_text='An arbitrary identifier for the medication statement.', max_length=200, primary_key=True, serialize=False)), - ('medication_code', django.contrib.postgres.fields.jsonb.JSONField(help_text='A code for medication. Accepted code systems: Medication Clinical Drug (RxNorm) and other.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'})])), - ('termination_reason', django.contrib.postgres.fields.jsonb.JSONField(help_text='A code explaining unplanned or premature termination of a course of medication. Accepted ontologies: SNOMED CT.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'ONTOLOGY_CLASS_LIST', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'})])), - ('treatment_intent', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The purpose of a treatment. Accepted ontologies: SNOMED CT.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'})])), - ('start_date', models.DateTimeField(blank=True, help_text='The start date/time of the medication.', null=True)), - ('end_date', models.DateTimeField(blank=True, help_text='The end date/time of the medication.', null=True)), - ('date_time', models.DateTimeField(blank=True, help_text='The date/time the medication was administered.', null=True)), - ], - options={ - 'ordering': ['id'], - }, - bases=(models.Model, chord_metadata_service.restapi.models.IndexableMixin), - ), - migrations.CreateModel( - name='TNMStaging', - fields=[ - ('id', models.CharField(help_text='An arbitrary identifier for the TNM staging.', max_length=200, primary_key=True, serialize=False)), - ('tnm_type', models.CharField(choices=[('clinical', 'clinical'), ('pathologic', 'pathologic')], help_text='TNM type: clinical or pathological.', max_length=200)), - ('stage_group', django.contrib.postgres.fields.jsonb.JSONField(help_text='The extent of the cancer in the body, according to the TNM classification system.Accepted ontologies: SNOMED CT, AJCC and others.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:complex_ontology_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Complex object to combine data value and staging system.', 'properties': {'data_value': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'staging_system': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}}, 'required': ['data_value'], 'title': 'Complex ontology', 'type': 'object'})])), - ('primary_tumor_category', django.contrib.postgres.fields.jsonb.JSONField(help_text='Category of the primary tumor, based on its size and extent. Accepted ontologies: SNOMED CT, AJCC and others.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:complex_ontology_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Complex object to combine data value and staging system.', 'properties': {'data_value': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'staging_system': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}}, 'required': ['data_value'], 'title': 'Complex ontology', 'type': 'object'})])), - ('regional_nodes_category', django.contrib.postgres.fields.jsonb.JSONField(help_text='Category of the presence or absence of metastases in regional lymph nodes. Accepted ontologies: SNOMED CT, AJCC and others.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:complex_ontology_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Complex object to combine data value and staging system.', 'properties': {'data_value': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'staging_system': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}}, 'required': ['data_value'], 'title': 'Complex ontology', 'type': 'object'})])), - ('distant_metastases_category', django.contrib.postgres.fields.jsonb.JSONField(help_text='Category describing the presence or absence of metastases in remote anatomical locations. Accepted ontologies: SNOMED CT, AJCC and others.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:complex_ontology_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Complex object to combine data value and staging system.', 'properties': {'data_value': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'staging_system': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}}, 'required': ['data_value'], 'title': 'Complex ontology', 'type': 'object'})])), - ('cancer_condition', models.ForeignKey(help_text='Cancer condition.', on_delete=django.db.models.deletion.CASCADE, to='mcode.CancerCondition')), - ], - options={ - 'ordering': ['id'], - }, - bases=(models.Model, chord_metadata_service.restapi.models.IndexableMixin), - ), - migrations.CreateModel( - name='MCodePacket', - fields=[ - ('id', models.CharField(help_text='An arbitrary identifier for the mcodepacket.', max_length=200, primary_key=True, serialize=False)), - ('cancer_condition', models.ForeignKey(blank=True, help_text="An Individual's cancer condition.", null=True, on_delete=django.db.models.deletion.SET_NULL, to='mcode.CancerCondition')), - ('cancer_related_procedures', models.ManyToManyField(blank=True, help_text='A radiological or surgical procedures addressing a cancer condition.', to='mcode.CancerRelatedProcedure')), - ('genomics_report', models.ForeignKey(blank=True, help_text='A genomics report associated with an Individual.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='mcode.GenomicsReport')), - ('medication_statement', models.ForeignKey(blank=True, help_text='Medication treatment addressed to an Individual.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='mcode.MedicationStatement')), - ('subject', models.ForeignKey(help_text='An individual who is a subject of mcodepacket.', on_delete=django.db.models.deletion.CASCADE, to='patients.Individual')), - ], - options={ - 'ordering': ['id'], - }, - bases=(models.Model, chord_metadata_service.restapi.models.IndexableMixin), - ), - migrations.CreateModel( - name='LabsVital', - fields=[ - ('id', models.CharField(help_text='An arbitrary identifier for the labs/vital tests.', max_length=200, primary_key=True, serialize=False)), - ('body_height', django.contrib.postgres.fields.jsonb.JSONField(help_text="The patient's height.", validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:quantity_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema for the datatype Quantity.', 'properties': {'code': {'type': 'string'}, 'comparator': {'enum': ['<', '>', '<=', '>=', '=']}, 'system': {'format': 'uri', 'type': 'string'}, 'unit': {'type': 'string'}, 'value': {'type': 'number'}}, 'title': 'Quantity schema', 'type': 'object'})])), - ('body_weight', django.contrib.postgres.fields.jsonb.JSONField(help_text="The patient's weight.", validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:quantity_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema for the datatype Quantity.', 'properties': {'code': {'type': 'string'}, 'comparator': {'enum': ['<', '>', '<=', '>=', '=']}, 'system': {'format': 'uri', 'type': 'string'}, 'unit': {'type': 'string'}, 'value': {'type': 'number'}}, 'title': 'Quantity schema', 'type': 'object'})])), - ('cbc_with_auto_differential_panel', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=200), blank=True, help_text='Reference to a laboratory observation in the CBC with Auto DifferentialPanel test.', null=True, size=None)), - ('comprehensive_metabolic_2000', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=200), blank=True, help_text='Reference to a laboratory observation in the CMP 2000 test.', null=True, size=None)), - ('blood_pressure_diastolic', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The blood pressure after the contraction of the heart while the chambers of the heart refill with blood, when the pressure is lowest.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:quantity_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema for the datatype Quantity.', 'properties': {'code': {'type': 'string'}, 'comparator': {'enum': ['<', '>', '<=', '>=', '=']}, 'system': {'format': 'uri', 'type': 'string'}, 'unit': {'type': 'string'}, 'value': {'type': 'number'}}, 'title': 'Quantity schema', 'type': 'object'})])), - ('blood_pressure_systolic', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The blood pressure during the contraction of the left ventricle of the heart, when blood pressure is at its highest.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:quantity_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema for the datatype Quantity.', 'properties': {'code': {'type': 'string'}, 'comparator': {'enum': ['<', '>', '<=', '>=', '=']}, 'system': {'format': 'uri', 'type': 'string'}, 'unit': {'type': 'string'}, 'value': {'type': 'number'}}, 'title': 'Quantity schema', 'type': 'object'})])), - ('tumor_marker_test', django.contrib.postgres.fields.jsonb.JSONField(help_text='An ontology or controlled vocabulary term to identify tumor marker test.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:tumor_marker_test', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Tumor marker test schema.', 'properties': {'code': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'data_value': {'anyOf': [{'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, {'$id': 'chord_metadata_service:quantity_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema for the datatype Quantity.', 'properties': {'code': {'type': 'string'}, 'comparator': {'enum': ['<', '>', '<=', '>=', '=']}, 'system': {'format': 'uri', 'type': 'string'}, 'unit': {'type': 'string'}, 'value': {'type': 'number'}}, 'title': 'Quantity schema', 'type': 'object'}, {'$id': 'chord_metadata_service:ratio', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Ratio schema.', 'properties': {'denominator': {'$id': 'chord_metadata_service:quantity_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema for the datatype Quantity.', 'properties': {'code': {'type': 'string'}, 'comparator': {'enum': ['<', '>', '<=', '>=', '=']}, 'system': {'format': 'uri', 'type': 'string'}, 'unit': {'type': 'string'}, 'value': {'type': 'number'}}, 'title': 'Quantity schema', 'type': 'object'}, 'numerator': {'$id': 'chord_metadata_service:quantity_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema for the datatype Quantity.', 'properties': {'code': {'type': 'string'}, 'comparator': {'enum': ['<', '>', '<=', '>=', '=']}, 'system': {'format': 'uri', 'type': 'string'}, 'unit': {'type': 'string'}, 'value': {'type': 'number'}}, 'title': 'Quantity schema', 'type': 'object'}}, 'title': 'Ratio', 'type': 'object'}]}}, 'required': ['code'], 'title': 'Tumor marker test', 'type': 'object'})])), - ('individual', models.ForeignKey(help_text='The individual who is the subject of the tests.', on_delete=django.db.models.deletion.CASCADE, to='patients.Individual')), - ], - options={ - 'ordering': ['id'], - }, - bases=(models.Model, chord_metadata_service.restapi.models.IndexableMixin), - ), - ] diff --git a/chord_metadata_service/mcode/migrations/0001_v1_0_0.py b/chord_metadata_service/mcode/migrations/0001_v1_0_0.py new file mode 100644 index 000000000..06d1fd76d --- /dev/null +++ b/chord_metadata_service/mcode/migrations/0001_v1_0_0.py @@ -0,0 +1,215 @@ +# Generated by Django 2.2.13 on 2020-07-06 14:55 + +import chord_metadata_service.restapi.models +import chord_metadata_service.restapi.validators +import django.contrib.postgres.fields +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('phenopackets', '0001_v1_0_0'), + ('chord', '0001_v1_0_0'), + ('patients', '0001_v1_0_0'), + ] + + operations = [ + migrations.CreateModel( + name='CancerCondition', + fields=[ + ('id', models.CharField(help_text='An arbitrary identifier for the cancer condition.', max_length=200, primary_key=True, serialize=False)), + ('condition_type', models.CharField(choices=[('primary', 'primary'), ('secondary', 'secondary')], help_text='Cancer condition type: primary or secondary.', max_length=200)), + ('body_site', django.contrib.postgres.fields.jsonb.JSONField(help_text='Code for the body location, optionally pre-coordinating laterality or direction. Accepted ontologies: SNOMED CT, ICD-O-3 and others.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)])), + ('laterality', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Body side of the body location, if needed to distinguish from a similar location on the other side of the body.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), + ('clinical_status', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='A flag indicating whether the condition is active or inactive, recurring, in remission, or resolved (as of the last update of the Condition). Accepted code system: http://terminology.hl7.org/CodeSystem/condition-clinical', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), + ('code', django.contrib.postgres.fields.jsonb.JSONField(help_text='A code describing the type of primary or secondary malignant neoplastic disease.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), + ('date_of_diagnosis', models.DateTimeField(blank=True, help_text='The date the disease was first clinically recognized with sufficient certainty, regardless of whether it was fully characterized at that time.', null=True)), + ('histology_morphology_behavior', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='A description of the morphologic and behavioral characteristics of the cancer. Accepted ontologies: SNOMED CT, ICD-O-3 and others.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), + ('verification_status', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='A flag indicating whether the condition is unconfirmed, provisional, differential, confirmed, refuted, or entered-in-error.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), + ('extra_properties', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema.', null=True)), + ('created', models.DateTimeField(auto_now=True)), + ('updated', models.DateTimeField(auto_now_add=True)), + ], + options={ + 'ordering': ['id'], + }, + bases=(models.Model, chord_metadata_service.restapi.models.IndexableMixin), + ), + migrations.CreateModel( + name='CancerGeneticVariant', + fields=[ + ('id', models.CharField(help_text='An arbitrary identifier for the cancer genetic variant.', max_length=200, primary_key=True, serialize=False)), + ('data_value', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The overall result of the genetic test; specifically, whether a variant is present, absent, no call, or indeterminant.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), + ('method', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The method used to perform the genetic test.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), + ('amino_acid_change', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The symbolic representation of an amino acid variant reported using HGVS nomenclature (pHGVS).', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), + ('amino_acid_change_type', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The type of change related to the amino acid variant.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), + ('cytogenetic_location', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The cytogenetic (chromosome) location.', null=True)), + ('cytogenetic_nomenclature', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The cytogenetic (chromosome) location, represented using the International System for Human Cytogenetic Nomenclature (ISCN).', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), + ('genomic_dna_change', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The symbolic representation of a genetic structural variant reported using HGVS nomenclature (gHGVS).', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), + ('genomic_source_class', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The genomic class of the specimen being analyzed, for example, germline for inherited genome, somatic for cancer genome, and prenatal for fetal genome.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), + ('variation_code', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The variation ID assigned by ClinVar.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)])), + ('extra_properties', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema.', null=True)), + ('created', models.DateTimeField(auto_now=True)), + ('updated', models.DateTimeField(auto_now_add=True)), + ('gene_studied', models.ManyToManyField(blank=True, help_text='A gene targeted for mutation analysis, identified in HUGO Gene Nomenclature Committee (HGNC) notation.', to='phenopackets.Gene')), + ], + options={ + 'ordering': ['id'], + }, + bases=(models.Model, chord_metadata_service.restapi.models.IndexableMixin), + ), + migrations.CreateModel( + name='CancerRelatedProcedure', + fields=[ + ('id', models.CharField(help_text='An arbitrary identifier for the procedure.', max_length=200, primary_key=True, serialize=False)), + ('procedure_type', models.CharField(choices=[('radiation', 'radiation'), ('surgical', 'surgical')], help_text='Type of cancer related procedure: radiation or surgical.', max_length=200)), + ('code', django.contrib.postgres.fields.jsonb.JSONField(help_text='Code for the procedure performed.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), + ('body_site', django.contrib.postgres.fields.jsonb.JSONField(help_text='The body location(s) where the procedure was performed.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)])), + ('laterality', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Body side of the body location, if needed to distinguish from a similar location on the other side of the body.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), + ('treatment_intent', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The purpose of a treatment.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), + ('reason_code', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The explanation or justification for why the surgical procedure was performed.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), + ('extra_properties', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema.', null=True)), + ('created', models.DateTimeField(auto_now=True)), + ('updated', models.DateTimeField(auto_now_add=True)), + ('reason_reference', models.ManyToManyField(blank=True, help_text='Reference to a primary or secondary cancer condition.', to='mcode.CancerCondition')), + ], + options={ + 'ordering': ['id'], + }, + bases=(models.Model, chord_metadata_service.restapi.models.IndexableMixin), + ), + migrations.CreateModel( + name='GeneticSpecimen', + fields=[ + ('id', models.CharField(help_text='An arbitrary identifier for the genetic specimen.', max_length=200, primary_key=True, serialize=False)), + ('specimen_type', django.contrib.postgres.fields.jsonb.JSONField(help_text='The kind of material that forms the specimen.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), + ('collection_body', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The anatomical collection site.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), + ('laterality', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Body side of the collection site, if needed to distinguish from a similar location on the other side of the body.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), + ('extra_properties', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema.', null=True)), + ('created', models.DateTimeField(auto_now=True)), + ('updated', models.DateTimeField(auto_now_add=True)), + ], + options={ + 'ordering': ['id'], + }, + bases=(models.Model, chord_metadata_service.restapi.models.IndexableMixin), + ), + migrations.CreateModel( + name='GenomicRegionStudied', + fields=[ + ('id', models.CharField(help_text='An arbitrary identifier for the genomic region studied.', max_length=200, primary_key=True, serialize=False)), + ('dna_ranges_examined', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The range(s) of the DNA sequence examined.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)])), + ('dna_region_description', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(help_text='The description for the DNA region studied in the genomics report.', max_length=100), blank=True, default=list, size=None)), + ('gene_mutation', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The gene mutations tested for in blood or tissue by molecular genetics methods.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)])), + ('gene_studied', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The ID for the gene studied.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)])), + ('genomic_reference_sequence_id', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Range(s) of DNA sequence examined.', null=True)), + ('genomic_region_coordinate_system', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The method of counting along the genome.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), + ('extra_properties', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema.', null=True)), + ('created', models.DateTimeField(auto_now=True)), + ('updated', models.DateTimeField(auto_now_add=True)), + ], + options={ + 'ordering': ['id'], + }, + bases=(models.Model, chord_metadata_service.restapi.models.IndexableMixin), + ), + migrations.CreateModel( + name='GenomicsReport', + fields=[ + ('id', models.CharField(help_text='An arbitrary identifier for the genetics report.', max_length=200, primary_key=True, serialize=False)), + ('code', django.contrib.postgres.fields.jsonb.JSONField(help_text='An ontology or controlled vocabulary term to identify the laboratory test. Accepted value sets: LOINC, GTR.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), + ('performing_organization_name', models.CharField(blank=True, help_text='The name of the organization producing the genomics report.', max_length=200)), + ('issued', models.DateTimeField(default=django.utils.timezone.now, help_text='The date/time this report was issued.')), + ('extra_properties', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema.', null=True)), + ('created', models.DateTimeField(auto_now=True)), + ('updated', models.DateTimeField(auto_now_add=True)), + ('genetic_specimen', models.ManyToManyField(blank=True, help_text='List of related genetic specimens.', to='mcode.GeneticSpecimen')), + ('genetic_variant', models.ForeignKey(blank=True, help_text='Related genetic variant.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='mcode.CancerGeneticVariant')), + ('genomic_region_studied', models.ForeignKey(blank=True, help_text='Related genomic region studied.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='mcode.GenomicRegionStudied')), + ], + options={ + 'ordering': ['id'], + }, + bases=(models.Model, chord_metadata_service.restapi.models.IndexableMixin), + ), + migrations.CreateModel( + name='MedicationStatement', + fields=[ + ('id', models.CharField(help_text='An arbitrary identifier for the medication statement.', max_length=200, primary_key=True, serialize=False)), + ('medication_code', django.contrib.postgres.fields.jsonb.JSONField(help_text='A code for medication. Accepted code systems: Medication Clinical Drug (RxNorm) and other.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), + ('termination_reason', django.contrib.postgres.fields.jsonb.JSONField(help_text='A code explaining unplanned or premature termination of a course of medication. Accepted ontologies: SNOMED CT.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)])), + ('treatment_intent', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The purpose of a treatment. Accepted ontologies: SNOMED CT.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), + ('start_date', models.DateTimeField(blank=True, help_text='The start date/time of the medication.', null=True)), + ('end_date', models.DateTimeField(blank=True, help_text='The end date/time of the medication.', null=True)), + ('extra_properties', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema.', null=True)), + ('created', models.DateTimeField(auto_now=True)), + ('updated', models.DateTimeField(auto_now_add=True)), + ], + options={ + 'ordering': ['id'], + }, + bases=(models.Model, chord_metadata_service.restapi.models.IndexableMixin), + ), + migrations.CreateModel( + name='TNMStaging', + fields=[ + ('id', models.CharField(help_text='An arbitrary identifier for the TNM staging.', max_length=200, primary_key=True, serialize=False)), + ('tnm_type', models.CharField(choices=[('clinical', 'clinical'), ('pathologic', 'pathologic')], help_text='TNM type: clinical or pathological.', max_length=200)), + ('stage_group', django.contrib.postgres.fields.jsonb.JSONField(help_text='The extent of the cancer in the body, according to the TNM classification system.Accepted ontologies: SNOMED CT, AJCC and others.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:complex_ontology_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Complex object to combine data value and staging system.', 'properties': {'data_value': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'staging_system': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}}, 'required': ['data_value'], 'title': 'Complex ontology', 'type': 'object'}, formats=['uri'])])), + ('primary_tumor_category', django.contrib.postgres.fields.jsonb.JSONField(help_text='Category of the primary tumor, based on its size and extent. Accepted ontologies: SNOMED CT, AJCC and others.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:complex_ontology_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Complex object to combine data value and staging system.', 'properties': {'data_value': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'staging_system': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}}, 'required': ['data_value'], 'title': 'Complex ontology', 'type': 'object'}, formats=['uri'])])), + ('regional_nodes_category', django.contrib.postgres.fields.jsonb.JSONField(help_text='Category of the presence or absence of metastases in regional lymph nodes. Accepted ontologies: SNOMED CT, AJCC and others.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:complex_ontology_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Complex object to combine data value and staging system.', 'properties': {'data_value': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'staging_system': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}}, 'required': ['data_value'], 'title': 'Complex ontology', 'type': 'object'}, formats=['uri'])])), + ('distant_metastases_category', django.contrib.postgres.fields.jsonb.JSONField(help_text='Category describing the presence or absence of metastases in remote anatomical locations. Accepted ontologies: SNOMED CT, AJCC and others.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:complex_ontology_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Complex object to combine data value and staging system.', 'properties': {'data_value': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'staging_system': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}}, 'required': ['data_value'], 'title': 'Complex ontology', 'type': 'object'}, formats=['uri'])])), + ('extra_properties', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema.', null=True)), + ('created', models.DateTimeField(auto_now=True)), + ('updated', models.DateTimeField(auto_now_add=True)), + ('cancer_condition', models.ForeignKey(help_text='Cancer condition.', on_delete=django.db.models.deletion.CASCADE, to='mcode.CancerCondition')), + ], + options={ + 'ordering': ['id'], + }, + bases=(models.Model, chord_metadata_service.restapi.models.IndexableMixin), + ), + migrations.CreateModel( + name='MCodePacket', + fields=[ + ('id', models.CharField(help_text='An arbitrary identifier for the mcodepacket.', max_length=200, primary_key=True, serialize=False)), + ('date_of_death', models.CharField(blank=True, help_text='An indication that the patient is no longer living, given by a date of death or boolean.', max_length=200)), + ('cancer_disease_status', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text="A clinician's qualitative judgment on the current trend of the cancer, e.g., whether it is stable, worsening (progressing), or improving (responding).", null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), + ('extra_properties', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema.', null=True)), + ('created', models.DateTimeField(auto_now=True)), + ('updated', models.DateTimeField(auto_now_add=True)), + ('cancer_condition', models.ManyToManyField(blank=True, help_text="An Individual's cancer condition.", to='mcode.CancerCondition')), + ('cancer_related_procedures', models.ManyToManyField(blank=True, help_text='A radiological or surgical procedures addressing a cancer condition.', to='mcode.CancerRelatedProcedure')), + ('genomics_report', models.ForeignKey(blank=True, help_text='A genomics report associated with an Individual.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='mcode.GenomicsReport')), + ('medication_statement', models.ManyToManyField(blank=True, help_text='Medication treatment addressed to an Individual.', to='mcode.MedicationStatement')), + ('subject', models.ForeignKey(help_text='An individual who is a subject of mcodepacket.', on_delete=django.db.models.deletion.CASCADE, to='patients.Individual')), + ('table', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='chord.Table')), + ], + options={ + 'ordering': ['id'], + }, + bases=(models.Model, chord_metadata_service.restapi.models.IndexableMixin), + ), + migrations.CreateModel( + name='LabsVital', + fields=[ + ('id', models.CharField(help_text='An arbitrary identifier for the labs/vital tests.', max_length=200, primary_key=True, serialize=False)), + ('tumor_marker_code', django.contrib.postgres.fields.jsonb.JSONField(help_text='A code identifying the type of tumor marker test.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), + ('tumor_marker_data_value', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The result of a tumor marker test.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:tumor_marker_data_value', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Tumor marker data value schema.', 'properties': {'value': {'anyOf': [{'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, {'$id': 'chord_metadata_service:quantity_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema for the datatype Quantity.', 'properties': {'code': {'type': 'string'}, 'comparator': {'enum': ['<', '>', '<=', '>=', '=']}, 'system': {'format': 'uri', 'type': 'string'}, 'unit': {'type': 'string'}, 'value': {'type': 'number'}}, 'title': 'Quantity schema', 'type': 'object'}, {'$id': 'chord_metadata_service:ratio', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Ratio schema.', 'properties': {'denominator': {'$id': 'chord_metadata_service:quantity_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema for the datatype Quantity.', 'properties': {'code': {'type': 'string'}, 'comparator': {'enum': ['<', '>', '<=', '>=', '=']}, 'system': {'format': 'uri', 'type': 'string'}, 'unit': {'type': 'string'}, 'value': {'type': 'number'}}, 'title': 'Quantity schema', 'type': 'object'}, 'numerator': {'$id': 'chord_metadata_service:quantity_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema for the datatype Quantity.', 'properties': {'code': {'type': 'string'}, 'comparator': {'enum': ['<', '>', '<=', '>=', '=']}, 'system': {'format': 'uri', 'type': 'string'}, 'unit': {'type': 'string'}, 'value': {'type': 'number'}}, 'title': 'Quantity schema', 'type': 'object'}}, 'title': 'Ratio', 'type': 'object'}]}}, 'title': 'Tumor marker data value', 'type': 'object'}, formats=None)])), + ('extra_properties', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema.', null=True)), + ('created', models.DateTimeField(auto_now=True)), + ('updated', models.DateTimeField(auto_now_add=True)), + ('individual', models.ForeignKey(help_text='The individual who is the subject of the tests.', on_delete=django.db.models.deletion.CASCADE, to='patients.Individual')), + ], + options={ + 'ordering': ['id'], + }, + bases=(models.Model, chord_metadata_service.restapi.models.IndexableMixin), + ), + ] diff --git a/chord_metadata_service/mcode/migrations/0002_auto_20200401_1008.py b/chord_metadata_service/mcode/migrations/0002_auto_20200401_1008.py deleted file mode 100644 index 9756f449e..000000000 --- a/chord_metadata_service/mcode/migrations/0002_auto_20200401_1008.py +++ /dev/null @@ -1,159 +0,0 @@ -# Generated by Django 2.2.10 on 2020-04-01 14:08 - -import django.contrib.postgres.fields.jsonb -from django.db import migrations, models -import django.utils.timezone - - -class Migration(migrations.Migration): - - dependencies = [ - ('mcode', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='cancercondition', - name='created', - field=models.DateTimeField(auto_now=True), - ), - migrations.AddField( - model_name='cancercondition', - name='extra_properties', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema.', null=True), - ), - migrations.AddField( - model_name='cancercondition', - name='updated', - field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), - preserve_default=False, - ), - migrations.AddField( - model_name='cancerrelatedprocedure', - name='created', - field=models.DateTimeField(auto_now=True), - ), - migrations.AddField( - model_name='cancerrelatedprocedure', - name='extra_properties', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema.', null=True), - ), - migrations.AddField( - model_name='cancerrelatedprocedure', - name='updated', - field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), - preserve_default=False, - ), - migrations.AddField( - model_name='geneticvariantfound', - name='created', - field=models.DateTimeField(auto_now=True), - ), - migrations.AddField( - model_name='geneticvariantfound', - name='extra_properties', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema.', null=True), - ), - migrations.AddField( - model_name='geneticvariantfound', - name='updated', - field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), - preserve_default=False, - ), - migrations.AddField( - model_name='geneticvarianttested', - name='created', - field=models.DateTimeField(auto_now=True), - ), - migrations.AddField( - model_name='geneticvarianttested', - name='extra_properties', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema.', null=True), - ), - migrations.AddField( - model_name='geneticvarianttested', - name='updated', - field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), - preserve_default=False, - ), - migrations.AddField( - model_name='genomicsreport', - name='created', - field=models.DateTimeField(auto_now=True), - ), - migrations.AddField( - model_name='genomicsreport', - name='extra_properties', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema.', null=True), - ), - migrations.AddField( - model_name='genomicsreport', - name='updated', - field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), - preserve_default=False, - ), - migrations.AddField( - model_name='labsvital', - name='created', - field=models.DateTimeField(auto_now=True), - ), - migrations.AddField( - model_name='labsvital', - name='extra_properties', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema.', null=True), - ), - migrations.AddField( - model_name='labsvital', - name='updated', - field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), - preserve_default=False, - ), - migrations.AddField( - model_name='mcodepacket', - name='created', - field=models.DateTimeField(auto_now=True), - ), - migrations.AddField( - model_name='mcodepacket', - name='extra_properties', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema.', null=True), - ), - migrations.AddField( - model_name='mcodepacket', - name='updated', - field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), - preserve_default=False, - ), - migrations.AddField( - model_name='medicationstatement', - name='created', - field=models.DateTimeField(auto_now=True), - ), - migrations.AddField( - model_name='medicationstatement', - name='extra_properties', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema.', null=True), - ), - migrations.AddField( - model_name='medicationstatement', - name='updated', - field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), - preserve_default=False, - ), - migrations.AddField( - model_name='tnmstaging', - name='created', - field=models.DateTimeField(auto_now=True), - ), - migrations.AddField( - model_name='tnmstaging', - name='extra_properties', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema.', null=True), - ), - migrations.AddField( - model_name='tnmstaging', - name='updated', - field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), - preserve_default=False, - ), - ] diff --git a/chord_metadata_service/mcode/migrations/0003_auto_20200513_1401.py b/chord_metadata_service/mcode/migrations/0003_auto_20200513_1401.py deleted file mode 100644 index 74eb69de3..000000000 --- a/chord_metadata_service/mcode/migrations/0003_auto_20200513_1401.py +++ /dev/null @@ -1,75 +0,0 @@ -# Generated by Django 2.2.12 on 2020-05-13 18:01 - -import chord_metadata_service.restapi.validators -import django.contrib.postgres.fields.jsonb -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('mcode', '0002_auto_20200401_1008'), - ] - - operations = [ - migrations.AlterField( - model_name='cancercondition', - name='body_location_code', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text='Code for the body location, optionally pre-coordinating laterality or direction. Accepted ontologies: SNOMED CT, ICD-O-3 and others.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)]), - ), - migrations.AlterField( - model_name='cancerrelatedprocedure', - name='occurence_time_or_period', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text='The date/time that a procedure was performed.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:time_or_period', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Time of Period schema.', 'properties': {'value': {'anyOf': [{'format': 'date-time', 'type': 'string'}, {'$id': 'chord_metadata_service:period_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Period schema.', 'properties': {'end': {'format': 'date-time', 'type': 'string'}, 'start': {'format': 'date-time', 'type': 'string'}}, 'title': 'Period', 'type': 'object'}]}}, 'title': 'Time of Period', 'type': 'object'}, formats=['date-time'])]), - ), - migrations.AlterField( - model_name='cancerrelatedprocedure', - name='target_body_site', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text='The body location(s) where the procedure was performed.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)]), - ), - migrations.AlterField( - model_name='labsvital', - name='blood_pressure_diastolic', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The blood pressure after the contraction of the heart while the chambers of the heart refill with blood, when the pressure is lowest.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:quantity_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema for the datatype Quantity.', 'properties': {'code': {'type': 'string'}, 'comparator': {'enum': ['<', '>', '<=', '>=', '=']}, 'system': {'format': 'uri', 'type': 'string'}, 'unit': {'type': 'string'}, 'value': {'type': 'number'}}, 'title': 'Quantity schema', 'type': 'object'}, formats=['uri'])]), - ), - migrations.AlterField( - model_name='labsvital', - name='blood_pressure_systolic', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The blood pressure during the contraction of the left ventricle of the heart, when blood pressure is at its highest.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:quantity_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema for the datatype Quantity.', 'properties': {'code': {'type': 'string'}, 'comparator': {'enum': ['<', '>', '<=', '>=', '=']}, 'system': {'format': 'uri', 'type': 'string'}, 'unit': {'type': 'string'}, 'value': {'type': 'number'}}, 'title': 'Quantity schema', 'type': 'object'}, formats=['uri'])]), - ), - migrations.AlterField( - model_name='labsvital', - name='body_height', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text="The patient's height.", validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:quantity_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema for the datatype Quantity.', 'properties': {'code': {'type': 'string'}, 'comparator': {'enum': ['<', '>', '<=', '>=', '=']}, 'system': {'format': 'uri', 'type': 'string'}, 'unit': {'type': 'string'}, 'value': {'type': 'number'}}, 'title': 'Quantity schema', 'type': 'object'}, formats=['uri'])]), - ), - migrations.AlterField( - model_name='labsvital', - name='body_weight', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text="The patient's weight.", validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:quantity_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema for the datatype Quantity.', 'properties': {'code': {'type': 'string'}, 'comparator': {'enum': ['<', '>', '<=', '>=', '=']}, 'system': {'format': 'uri', 'type': 'string'}, 'unit': {'type': 'string'}, 'value': {'type': 'number'}}, 'title': 'Quantity schema', 'type': 'object'}, formats=['uri'])]), - ), - migrations.AlterField( - model_name='medicationstatement', - name='termination_reason', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text='A code explaining unplanned or premature termination of a course of medication. Accepted ontologies: SNOMED CT.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)]), - ), - migrations.AlterField( - model_name='tnmstaging', - name='distant_metastases_category', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text='Category describing the presence or absence of metastases in remote anatomical locations. Accepted ontologies: SNOMED CT, AJCC and others.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:complex_ontology_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Complex object to combine data value and staging system.', 'properties': {'data_value': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'staging_system': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}}, 'required': ['data_value'], 'title': 'Complex ontology', 'type': 'object'}, formats=['uri'])]), - ), - migrations.AlterField( - model_name='tnmstaging', - name='primary_tumor_category', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text='Category of the primary tumor, based on its size and extent. Accepted ontologies: SNOMED CT, AJCC and others.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:complex_ontology_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Complex object to combine data value and staging system.', 'properties': {'data_value': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'staging_system': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}}, 'required': ['data_value'], 'title': 'Complex ontology', 'type': 'object'}, formats=['uri'])]), - ), - migrations.AlterField( - model_name='tnmstaging', - name='regional_nodes_category', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text='Category of the presence or absence of metastases in regional lymph nodes. Accepted ontologies: SNOMED CT, AJCC and others.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:complex_ontology_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Complex object to combine data value and staging system.', 'properties': {'data_value': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'staging_system': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}}, 'required': ['data_value'], 'title': 'Complex ontology', 'type': 'object'}, formats=['uri'])]), - ), - migrations.AlterField( - model_name='tnmstaging', - name='stage_group', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text='The extent of the cancer in the body, according to the TNM classification system.Accepted ontologies: SNOMED CT, AJCC and others.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:complex_ontology_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Complex object to combine data value and staging system.', 'properties': {'data_value': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'staging_system': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}}, 'required': ['data_value'], 'title': 'Complex ontology', 'type': 'object'}, formats=['uri'])]), - ), - ] diff --git a/chord_metadata_service/mcode/migrations/0004_auto_20200514_1541.py b/chord_metadata_service/mcode/migrations/0004_auto_20200514_1541.py deleted file mode 100644 index 8d880bb51..000000000 --- a/chord_metadata_service/mcode/migrations/0004_auto_20200514_1541.py +++ /dev/null @@ -1,130 +0,0 @@ -# Generated by Django 2.2.12 on 2020-05-14 19:41 - -import chord_metadata_service.restapi.validators -import django.contrib.postgres.fields.jsonb -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('mcode', '0003_auto_20200513_1401'), - ] - - operations = [ - migrations.AlterField( - model_name='cancercondition', - name='body_location_code', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text='Code for the body location, optionally pre-coordinating laterality or direction. Accepted ontologies: SNOMED CT, ICD-O-3 and others.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)]), - ), - migrations.AlterField( - model_name='cancercondition', - name='clinical_status', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='A flag indicating whether the condition is active or inactive, recurring, in remission, or resolved (as of the last update of the Condition). Accepted code system: http://terminology.hl7.org/CodeSystem/condition-clinical', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='cancercondition', - name='condition_code', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text='A code describing the type of primary or secondary malignant neoplastic disease.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='cancercondition', - name='histology_morphology_behavior', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='A description of the morphologic and behavioral characteristics of the cancer. Accepted ontologies: SNOMED CT, ICD-O-3 and others.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='cancerrelatedprocedure', - name='code', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text='Code for the procedure performed.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='cancerrelatedprocedure', - name='target_body_site', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text='The body location(s) where the procedure was performed.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)]), - ), - migrations.AlterField( - model_name='cancerrelatedprocedure', - name='treatment_intent', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The purpose of a treatment.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='geneticvariantfound', - name='genomic_source_class', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology or controlled vocabulary term to identify the genomic class of the specimen being analyzed.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='geneticvariantfound', - name='method', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology or controlled vocabulary term to identify the method used to perform the genetic test. Accepted value set: NCIT.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='geneticvariantfound', - name='variant_found_identifier', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The variation ID assigned by HGVS, for example, 360448 is the identifier for NM_005228.4(EGFR):c.-237A>G (single nucleotide variant in EGFR). Accepted value set: ClinVar.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='geneticvarianttested', - name='data_value', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology or controlled vocabulary term to identify positive or negative value forthe mutation. Accepted value set: SNOMED CT.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='geneticvarianttested', - name='method', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology or controlled vocabulary term to identify the method used to perform the genetic test. Accepted value set: NCIT.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='geneticvarianttested', - name='variant_tested_identifier', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The variation ID assigned by HGVS, for example, 360448 is the identifier for NM_005228.4(EGFR):c.-237A>G (single nucleotide variant in EGFR).', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='genomicsreport', - name='specimen_type', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology or controlled vocabulary term to identify the type of material the specimen contains or consists of. Accepted value set: HL7 Version 2 and Specimen Type.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='genomicsreport', - name='test_name', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text='An ontology or controlled vocabulary term to identify the laboratory test. Accepted value sets: LOINC, GTR.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='labsvital', - name='tumor_marker_test', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text='An ontology or controlled vocabulary term to identify tumor marker test.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:tumor_marker_test', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Tumor marker test schema.', 'properties': {'code': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'data_value': {'anyOf': [{'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, {'$id': 'chord_metadata_service:quantity_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema for the datatype Quantity.', 'properties': {'code': {'type': 'string'}, 'comparator': {'enum': ['<', '>', '<=', '>=', '=']}, 'system': {'format': 'uri', 'type': 'string'}, 'unit': {'type': 'string'}, 'value': {'type': 'number'}}, 'title': 'Quantity schema', 'type': 'object'}, {'$id': 'chord_metadata_service:ratio', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Ratio schema.', 'properties': {'denominator': {'$id': 'chord_metadata_service:quantity_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema for the datatype Quantity.', 'properties': {'code': {'type': 'string'}, 'comparator': {'enum': ['<', '>', '<=', '>=', '=']}, 'system': {'format': 'uri', 'type': 'string'}, 'unit': {'type': 'string'}, 'value': {'type': 'number'}}, 'title': 'Quantity schema', 'type': 'object'}, 'numerator': {'$id': 'chord_metadata_service:quantity_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema for the datatype Quantity.', 'properties': {'code': {'type': 'string'}, 'comparator': {'enum': ['<', '>', '<=', '>=', '=']}, 'system': {'format': 'uri', 'type': 'string'}, 'unit': {'type': 'string'}, 'value': {'type': 'number'}}, 'title': 'Quantity schema', 'type': 'object'}}, 'title': 'Ratio', 'type': 'object'}]}}, 'required': ['code'], 'title': 'Tumor marker test', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='medicationstatement', - name='medication_code', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text='A code for medication. Accepted code systems: Medication Clinical Drug (RxNorm) and other.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='medicationstatement', - name='termination_reason', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text='A code explaining unplanned or premature termination of a course of medication. Accepted ontologies: SNOMED CT.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)]), - ), - migrations.AlterField( - model_name='medicationstatement', - name='treatment_intent', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The purpose of a treatment. Accepted ontologies: SNOMED CT.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='tnmstaging', - name='distant_metastases_category', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text='Category describing the presence or absence of metastases in remote anatomical locations. Accepted ontologies: SNOMED CT, AJCC and others.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:complex_ontology_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Complex object to combine data value and staging system.', 'properties': {'data_value': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'staging_system': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}}, 'required': ['data_value'], 'title': 'Complex ontology', 'type': 'object'}, formats=['uri'])]), - ), - migrations.AlterField( - model_name='tnmstaging', - name='primary_tumor_category', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text='Category of the primary tumor, based on its size and extent. Accepted ontologies: SNOMED CT, AJCC and others.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:complex_ontology_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Complex object to combine data value and staging system.', 'properties': {'data_value': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'staging_system': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}}, 'required': ['data_value'], 'title': 'Complex ontology', 'type': 'object'}, formats=['uri'])]), - ), - migrations.AlterField( - model_name='tnmstaging', - name='regional_nodes_category', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text='Category of the presence or absence of metastases in regional lymph nodes. Accepted ontologies: SNOMED CT, AJCC and others.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:complex_ontology_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Complex object to combine data value and staging system.', 'properties': {'data_value': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'staging_system': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}}, 'required': ['data_value'], 'title': 'Complex ontology', 'type': 'object'}, formats=['uri'])]), - ), - migrations.AlterField( - model_name='tnmstaging', - name='stage_group', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text='The extent of the cancer in the body, according to the TNM classification system.Accepted ontologies: SNOMED CT, AJCC and others.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:complex_ontology_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Complex object to combine data value and staging system.', 'properties': {'data_value': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'staging_system': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}}, 'required': ['data_value'], 'title': 'Complex ontology', 'type': 'object'}, formats=['uri'])]), - ), - ] diff --git a/chord_metadata_service/mcode/migrations/0005_auto_20200519_1538.py b/chord_metadata_service/mcode/migrations/0005_auto_20200519_1538.py deleted file mode 100644 index bee3527e6..000000000 --- a/chord_metadata_service/mcode/migrations/0005_auto_20200519_1538.py +++ /dev/null @@ -1,130 +0,0 @@ -# Generated by Django 2.2.12 on 2020-05-19 15:38 - -import chord_metadata_service.restapi.validators -import django.contrib.postgres.fields.jsonb -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('mcode', '0004_auto_20200514_1541'), - ] - - operations = [ - migrations.AlterField( - model_name='cancercondition', - name='body_location_code', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text='Code for the body location, optionally pre-coordinating laterality or direction. Accepted ontologies: SNOMED CT, ICD-O-3 and others.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)]), - ), - migrations.AlterField( - model_name='cancercondition', - name='clinical_status', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='A flag indicating whether the condition is active or inactive, recurring, in remission, or resolved (as of the last update of the Condition). Accepted code system: http://terminology.hl7.org/CodeSystem/condition-clinical', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='cancercondition', - name='condition_code', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text='A code describing the type of primary or secondary malignant neoplastic disease.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='cancercondition', - name='histology_morphology_behavior', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='A description of the morphologic and behavioral characteristics of the cancer. Accepted ontologies: SNOMED CT, ICD-O-3 and others.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='cancerrelatedprocedure', - name='code', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text='Code for the procedure performed.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='cancerrelatedprocedure', - name='target_body_site', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text='The body location(s) where the procedure was performed.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)]), - ), - migrations.AlterField( - model_name='cancerrelatedprocedure', - name='treatment_intent', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The purpose of a treatment.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='geneticvariantfound', - name='genomic_source_class', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology or controlled vocabulary term to identify the genomic class of the specimen being analyzed.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='geneticvariantfound', - name='method', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology or controlled vocabulary term to identify the method used to perform the genetic test. Accepted value set: NCIT.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='geneticvariantfound', - name='variant_found_identifier', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The variation ID assigned by HGVS, for example, 360448 is the identifier for NM_005228.4(EGFR):c.-237A>G (single nucleotide variant in EGFR). Accepted value set: ClinVar.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='geneticvarianttested', - name='data_value', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology or controlled vocabulary term to identify positive or negative value forthe mutation. Accepted value set: SNOMED CT.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='geneticvarianttested', - name='method', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology or controlled vocabulary term to identify the method used to perform the genetic test. Accepted value set: NCIT.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='geneticvarianttested', - name='variant_tested_identifier', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The variation ID assigned by HGVS, for example, 360448 is the identifier for NM_005228.4(EGFR):c.-237A>G (single nucleotide variant in EGFR).', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='genomicsreport', - name='specimen_type', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology or controlled vocabulary term to identify the type of material the specimen contains or consists of. Accepted value set: HL7 Version 2 and Specimen Type.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='genomicsreport', - name='test_name', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text='An ontology or controlled vocabulary term to identify the laboratory test. Accepted value sets: LOINC, GTR.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='labsvital', - name='tumor_marker_test', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text='An ontology or controlled vocabulary term to identify tumor marker test.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:tumor_marker_test', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Tumor marker test schema.', 'properties': {'code': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'data_value': {'anyOf': [{'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, {'$id': 'chord_metadata_service:quantity_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema for the datatype Quantity.', 'properties': {'code': {'type': 'string'}, 'comparator': {'enum': ['<', '>', '<=', '>=', '=']}, 'system': {'format': 'uri', 'type': 'string'}, 'unit': {'type': 'string'}, 'value': {'type': 'number'}}, 'title': 'Quantity schema', 'type': 'object'}, {'$id': 'chord_metadata_service:ratio', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Ratio schema.', 'properties': {'denominator': {'$id': 'chord_metadata_service:quantity_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema for the datatype Quantity.', 'properties': {'code': {'type': 'string'}, 'comparator': {'enum': ['<', '>', '<=', '>=', '=']}, 'system': {'format': 'uri', 'type': 'string'}, 'unit': {'type': 'string'}, 'value': {'type': 'number'}}, 'title': 'Quantity schema', 'type': 'object'}, 'numerator': {'$id': 'chord_metadata_service:quantity_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema for the datatype Quantity.', 'properties': {'code': {'type': 'string'}, 'comparator': {'enum': ['<', '>', '<=', '>=', '=']}, 'system': {'format': 'uri', 'type': 'string'}, 'unit': {'type': 'string'}, 'value': {'type': 'number'}}, 'title': 'Quantity schema', 'type': 'object'}}, 'title': 'Ratio', 'type': 'object'}]}}, 'required': ['code'], 'title': 'Tumor marker test', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='medicationstatement', - name='medication_code', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text='A code for medication. Accepted code systems: Medication Clinical Drug (RxNorm) and other.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='medicationstatement', - name='termination_reason', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text='A code explaining unplanned or premature termination of a course of medication. Accepted ontologies: SNOMED CT.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)]), - ), - migrations.AlterField( - model_name='medicationstatement', - name='treatment_intent', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The purpose of a treatment. Accepted ontologies: SNOMED CT.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='tnmstaging', - name='distant_metastases_category', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text='Category describing the presence or absence of metastases in remote anatomical locations. Accepted ontologies: SNOMED CT, AJCC and others.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:complex_ontology_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Complex object to combine data value and staging system.', 'properties': {'data_value': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'staging_system': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}}, 'required': ['data_value'], 'title': 'Complex ontology', 'type': 'object'}, formats=['uri'])]), - ), - migrations.AlterField( - model_name='tnmstaging', - name='primary_tumor_category', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text='Category of the primary tumor, based on its size and extent. Accepted ontologies: SNOMED CT, AJCC and others.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:complex_ontology_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Complex object to combine data value and staging system.', 'properties': {'data_value': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'staging_system': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}}, 'required': ['data_value'], 'title': 'Complex ontology', 'type': 'object'}, formats=['uri'])]), - ), - migrations.AlterField( - model_name='tnmstaging', - name='regional_nodes_category', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text='Category of the presence or absence of metastases in regional lymph nodes. Accepted ontologies: SNOMED CT, AJCC and others.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:complex_ontology_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Complex object to combine data value and staging system.', 'properties': {'data_value': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'staging_system': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}}, 'required': ['data_value'], 'title': 'Complex ontology', 'type': 'object'}, formats=['uri'])]), - ), - migrations.AlterField( - model_name='tnmstaging', - name='stage_group', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text='The extent of the cancer in the body, according to the TNM classification system.Accepted ontologies: SNOMED CT, AJCC and others.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:complex_ontology_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Complex object to combine data value and staging system.', 'properties': {'data_value': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'staging_system': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}}, 'required': ['data_value'], 'title': 'Complex ontology', 'type': 'object'}, formats=['uri'])]), - ), - ] diff --git a/chord_metadata_service/mcode/migrations/0006_auto_20200610_2248.py b/chord_metadata_service/mcode/migrations/0006_auto_20200610_2248.py deleted file mode 100644 index a7c60bd94..000000000 --- a/chord_metadata_service/mcode/migrations/0006_auto_20200610_2248.py +++ /dev/null @@ -1,227 +0,0 @@ -# Generated by Django 2.2.13 on 2020-06-11 02:48 - -import chord_metadata_service.restapi.models -import chord_metadata_service.restapi.validators -import datetime -import django.contrib.postgres.fields -import django.contrib.postgres.fields.jsonb -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('phenopackets', '0012_auto_20200525_2116'), - ('mcode', '0005_auto_20200519_1538'), - ] - - operations = [ - migrations.CreateModel( - name='CancerGeneticVariant', - fields=[ - ('id', models.CharField(help_text='An arbitrary identifier for the cancer genetic variant.', max_length=200, primary_key=True, serialize=False)), - ('data_value', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The overall result of the genetic test; specifically, whether a variant is present, absent, no call, or indeterminant.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), - ('method', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The method used to perform the genetic test.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), - ('amino_acid_change', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The symbolic representation of an amino acid variant reported using HGVS nomenclature (pHGVS).', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), - ('amino_acid_change_type', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The type of change related to the amino acid variant.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), - ('cytogenetic_location', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The cytogenetic (chromosome) location.', null=True)), - ('cytogenetic_nomenclature', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The cytogenetic (chromosome) location, represented using the International System for Human Cytogenetic Nomenclature (ISCN).', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), - ('genomic_dna_change', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The symbolic representation of a genetic structural variant reported using HGVS nomenclature (gHGVS).', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), - ('genomic_source_class', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The genomic class of the specimen being analyzed, for example, germline for inherited genome, somatic for cancer genome, and prenatal for fetal genome.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), - ('variation_code', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The variation ID assigned by ClinVar.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)])), - ('extra_properties', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema.', null=True)), - ('created', models.DateTimeField(auto_now=True)), - ('updated', models.DateTimeField(auto_now_add=True)), - ('gene_studied', models.ManyToManyField(blank=True, help_text='A gene targeted for mutation analysis, identified in HUGO Gene Nomenclature Committee (HGNC) notation.', to='phenopackets.Gene')), - ], - options={ - 'ordering': ['id'], - }, - bases=(models.Model, chord_metadata_service.restapi.models.IndexableMixin), - ), - migrations.CreateModel( - name='GeneticSpecimen', - fields=[ - ('id', models.CharField(help_text='An arbitrary identifier for the genetic specimen.', max_length=200, primary_key=True, serialize=False)), - ('specimen_type', django.contrib.postgres.fields.jsonb.JSONField(help_text='The kind of material that forms the specimen.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), - ('collection_body', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The anatomical collection site.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), - ('laterality', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Body side of the collection site, if needed to distinguish from a similar location on the other side of the body.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), - ('extra_properties', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema.', null=True)), - ('created', models.DateTimeField(auto_now=True)), - ('updated', models.DateTimeField(auto_now_add=True)), - ], - options={ - 'ordering': ['id'], - }, - bases=(models.Model, chord_metadata_service.restapi.models.IndexableMixin), - ), - migrations.CreateModel( - name='GenomicRegionStudied', - fields=[ - ('id', models.CharField(help_text='An arbitrary identifier for the genomic region studied.', max_length=200, primary_key=True, serialize=False)), - ('dna_ranges_examined', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The range(s) of the DNA sequence examined.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)])), - ('dna_region_description', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(help_text='The description for the DNA region studied in the genomics report.', max_length=100), blank=True, default=list, size=None)), - ('gene_mutation', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The gene mutations tested for in blood or tissue by molecular genetics methods.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)])), - ('gene_studied', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The ID for the gene studied.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)])), - ('genomic_reference_sequence_id', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Range(s) of DNA sequence examined.', null=True)), - ('genomic_region_coordinate_system', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The method of counting along the genome.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), - ('extra_properties', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema.', null=True)), - ('created', models.DateTimeField(auto_now=True)), - ('updated', models.DateTimeField(auto_now_add=True)), - ], - options={ - 'ordering': ['id'], - }, - bases=(models.Model, chord_metadata_service.restapi.models.IndexableMixin), - ), - migrations.RemoveField( - model_name='geneticvarianttested', - name='gene_studied', - ), - migrations.RenameField( - model_name='cancercondition', - old_name='body_location_code', - new_name='body_site', - ), - migrations.RenameField( - model_name='cancercondition', - old_name='condition_code', - new_name='code', - ), - migrations.RenameField( - model_name='cancerrelatedprocedure', - old_name='target_body_site', - new_name='body_site', - ), - migrations.RenameField( - model_name='genomicsreport', - old_name='test_name', - new_name='code', - ), - migrations.RemoveField( - model_name='cancerrelatedprocedure', - name='occurence_time_or_period', - ), - migrations.RemoveField( - model_name='genomicsreport', - name='genetic_variant_found', - ), - migrations.RemoveField( - model_name='genomicsreport', - name='genetic_variant_tested', - ), - migrations.RemoveField( - model_name='genomicsreport', - name='specimen_type', - ), - migrations.RemoveField( - model_name='labsvital', - name='blood_pressure_diastolic', - ), - migrations.RemoveField( - model_name='labsvital', - name='blood_pressure_systolic', - ), - migrations.RemoveField( - model_name='labsvital', - name='body_height', - ), - migrations.RemoveField( - model_name='labsvital', - name='body_weight', - ), - migrations.RemoveField( - model_name='labsvital', - name='cbc_with_auto_differential_panel', - ), - migrations.RemoveField( - model_name='labsvital', - name='comprehensive_metabolic_2000', - ), - migrations.RemoveField( - model_name='labsvital', - name='tumor_marker_test', - ), - migrations.RemoveField( - model_name='medicationstatement', - name='date_time', - ), - migrations.AddField( - model_name='cancercondition', - name='laterality', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Body side of the body location, if needed to distinguish from a similar location on the other side of the body.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AddField( - model_name='cancercondition', - name='verification_status', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='A flag indicating whether the condition is unconfirmed, provisional, differential, confirmed, refuted, or entered-in-error.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AddField( - model_name='cancerrelatedprocedure', - name='laterality', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Body side of the body location, if needed to distinguish from a similar location on the other side of the body.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AddField( - model_name='cancerrelatedprocedure', - name='reason_code', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The explanation or justification for why the surgical procedure was performed.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AddField( - model_name='cancerrelatedprocedure', - name='reason_reference', - field=models.ManyToManyField(blank=True, help_text='Reference to a primary or secondary cancer condition.', to='mcode.CancerCondition'), - ), - migrations.AddField( - model_name='genomicsreport', - name='issued', - field=models.DateTimeField(default=datetime.datetime.now, help_text='The date/time this report was issued.'), - ), - migrations.AddField( - model_name='labsvital', - name='tumor_marker_code', - field=django.contrib.postgres.fields.jsonb.JSONField(default=None, help_text='A code identifying the type of tumor marker test.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - preserve_default=False, - ), - migrations.AddField( - model_name='labsvital', - name='tumor_marker_data_value', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The result of a tumor marker test.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:tumor_marker_data_value', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Tumor marker data value schema.', 'properties': {'value': {'anyOf': [{'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, {'$id': 'chord_metadata_service:quantity_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema for the datatype Quantity.', 'properties': {'code': {'type': 'string'}, 'comparator': {'enum': ['<', '>', '<=', '>=', '=']}, 'system': {'format': 'uri', 'type': 'string'}, 'unit': {'type': 'string'}, 'value': {'type': 'number'}}, 'title': 'Quantity schema', 'type': 'object'}, {'$id': 'chord_metadata_service:ratio', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Ratio schema.', 'properties': {'denominator': {'$id': 'chord_metadata_service:quantity_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema for the datatype Quantity.', 'properties': {'code': {'type': 'string'}, 'comparator': {'enum': ['<', '>', '<=', '>=', '=']}, 'system': {'format': 'uri', 'type': 'string'}, 'unit': {'type': 'string'}, 'value': {'type': 'number'}}, 'title': 'Quantity schema', 'type': 'object'}, 'numerator': {'$id': 'chord_metadata_service:quantity_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema for the datatype Quantity.', 'properties': {'code': {'type': 'string'}, 'comparator': {'enum': ['<', '>', '<=', '>=', '=']}, 'system': {'format': 'uri', 'type': 'string'}, 'unit': {'type': 'string'}, 'value': {'type': 'number'}}, 'title': 'Quantity schema', 'type': 'object'}}, 'title': 'Ratio', 'type': 'object'}]}}, 'title': 'Tumor marker data value', 'type': 'object'}, formats=None)]), - ), - migrations.AddField( - model_name='mcodepacket', - name='cancer_disease_status', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text="A clinician's qualitative judgment on the current trend of the cancer, e.g., whether it is stable, worsening (progressing), or improving (responding).", null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AddField( - model_name='mcodepacket', - name='date_of_death', - field=models.CharField(blank=True, help_text='An indication that the patient is no longer living, given by a date of death or boolean.', max_length=200), - ), - migrations.AlterField( - model_name='cancerrelatedprocedure', - name='procedure_type', - field=models.CharField(choices=[('radiation', 'radiation'), ('surgical', 'surgical')], help_text='Type of cancer related procedure: radiation or surgical.', max_length=200), - ), - migrations.DeleteModel( - name='GeneticVariantFound', - ), - migrations.DeleteModel( - name='GeneticVariantTested', - ), - migrations.AddField( - model_name='genomicsreport', - name='genetic_specimen', - field=models.ManyToManyField(blank=True, help_text='List of related genetic specimens.', to='mcode.GeneticSpecimen'), - ), - migrations.AddField( - model_name='genomicsreport', - name='genetic_variant', - field=models.ForeignKey(blank=True, help_text='Related genetic variant.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='mcode.CancerGeneticVariant'), - ), - migrations.AddField( - model_name='genomicsreport', - name='genomic_region_studied', - field=models.ForeignKey(blank=True, help_text='Related genomic region studied.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='mcode.GenomicRegionStudied'), - ), - ] diff --git a/chord_metadata_service/mcode/migrations/0007_auto_20200610_2254.py b/chord_metadata_service/mcode/migrations/0007_auto_20200610_2254.py deleted file mode 100644 index d4b67970c..000000000 --- a/chord_metadata_service/mcode/migrations/0007_auto_20200610_2254.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 2.2.13 on 2020-06-11 02:54 - -from django.db import migrations, models -import django.utils.timezone - - -class Migration(migrations.Migration): - - dependencies = [ - ('mcode', '0006_auto_20200610_2248'), - ] - - operations = [ - migrations.AlterField( - model_name='genomicsreport', - name='issued', - field=models.DateTimeField(default=django.utils.timezone.now, help_text='The date/time this report was issued.'), - ), - ] diff --git a/chord_metadata_service/mcode/migrations/0008_auto_20200610_2320.py b/chord_metadata_service/mcode/migrations/0008_auto_20200610_2320.py deleted file mode 100644 index de0404817..000000000 --- a/chord_metadata_service/mcode/migrations/0008_auto_20200610_2320.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 2.2.13 on 2020-06-11 03:20 - -import datetime -from django.db import migrations, models -from django.utils.timezone import utc - - -class Migration(migrations.Migration): - - dependencies = [ - ('mcode', '0007_auto_20200610_2254'), - ] - - operations = [ - migrations.AlterField( - model_name='genomicsreport', - name='issued', - field=models.DateTimeField(default=datetime.datetime(2020, 6, 11, 3, 20, 6, 606068, tzinfo=utc), help_text='The date/time this report was issued.'), - ), - ] diff --git a/chord_metadata_service/mcode/migrations/0009_auto_20200618_1318.py b/chord_metadata_service/mcode/migrations/0009_auto_20200618_1318.py deleted file mode 100644 index 97e6d7ad4..000000000 --- a/chord_metadata_service/mcode/migrations/0009_auto_20200618_1318.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 2.2.13 on 2020-06-18 17:18 - -import datetime -from django.db import migrations, models -import django.db.models.deletion -from django.utils.timezone import utc - - -class Migration(migrations.Migration): - - dependencies = [ - ('chord', '0018_auto_20200601_1708'), - ('mcode', '0008_auto_20200610_2320'), - ] - - operations = [ - migrations.AddField( - model_name='mcodepacket', - name='table', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='chord.Table'), - ), - migrations.AlterField( - model_name='genomicsreport', - name='issued', - field=models.DateTimeField(default=datetime.datetime(2020, 6, 18, 17, 18, 39, 482556, tzinfo=utc), help_text='The date/time this report was issued.'), - ), - ] diff --git a/chord_metadata_service/mcode/migrations/0010_auto_20200618_1825.py b/chord_metadata_service/mcode/migrations/0010_auto_20200618_1825.py deleted file mode 100644 index 5f9761207..000000000 --- a/chord_metadata_service/mcode/migrations/0010_auto_20200618_1825.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 2.2.13 on 2020-06-18 22:25 - -from django.db import migrations, models -import django.utils.timezone - - -class Migration(migrations.Migration): - - dependencies = [ - ('mcode', '0009_auto_20200618_1318'), - ] - - operations = [ - migrations.AlterField( - model_name='genomicsreport', - name='issued', - field=models.DateTimeField(default=django.utils.timezone.now, help_text='The date/time this report was issued.'), - ), - migrations.RemoveField( - model_name='mcodepacket', - name='cancer_condition', - ), - migrations.AddField( - model_name='mcodepacket', - name='cancer_condition', - field=models.ManyToManyField(blank=True, help_text="An Individual's cancer condition.", to='mcode.CancerCondition'), - ), - ] diff --git a/chord_metadata_service/mcode/migrations/0011_auto_20200625_1117.py b/chord_metadata_service/mcode/migrations/0011_auto_20200625_1117.py deleted file mode 100644 index 050ddab04..000000000 --- a/chord_metadata_service/mcode/migrations/0011_auto_20200625_1117.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 2.2.13 on 2020-06-25 15:17 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('mcode', '0010_auto_20200618_1825'), - ] - - operations = [ - migrations.RemoveField( - model_name='mcodepacket', - name='medication_statement', - ), - migrations.AddField( - model_name='mcodepacket', - name='medication_statement', - field=models.ManyToManyField(blank=True, help_text='Medication treatment addressed to an Individual.', to='mcode.MedicationStatement'), - ), - ] diff --git a/chord_metadata_service/patients/migrations/0001_initial.py b/chord_metadata_service/patients/migrations/0001_initial.py deleted file mode 100644 index e08f37220..000000000 --- a/chord_metadata_service/patients/migrations/0001_initial.py +++ /dev/null @@ -1,35 +0,0 @@ -# Generated by Django 2.2.8 on 2019-12-10 21:04 - -import django.contrib.postgres.fields -import django.contrib.postgres.fields.jsonb -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='Individual', - fields=[ - ('id', models.CharField(help_text='An arbitrary identifier for the individual.', max_length=200, primary_key=True, serialize=False)), - ('alternate_ids', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=200), blank=True, help_text='A list of alternative identifiers for the individual.', null=True, size=None)), - ('date_of_birth', models.DateField(blank=True, help_text='A timestamp either exact or imprecise.', null=True)), - ('age', models.CharField(blank=True, help_text='The age or age range of the individual.', max_length=200)), - ('sex', models.CharField(blank=True, choices=[('UNKNOWN_SEX', 'UNKNOWN_SEX'), ('FEMALE', 'FEMALE'), ('MALE', 'MALE'), ('OTHER_SEX', 'OTHER_SEX')], help_text='Observed apparent sex of the individual.', max_length=200, null=True)), - ('karyotypic_sex', models.CharField(choices=[('UNKNOWN_KARYOTYPE', 'UNKNOWN_KARYOTYPE'), ('XX', 'XX'), ('XY', 'XY'), ('XO', 'XO'), ('XXY', 'XXY'), ('XXX', 'XXX'), ('XXYY', 'XXYY'), ('XXXY', 'XXXY'), ('XXXX', 'XXXX'), ('XYY', 'XYY'), ('OTHER_KARYOTYPE', 'OTHER_KARYOTYPE')], default='UNKNOWN_KARYOTYPE', help_text='The karyotypic sex of the individual.', max_length=200)), - ('taxonomy', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Ontology resource representing the species (e.g., NCBITaxon:9615).', null=True)), - ('active', models.BooleanField(default=False, help_text="Whether this patient's record is in active use.")), - ('deceased', models.BooleanField(default=False, help_text='Indicates if the individual is deceased or not.')), - ('race', models.CharField(blank=True, help_text="A code for the person's race.", max_length=200)), - ('ethnicity', models.CharField(blank=True, help_text="A code for the person's ethnicity.", max_length=200)), - ('extra_properties', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema', null=True)), - ('created', models.DateTimeField(auto_now=True)), - ('updated', models.DateTimeField(auto_now_add=True)), - ], - ), - ] diff --git a/chord_metadata_service/patients/migrations/0001_v1_0_0.py b/chord_metadata_service/patients/migrations/0001_v1_0_0.py new file mode 100644 index 000000000..65e28e48c --- /dev/null +++ b/chord_metadata_service/patients/migrations/0001_v1_0_0.py @@ -0,0 +1,41 @@ +# Generated by Django 2.2.13 on 2020-07-06 14:55 + +import chord_metadata_service.restapi.models +import chord_metadata_service.restapi.validators +import django.contrib.postgres.fields +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Individual', + fields=[ + ('id', models.CharField(help_text='An arbitrary identifier for the individual.', max_length=200, primary_key=True, serialize=False)), + ('alternate_ids', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=200), blank=True, help_text='A list of alternative identifiers for the individual.', null=True, size=None)), + ('date_of_birth', models.DateField(blank=True, help_text='A timestamp either exact or imprecise.', null=True)), + ('age', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The age or age range of the individual.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:age_or_age_range_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'An age object describing the age of the individual at the time of collection of biospecimens or phenotypic observations.', 'oneOf': [{'$id': 'chord_metadata_service:age_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ISO8601 duration string (e.g. P40Y10M05D for 40 years, 10 months, 5 days) representing an age of a subject.', 'help': 'An ISO8601 duration string (e.g. P40Y10M05D for 40 years, 10 months, 5 days) representing an age of a subject.', 'properties': {'age': {'description': 'An ISO8601 duration string (e.g. P40Y10M05D for 40 years, 10 months, 5 days) representing an age of a subject.', 'help': 'Age of a subject.', 'type': 'string'}}, 'required': ['age'], 'title': 'Age schema', 'type': 'object'}, {'$id': 'chord_metadata_service:age_range_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': "Age range of a subject (e.g. when a subject's age falls into a bin.)", 'help': "Age range of a subject (e.g. when a subject's age falls into a bin.)", 'properties': {'end': {'$id': 'chord_metadata_service:age_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ISO8601 duration string representing the end of the age range bin.', 'help': 'An ISO8601 duration string representing the end of the age range bin.', 'properties': {'age': {'description': 'An ISO8601 duration string (e.g. P40Y10M05D for 40 years, 10 months, 5 days) representing an age of a subject.', 'help': 'Age of a subject.', 'type': 'string'}}, 'required': ['age'], 'title': 'Age schema', 'type': 'object'}, 'start': {'$id': 'chord_metadata_service:age_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ISO8601 duration string representing the start of the age range bin.', 'help': 'An ISO8601 duration string representing the start of the age range bin.', 'properties': {'age': {'description': 'An ISO8601 duration string (e.g. P40Y10M05D for 40 years, 10 months, 5 days) representing an age of a subject.', 'help': 'Age of a subject.', 'type': 'string'}}, 'required': ['age'], 'title': 'Age schema', 'type': 'object'}}, 'required': ['start', 'end'], 'title': 'Age range schema', 'type': 'object'}], 'title': 'Age schema', 'type': 'object'}, formats=None)])), + ('sex', models.CharField(blank=True, choices=[('UNKNOWN_SEX', 'UNKNOWN_SEX'), ('FEMALE', 'FEMALE'), ('MALE', 'MALE'), ('OTHER_SEX', 'OTHER_SEX')], help_text='Observed apparent sex of the individual.', max_length=200, null=True)), + ('karyotypic_sex', models.CharField(choices=[('UNKNOWN_KARYOTYPE', 'UNKNOWN_KARYOTYPE'), ('XX', 'XX'), ('XY', 'XY'), ('XO', 'XO'), ('XXY', 'XXY'), ('XXX', 'XXX'), ('XXYY', 'XXYY'), ('XXXY', 'XXXY'), ('XXXX', 'XXXX'), ('XYY', 'XYY'), ('OTHER_KARYOTYPE', 'OTHER_KARYOTYPE')], default='UNKNOWN_KARYOTYPE', help_text='The karyotypic sex of the individual.', max_length=200)), + ('taxonomy', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Ontology resource representing the species (e.g., NCBITaxon:9615).', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), + ('active', models.BooleanField(default=False, help_text="Whether this patient's record is in active use.")), + ('deceased', models.BooleanField(default=False, help_text='Indicates if the individual is deceased or not.')), + ('comorbid_condition', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='One or more conditions that occur with primary condition.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:comorbid_condition_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Comorbid condition schema.', 'properties': {'clinical_status': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'code': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}}, 'required': [], 'title': 'Comorbid Condition schema', 'type': 'object'}, formats=None)])), + ('ecog_performance_status', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Value representing the Eastern Cooperative Oncology Group performance status.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), + ('karnofsky', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Value representing the Karnofsky Performance status.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), + ('race', models.CharField(blank=True, help_text="A code for the person's race.", max_length=200)), + ('ethnicity', models.CharField(blank=True, help_text="A code for the person's ethnicity.", max_length=200)), + ('extra_properties', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema', null=True)), + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ], + bases=(models.Model, chord_metadata_service.restapi.models.IndexableMixin), + ), + ] diff --git a/chord_metadata_service/patients/migrations/0002_remove_individual_age.py b/chord_metadata_service/patients/migrations/0002_remove_individual_age.py deleted file mode 100644 index 65edbd807..000000000 --- a/chord_metadata_service/patients/migrations/0002_remove_individual_age.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 2.2.7 on 2019-12-16 22:04 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('patients', '0001_initial'), - ] - - operations = [ - migrations.RemoveField( - model_name='individual', - name='age', - ), - ] diff --git a/chord_metadata_service/patients/migrations/0003_individual_age.py b/chord_metadata_service/patients/migrations/0003_individual_age.py deleted file mode 100644 index ef375de5e..000000000 --- a/chord_metadata_service/patients/migrations/0003_individual_age.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 2.2.7 on 2019-12-16 22:04 - -import django.contrib.postgres.fields.jsonb -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('patients', '0002_remove_individual_age'), - ] - - operations = [ - migrations.AddField( - model_name='individual', - name='age', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The age or age range of the individual.', null=True), - ), - ] diff --git a/chord_metadata_service/patients/migrations/0004_auto_20200129_1537.py b/chord_metadata_service/patients/migrations/0004_auto_20200129_1537.py deleted file mode 100644 index 7689a5273..000000000 --- a/chord_metadata_service/patients/migrations/0004_auto_20200129_1537.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 2.2.9 on 2020-01-29 15:37 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('patients', '0003_individual_age'), - ] - - operations = [ - migrations.AlterField( - model_name='individual', - name='created', - field=models.DateTimeField(auto_now_add=True), - ), - migrations.AlterField( - model_name='individual', - name='updated', - field=models.DateTimeField(auto_now=True), - ), - ] diff --git a/chord_metadata_service/patients/migrations/0005_auto_20200311_1610.py b/chord_metadata_service/patients/migrations/0005_auto_20200311_1610.py deleted file mode 100644 index c763dc652..000000000 --- a/chord_metadata_service/patients/migrations/0005_auto_20200311_1610.py +++ /dev/null @@ -1,29 +0,0 @@ -# Generated by Django 2.2.10 on 2020-03-11 20:10 - -import django.contrib.postgres.fields.jsonb -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('patients', '0004_auto_20200129_1537'), - ] - - operations = [ - migrations.AddField( - model_name='individual', - name='comorbid_condition', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='One or more conditions that occur with primary condition.', null=True), - ), - migrations.AddField( - model_name='individual', - name='ecog_performance_status', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Value representing the Eastern Cooperative Oncology Group performance status.', null=True), - ), - migrations.AddField( - model_name='individual', - name='karnofsky', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Value representing the Karnofsky Performance status.', null=True), - ), - ] diff --git a/chord_metadata_service/patients/migrations/0006_auto_20200401_1504.py b/chord_metadata_service/patients/migrations/0006_auto_20200401_1504.py deleted file mode 100644 index b45c7ab94..000000000 --- a/chord_metadata_service/patients/migrations/0006_auto_20200401_1504.py +++ /dev/null @@ -1,40 +0,0 @@ -# Generated by Django 2.2.10 on 2020-04-01 19:04 - -import chord_metadata_service.restapi.validators -import django.contrib.postgres.fields.jsonb -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('patients', '0005_auto_20200311_1610'), - ] - - operations = [ - migrations.AlterField( - model_name='individual', - name='age', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The age or age range of the individual.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:age_or_age_range_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An age object describing the age of the individual at the time of collection of biospecimens or phenotypic observations.', 'properties': {'age': {'anyOf': [{'description': 'An ISO8601 string represent age.', 'type': 'string'}, {'$id': 'chord_metadata_service:age_range_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An age range of a subject.', 'properties': {'end': {'properties': {'age': {'description': 'An ISO8601 string represent age.', 'type': 'string'}}, 'type': 'object'}, 'start': {'properties': {'age': {'description': 'An ISO8601 string represent age.', 'type': 'string'}}, 'type': 'object'}}, 'required': ['start', 'end'], 'title': 'Age schema', 'type': 'object'}]}}, 'title': 'Age schema', 'type': 'object'})]), - ), - migrations.AlterField( - model_name='individual', - name='comorbid_condition', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='One or more conditions that occur with primary condition.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:comorbid_condition_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Comorbid condition schema.', 'properties': {'clinical_status': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'code': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}}, 'required': [], 'title': 'Comorbid Condition schema', 'type': 'object'})]), - ), - migrations.AlterField( - model_name='individual', - name='ecog_performance_status', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Value representing the Eastern Cooperative Oncology Group performance status.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'})]), - ), - migrations.AlterField( - model_name='individual', - name='karnofsky', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Value representing the Karnofsky Performance status.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'})]), - ), - migrations.AlterField( - model_name='individual', - name='taxonomy', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Ontology resource representing the species (e.g., NCBITaxon:9615).', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'})]), - ), - ] diff --git a/chord_metadata_service/patients/migrations/0007_auto_20200430_1444.py b/chord_metadata_service/patients/migrations/0007_auto_20200430_1444.py deleted file mode 100644 index cc0ddae7b..000000000 --- a/chord_metadata_service/patients/migrations/0007_auto_20200430_1444.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 2.2.12 on 2020-04-30 14:44 - -import chord_metadata_service.restapi.validators -import django.contrib.postgres.fields.jsonb -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('patients', '0006_auto_20200401_1504'), - ] - - operations = [ - migrations.AlterField( - model_name='individual', - name='age', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The age or age range of the individual.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:age_or_age_range_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'An age object describing the age of the individual at the time of collection of biospecimens or phenotypic observations.', 'oneOf': [{'$id': 'chord_metadata_service:age_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An age of a subject.', 'properties': {'age': {'description': 'An ISO8601 string represent age.', 'type': 'string'}}, 'required': ['age'], 'title': 'Age schema', 'type': 'object'}, {'$id': 'chord_metadata_service:age_range_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An age range of a subject.', 'properties': {'end': {'$id': 'chord_metadata_service:age_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An age of a subject.', 'properties': {'age': {'description': 'An ISO8601 string represent age.', 'type': 'string'}}, 'required': ['age'], 'title': 'Age schema', 'type': 'object'}, 'start': {'$id': 'chord_metadata_service:age_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An age of a subject.', 'properties': {'age': {'description': 'An ISO8601 string represent age.', 'type': 'string'}}, 'required': ['age'], 'title': 'Age schema', 'type': 'object'}}, 'required': ['start', 'end'], 'title': 'Age range schema', 'type': 'object'}], 'title': 'Age schema', 'type': 'object'})]), - ), - ] diff --git a/chord_metadata_service/patients/migrations/0008_auto_20200513_1401.py b/chord_metadata_service/patients/migrations/0008_auto_20200513_1401.py deleted file mode 100644 index 5b9855122..000000000 --- a/chord_metadata_service/patients/migrations/0008_auto_20200513_1401.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 2.2.12 on 2020-05-13 18:01 - -import chord_metadata_service.restapi.validators -import django.contrib.postgres.fields.jsonb -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('patients', '0007_auto_20200430_1444'), - ] - - operations = [ - migrations.AlterField( - model_name='individual', - name='age', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The age or age range of the individual.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:age_or_age_range_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'An age object describing the age of the individual at the time of collection of biospecimens or phenotypic observations.', 'oneOf': [{'$id': 'chord_metadata_service:age_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ISO8601 duration string (e.g. P40Y10M05D for 40 years, 10 months, 5 days) representing an age of a subject.', 'help': 'An ISO8601 duration string (e.g. P40Y10M05D for 40 years, 10 months, 5 days) representing an age of a subject.', 'properties': {'age': {'description': 'An ISO8601 duration string (e.g. P40Y10M05D for 40 years, 10 months, 5 days) representing an age of a subject.', 'help': 'Age of a subject.', 'type': 'string'}}, 'required': ['age'], 'title': 'Age schema', 'type': 'object'}, {'$id': 'chord_metadata_service:age_range_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': "Age range of a subject (e.g. when a subject's age falls into a bin.)", 'help': "Age range of a subject (e.g. when a subject's age falls into a bin.)", 'properties': {'end': {'$id': 'chord_metadata_service:age_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ISO8601 duration string representing the end of the age range bin.', 'help': 'An ISO8601 duration string representing the end of the age range bin.', 'properties': {'age': {'description': 'An ISO8601 duration string (e.g. P40Y10M05D for 40 years, 10 months, 5 days) representing an age of a subject.', 'help': 'Age of a subject.', 'type': 'string'}}, 'required': ['age'], 'title': 'Age schema', 'type': 'object'}, 'start': {'$id': 'chord_metadata_service:age_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ISO8601 duration string representing the start of the age range bin.', 'help': 'An ISO8601 duration string representing the start of the age range bin.', 'properties': {'age': {'description': 'An ISO8601 duration string (e.g. P40Y10M05D for 40 years, 10 months, 5 days) representing an age of a subject.', 'help': 'Age of a subject.', 'type': 'string'}}, 'required': ['age'], 'title': 'Age schema', 'type': 'object'}}, 'required': ['start', 'end'], 'title': 'Age range schema', 'type': 'object'}], 'title': 'Age schema', 'type': 'object'}, formats=None)]), - ), - ] diff --git a/chord_metadata_service/patients/migrations/0009_auto_20200514_1541.py b/chord_metadata_service/patients/migrations/0009_auto_20200514_1541.py deleted file mode 100644 index 592fa61ff..000000000 --- a/chord_metadata_service/patients/migrations/0009_auto_20200514_1541.py +++ /dev/null @@ -1,35 +0,0 @@ -# Generated by Django 2.2.12 on 2020-05-14 19:41 - -import chord_metadata_service.restapi.validators -import django.contrib.postgres.fields.jsonb -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('patients', '0008_auto_20200513_1401'), - ] - - operations = [ - migrations.AlterField( - model_name='individual', - name='comorbid_condition', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='One or more conditions that occur with primary condition.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:comorbid_condition_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Comorbid condition schema.', 'properties': {'clinical_status': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'code': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}}, 'required': [], 'title': 'Comorbid Condition schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='individual', - name='ecog_performance_status', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Value representing the Eastern Cooperative Oncology Group performance status.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='individual', - name='karnofsky', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Value representing the Karnofsky Performance status.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='individual', - name='taxonomy', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Ontology resource representing the species (e.g., NCBITaxon:9615).', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - ] diff --git a/chord_metadata_service/patients/migrations/0010_auto_20200519_1538.py b/chord_metadata_service/patients/migrations/0010_auto_20200519_1538.py deleted file mode 100644 index 79a8fa34f..000000000 --- a/chord_metadata_service/patients/migrations/0010_auto_20200519_1538.py +++ /dev/null @@ -1,35 +0,0 @@ -# Generated by Django 2.2.12 on 2020-05-19 15:38 - -import chord_metadata_service.restapi.validators -import django.contrib.postgres.fields.jsonb -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('patients', '0009_auto_20200514_1541'), - ] - - operations = [ - migrations.AlterField( - model_name='individual', - name='comorbid_condition', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='One or more conditions that occur with primary condition.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:comorbid_condition_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Comorbid condition schema.', 'properties': {'clinical_status': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'code': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}}, 'required': [], 'title': 'Comorbid Condition schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='individual', - name='ecog_performance_status', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Value representing the Eastern Cooperative Oncology Group performance status.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='individual', - name='karnofsky', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Value representing the Karnofsky Performance status.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='individual', - name='taxonomy', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Ontology resource representing the species (e.g., NCBITaxon:9615).', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - ] diff --git a/chord_metadata_service/phenopackets/migrations/0001_initial.py b/chord_metadata_service/phenopackets/migrations/0001_initial.py deleted file mode 100644 index 6e4090541..000000000 --- a/chord_metadata_service/phenopackets/migrations/0001_initial.py +++ /dev/null @@ -1,234 +0,0 @@ -# Generated by Django 2.2.8 on 2019-12-10 21:04 - -import django.contrib.postgres.fields -import django.contrib.postgres.fields.jsonb -from django.db import migrations, models -import django.db.models.deletion -import django.utils.timezone - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('patients', '0001_initial'), - ('chord', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='Biosample', - fields=[ - ('id', models.CharField(help_text='An arbitrary identifier.', max_length=200, primary_key=True, serialize=False)), - ('description', models.CharField(blank=True, help_text='The biosample’s description.', max_length=200)), - ('sampled_tissue', django.contrib.postgres.fields.jsonb.JSONField(help_text='An Ontology term describing the tissue from which the sample was taken.')), - ('taxonomy', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An Ontology term describing the species of the sampled individual.', null=True)), - ('individual_age_at_collection', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Age of the proband at the time the sample was taken.', null=True)), - ('histological_diagnosis', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An Ontology term describing the disease diagnosis that was inferred from the histological examination.', null=True)), - ('tumor_progression', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An Ontology term describing primary, metastatic, recurrent.', null=True)), - ('tumor_grade', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An Ontology term describing the tumor grade. Potentially a child term of NCIT:C28076 or equivalent.', null=True)), - ('diagnostic_markers', django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True), blank=True, help_text='List of Ontology terms describing clinically relevant biomarkers.', null=True, size=None)), - ('is_control_sample', models.BooleanField(default=False, help_text='Whether the sample is being used as a normal control.')), - ('extra_properties', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema', null=True)), - ('created', models.DateTimeField(auto_now=True)), - ('updated', models.DateTimeField(auto_now_add=True)), - ], - ), - migrations.CreateModel( - name='Diagnosis', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('extra_properties', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema', null=True)), - ('created', models.DateTimeField(auto_now=True)), - ('updated', models.DateTimeField(auto_now_add=True)), - ], - ), - migrations.CreateModel( - name='Disease', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('term', django.contrib.postgres.fields.jsonb.JSONField(help_text='An ontology term that represents the disease.')), - ('onset', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An element representing the age of onset of the disease.', null=True)), - ('disease_stage', django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True), blank=True, help_text='List of terms representing the disease stage.', null=True, size=None)), - ('tnm_finding', django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True), blank=True, help_text='List of terms representing the tumor TNM score.', null=True, size=None)), - ('extra_properties', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema', null=True)), - ('created', models.DateTimeField(auto_now=True)), - ('updated', models.DateTimeField(auto_now_add=True)), - ], - ), - migrations.CreateModel( - name='Gene', - fields=[ - ('id', models.CharField(help_text='Official identifier of the gene.', max_length=200, primary_key=True, serialize=False)), - ('alternate_ids', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=200), blank=True, help_text='Alternative identifier(s) of the gene.', null=True, size=None)), - ('symbol', models.CharField(help_text='Official gene symbol.', max_length=200)), - ('extra_properties', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema', null=True)), - ('created', models.DateTimeField(auto_now=True)), - ('updated', models.DateTimeField(auto_now_add=True)), - ], - ), - migrations.CreateModel( - name='HtsFile', - fields=[ - ('uri', models.URLField(help_text='A valid URI for the file.', primary_key=True, serialize=False)), - ('description', models.CharField(blank=True, help_text='An arbitrary description of the file contents.', max_length=200)), - ('hts_format', models.CharField(choices=[('UNKNOWN', 'UNKNOWN'), ('SAM', 'SAM'), ('BAM', 'BAM'), ('CRAM', 'CRAM'), ('VCF', 'VCF'), ('BCF', 'BCF'), ('GVCF', 'GVCF')], help_text='A format of the file.', max_length=200)), - ('genome_assembly', models.CharField(help_text='The genome assembly the contents of this file was called against.', max_length=200)), - ('individual_to_sample_identifiers', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='The mapping between the Individual.id or Biosample.id to the sample identifier in the HTS file', null=True)), - ('extra_properties', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema', null=True)), - ('created', models.DateTimeField(auto_now=True)), - ('updated', models.DateTimeField(auto_now_add=True)), - ], - ), - migrations.CreateModel( - name='MetaData', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', models.DateTimeField(default=django.utils.timezone.now, help_text='Time when this object was created.')), - ('created_by', models.CharField(help_text='Name of person who created the phenopacket.', max_length=200)), - ('submitted_by', models.CharField(blank=True, help_text='Name of person who submitted the phenopacket.', max_length=200)), - ('updates', django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True), blank=True, help_text='List of updates to the phenopacket.', null=True, size=None)), - ('phenopacket_schema_version', models.CharField(blank=True, help_text='Schema version of the current phenopacket.', max_length=200)), - ('external_references', django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True), blank=True, help_text='List of external resources from the phenopacket was derived.', null=True, size=None)), - ('extra_properties', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema', null=True)), - ('updated', models.DateTimeField(auto_now_add=True)), - ], - ), - migrations.CreateModel( - name='Phenopacket', - fields=[ - ('id', models.CharField(help_text='An arbitrary identifier for the phenopacket.', max_length=200, primary_key=True, serialize=False)), - ('extra_properties', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema', null=True)), - ('created', models.DateTimeField(auto_now=True)), - ('updated', models.DateTimeField(auto_now_add=True)), - ('biosamples', models.ManyToManyField(blank=True, help_text='The biosamples that have been derived from an individual who is the subject of the Phenopacket. Rr a collection of biosamples in isolation.', to='phenopackets.Biosample')), - ('dataset', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='chord.Dataset')), - ('diseases', models.ManyToManyField(blank=True, help_text='Disease(s) diagnosed in the proband.', to='phenopackets.Disease')), - ('genes', models.ManyToManyField(blank=True, help_text='Gene deemed to be relevant to the case.', to='phenopackets.Gene')), - ('hts_files', models.ManyToManyField(blank=True, help_text='VCF or other high-throughput sequencing files.', to='phenopackets.HtsFile')), - ('meta_data', models.ForeignKey(help_text='Information about ontologies and references used in the phenopacket.', on_delete=django.db.models.deletion.CASCADE, to='phenopackets.MetaData')), - ('subject', models.ForeignKey(help_text='The proband.', on_delete=django.db.models.deletion.CASCADE, related_name='phenopackets', to='patients.Individual')), - ], - ), - migrations.CreateModel( - name='Procedure', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('code', django.contrib.postgres.fields.jsonb.JSONField(help_text='Clinical procedure performed on a subject.')), - ('body_site', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Specific body site if unable to represent this is the code.', null=True)), - ('extra_properties', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema', null=True)), - ('created', models.DateTimeField(auto_now=True)), - ('updated', models.DateTimeField(auto_now_add=True)), - ], - ), - migrations.CreateModel( - name='Resource', - fields=[ - ('id', models.CharField(help_text='For OBO ontologies, the value of this string MUST always be the official OBO ID, which is always equivalent to the ID prefix in lower case. For other resources use the prefix in identifiers.org.', max_length=200, primary_key=True, serialize=False)), - ('name', models.CharField(help_text='The full name of the ontology referred to by the id element.', max_length=200)), - ('namespace_prefix', models.CharField(help_text='The prefix used in the CURIE of an Ontology term.', max_length=200)), - ('url', models.URLField(help_text='For OBO ontologies, this MUST be the PURL. Other resources should link to the official or top-level url.')), - ('version', models.CharField(help_text='The version of the resource or ontology used to make the annotation.', max_length=200)), - ('iri_prefix', models.URLField(help_text='The full IRI prefix which can be used with the namespace_prefix and the Ontology::id to resolve to an IRI for a term.')), - ('extra_properties', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema', null=True)), - ('created', models.DateTimeField(auto_now=True)), - ('updated', models.DateTimeField(auto_now_add=True)), - ], - ), - migrations.CreateModel( - name='Variant', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('allele_type', models.CharField(choices=[('hgvsAllele', 'hgvsAllele'), ('vcfAllele', 'vcfAllele'), ('spdiAllele', 'spdiAllele'), ('iscnAllele', 'iscnAllele')], help_text='One of four allele types.', max_length=200)), - ('allele', django.contrib.postgres.fields.jsonb.JSONField()), - ('zygosity', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Genotype Ontology (GENO) term representing the zygosity of the variant.', null=True)), - ('extra_properties', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema', null=True)), - ('created', models.DateTimeField(auto_now=True)), - ('updated', models.DateTimeField(auto_now_add=True)), - ], - ), - migrations.CreateModel( - name='PhenotypicFeature', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('description', models.CharField(blank=True, help_text='Human-readable verbiage NOT for structured text', max_length=200)), - ('pftype', django.contrib.postgres.fields.jsonb.JSONField(help_text='Ontology term that describes the phenotype.', verbose_name='type')), - ('negated', models.BooleanField(default=False, help_text='This element is a flag to indicate whether the phenotype was observed or not.')), - ('severity', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Description of the severity of the featurerepresented by a term from HP:0012824.', null=True)), - ('modifier', django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True), blank=True, help_text='This element is intended to provide more expressive or precise descriptions of a phenotypic feature, including attributes such as positionality and external factors that tend to trigger or ameliorate the feature.', null=True, size=None)), - ('onset', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='This element can be used to describe the age at which a phenotypic feature was first noticed or diagnosed.', null=True)), - ('evidence', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='This element intends to represent the evidence for an assertion such as an observation of a PhenotypicFeature.', null=True)), - ('extra_properties', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema', null=True)), - ('created', models.DateTimeField(auto_now=True)), - ('updated', models.DateTimeField(auto_now_add=True)), - ('biosample', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='phenotypic_features', to='phenopackets.Biosample')), - ('phenopacket', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='phenotypic_features', to='phenopackets.Phenopacket')), - ], - ), - migrations.AddField( - model_name='phenopacket', - name='variants', - field=models.ManyToManyField(blank=True, help_text='Variants identified in the proband.', to='phenopackets.Variant'), - ), - migrations.AddField( - model_name='metadata', - name='resources', - field=models.ManyToManyField(help_text='This element contains a listing of the ontologies/resources referenced in the phenopacket.', to='phenopackets.Resource'), - ), - migrations.CreateModel( - name='Interpretation', - fields=[ - ('id', models.CharField(help_text='An arbitrary identifier for the interpretation.', max_length=200, primary_key=True, serialize=False)), - ('resolution_status', models.CharField(blank=True, choices=[('UNKNOWN', 'UNKNOWN'), ('SOLVED', 'SOLVED'), ('UNSOLVED', 'UNSOLVED'), ('IN_PROGRESS', 'IN_PROGRESS')], help_text='The current status of work on the case.', max_length=200)), - ('extra_properties', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema', null=True)), - ('created', models.DateTimeField(auto_now=True)), - ('updated', models.DateTimeField(auto_now_add=True)), - ('diagnosis', models.ManyToManyField(help_text='One or more diagnoses, if made.', to='phenopackets.Diagnosis')), - ('meta_data', models.ForeignKey(help_text='Metadata about this interpretation.', on_delete=django.db.models.deletion.CASCADE, to='phenopackets.MetaData')), - ('phenopacket', models.ForeignKey(help_text='The subject of this interpretation.', on_delete=django.db.models.deletion.CASCADE, related_name='interpretations', to='phenopackets.Phenopacket')), - ], - ), - migrations.CreateModel( - name='GenomicInterpretation', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('status', models.CharField(choices=[('UNKNOWN', 'UNKNOWN'), ('REJECTED', 'REJECTED'), ('CANDIDATE', 'CANDIDATE'), ('CAUSATIVE', 'CAUSATIVE')], help_text='How the call of this GenomicInterpretation was interpreted.', max_length=200)), - ('extra_properties', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema', null=True)), - ('created', models.DateTimeField(auto_now=True)), - ('updated', models.DateTimeField(auto_now_add=True)), - ('gene', models.ForeignKey(blank=True, help_text='The gene contributing to the diagnosis.', null=True, on_delete=django.db.models.deletion.CASCADE, to='phenopackets.Gene')), - ('variant', models.ForeignKey(blank=True, help_text='The variant contributing to the diagnosis.', null=True, on_delete=django.db.models.deletion.CASCADE, to='phenopackets.Variant')), - ], - ), - migrations.AddField( - model_name='diagnosis', - name='disease', - field=models.ForeignKey(help_text='The diagnosed condition.', on_delete=django.db.models.deletion.CASCADE, to='phenopackets.Disease'), - ), - migrations.AddField( - model_name='diagnosis', - name='genomic_interpretations', - field=models.ManyToManyField(blank=True, help_text='The genomic elements assessed as being responsible for the disease.', to='phenopackets.GenomicInterpretation'), - ), - migrations.AddField( - model_name='biosample', - name='hts_files', - field=models.ManyToManyField(blank=True, help_text='List of high-throughput sequencing files derived from the biosample.', related_name='biosample_hts_files', to='phenopackets.HtsFile'), - ), - migrations.AddField( - model_name='biosample', - name='individual', - field=models.ForeignKey(blank=True, help_text='The id of the Individual this biosample was derived from.', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='biosamples', to='patients.Individual'), - ), - migrations.AddField( - model_name='biosample', - name='procedure', - field=models.ForeignKey(help_text='The procedure used to extract the biosample.', on_delete=django.db.models.deletion.CASCADE, to='phenopackets.Procedure'), - ), - migrations.AddField( - model_name='biosample', - name='variants', - field=models.ManyToManyField(blank=True, help_text='List of variants determined to be present in the biosample.', to='phenopackets.Variant'), - ), - ] diff --git a/chord_metadata_service/phenopackets/migrations/0001_v1_0_0.py b/chord_metadata_service/phenopackets/migrations/0001_v1_0_0.py new file mode 100644 index 000000000..82694129a --- /dev/null +++ b/chord_metadata_service/phenopackets/migrations/0001_v1_0_0.py @@ -0,0 +1,224 @@ +# Generated by Django 2.2.13 on 2020-07-06 14:55 + +import chord_metadata_service.restapi.models +import chord_metadata_service.restapi.validators +import django.contrib.postgres.fields +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('chord', '0001_v1_0_0'), + ('patients', '0001_v1_0_0'), + ('resources', '0001_v1_0_0'), + ] + + operations = [ + migrations.CreateModel( + name='Biosample', + fields=[ + ('id', models.CharField(help_text='Unique arbitrary, researcher-specified identifier for the biosample.', max_length=200, primary_key=True, serialize=False)), + ('description', models.CharField(blank=True, help_text='Human-readable, unstructured text describing the biosample or providing additional information.', max_length=200)), + ('sampled_tissue', django.contrib.postgres.fields.jsonb.JSONField(help_text='An ontology term describing the tissue from which the specimen was collected. The use of UBERON is recommended.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), + ('taxonomy', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term specified when more than one organism may be studied. It is advised that codesfrom the NCBI Taxonomy resource are used, e.g. NCBITaxon:9606 for humans.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), + ('individual_age_at_collection', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='individual_age_at_collection', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:age_or_age_range_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'An age object describing the age of the individual at the time of collection of biospecimens or phenotypic observations.', 'oneOf': [{'$id': 'chord_metadata_service:age_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ISO8601 duration string (e.g. P40Y10M05D for 40 years, 10 months, 5 days) representing an age of a subject.', 'help': 'An ISO8601 duration string (e.g. P40Y10M05D for 40 years, 10 months, 5 days) representing an age of a subject.', 'properties': {'age': {'description': 'An ISO8601 duration string (e.g. P40Y10M05D for 40 years, 10 months, 5 days) representing an age of a subject.', 'help': 'Age of a subject.', 'type': 'string'}}, 'required': ['age'], 'title': 'Age schema', 'type': 'object'}, {'$id': 'chord_metadata_service:age_range_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': "Age range of a subject (e.g. when a subject's age falls into a bin.)", 'help': "Age range of a subject (e.g. when a subject's age falls into a bin.)", 'properties': {'end': {'$id': 'chord_metadata_service:age_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ISO8601 duration string representing the end of the age range bin.', 'help': 'An ISO8601 duration string representing the end of the age range bin.', 'properties': {'age': {'description': 'An ISO8601 duration string (e.g. P40Y10M05D for 40 years, 10 months, 5 days) representing an age of a subject.', 'help': 'Age of a subject.', 'type': 'string'}}, 'required': ['age'], 'title': 'Age schema', 'type': 'object'}, 'start': {'$id': 'chord_metadata_service:age_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ISO8601 duration string representing the start of the age range bin.', 'help': 'An ISO8601 duration string representing the start of the age range bin.', 'properties': {'age': {'description': 'An ISO8601 duration string (e.g. P40Y10M05D for 40 years, 10 months, 5 days) representing an age of a subject.', 'help': 'Age of a subject.', 'type': 'string'}}, 'required': ['age'], 'title': 'Age schema', 'type': 'object'}}, 'required': ['start', 'end'], 'title': 'Age range schema', 'type': 'object'}], 'title': 'Age schema', 'type': 'object'}, formats=None)])), + ('histological_diagnosis', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term representing a refinement of the clinical diagnosis. Normal samples could be tagged with NCIT:C38757, representing a negative finding.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), + ('tumor_progression', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term representing if the specimen is from a primary tumour, a metastasis, or a recurrence. There are multiple ways of representing this using ontology terms, and the terms chosen will have a specific meaning that is application specific.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), + ('tumor_grade', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term representing the tumour grade. This should be a child term of NCIT:C28076 (Disease Grade Qualifier) or equivalent.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), + ('diagnostic_markers', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='A list of ontology terms representing clinically-relevant bio-markers.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)])), + ('is_control_sample', models.BooleanField(default=False, help_text='Whether the sample is being used as a normal control.')), + ('extra_properties', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema.', null=True)), + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ], + bases=(models.Model, chord_metadata_service.restapi.models.IndexableMixin), + ), + migrations.CreateModel( + name='Diagnosis', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('extra_properties', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema', null=True)), + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ], + ), + migrations.CreateModel( + name='Disease', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('term', django.contrib.postgres.fields.jsonb.JSONField(help_text="An ontology term that represents the disease. It's recommended that one of the OMIM, Orphanet, or MONDO ontologies is used for rare human diseases.", validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), + ('onset', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='A representation of the age of onset of the disease', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:disease_onset_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'anyOf': [{'$id': 'chord_metadata_service:age_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ISO8601 duration string (e.g. P40Y10M05D for 40 years, 10 months, 5 days) representing an age of a subject.', 'help': 'An ISO8601 duration string (e.g. P40Y10M05D for 40 years, 10 months, 5 days) representing an age of a subject.', 'properties': {'age': {'description': 'An ISO8601 duration string (e.g. P40Y10M05D for 40 years, 10 months, 5 days) representing an age of a subject.', 'help': 'Age of a subject.', 'type': 'string'}}, 'required': ['age'], 'title': 'Age schema', 'type': 'object'}, {'$id': 'chord_metadata_service:age_range_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': "Age range of a subject (e.g. when a subject's age falls into a bin.)", 'help': "Age range of a subject (e.g. when a subject's age falls into a bin.)", 'properties': {'end': {'$id': 'chord_metadata_service:age_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ISO8601 duration string representing the end of the age range bin.', 'help': 'An ISO8601 duration string representing the end of the age range bin.', 'properties': {'age': {'description': 'An ISO8601 duration string (e.g. P40Y10M05D for 40 years, 10 months, 5 days) representing an age of a subject.', 'help': 'Age of a subject.', 'type': 'string'}}, 'required': ['age'], 'title': 'Age schema', 'type': 'object'}, 'start': {'$id': 'chord_metadata_service:age_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ISO8601 duration string representing the start of the age range bin.', 'help': 'An ISO8601 duration string representing the start of the age range bin.', 'properties': {'age': {'description': 'An ISO8601 duration string (e.g. P40Y10M05D for 40 years, 10 months, 5 days) representing an age of a subject.', 'help': 'Age of a subject.', 'type': 'string'}}, 'required': ['age'], 'title': 'Age schema', 'type': 'object'}}, 'required': ['start', 'end'], 'title': 'Age range schema', 'type': 'object'}, {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}], 'description': 'Schema for the age of the onset of the disease.', 'title': 'Onset age', 'type': 'object'}, formats=None)])), + ('disease_stage', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='A list of terms representing the disease stage. Elements should be derived from child terms of NCIT:C28108 (Disease Stage Qualifier) or equivalent hierarchy from another ontology.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)])), + ('tnm_finding', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='A list of terms representing the tumour TNM score. Elements should be derived from child terms of NCIT:C48232 (Cancer TNM Finding) or equivalent hierarchy from another ontology.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)])), + ('extra_properties', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema.', null=True)), + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ], + bases=(models.Model, chord_metadata_service.restapi.models.IndexableMixin), + ), + migrations.CreateModel( + name='Gene', + fields=[ + ('id', models.CharField(help_text='Official identifier of the gene. It SHOULD be a CURIE identifier with a prefix used by the official organism gene nomenclature committee, e.g. HGNC:347 for humans.', max_length=200, primary_key=True, serialize=False)), + ('alternate_ids', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=200), blank=True, default=list, help_text='A list of identifiers for alternative resources where the gene is used or catalogued.', size=None)), + ('symbol', models.CharField(help_text="A gene's official gene symbol as designated by the organism's gene nomenclature committee, e.g. ETF1 from the HUGO Gene Nomenclature committee.", max_length=200)), + ('extra_properties', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema.', null=True)), + ('created', models.DateTimeField(auto_now=True)), + ('updated', models.DateTimeField(auto_now_add=True)), + ], + ), + migrations.CreateModel( + name='HtsFile', + fields=[ + ('uri', models.URLField(help_text='A valid URI to the file', primary_key=True, serialize=False)), + ('description', models.CharField(blank=True, help_text='Human-readable text describing the file.', max_length=200)), + ('hts_format', models.CharField(choices=[('UNKNOWN', 'UNKNOWN'), ('SAM', 'SAM'), ('BAM', 'BAM'), ('CRAM', 'CRAM'), ('VCF', 'VCF'), ('BCF', 'BCF'), ('GVCF', 'GVCF')], help_text="The file's format; one of SAM, BAM, CRAM, VCF, BCF, GVCF, FASTQ, or UNKNOWN.", max_length=200)), + ('genome_assembly', models.CharField(help_text='Genome assembly ID for the file, e.g. GRCh38.', max_length=200)), + ('individual_to_sample_identifiers', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Mapping between individual or biosample IDs and the sample identifier in the HTS file.', null=True)), + ('extra_properties', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema.', null=True)), + ('created', models.DateTimeField(auto_now=True)), + ('updated', models.DateTimeField(auto_now_add=True)), + ], + bases=(models.Model, chord_metadata_service.restapi.models.IndexableMixin), + ), + migrations.CreateModel( + name='MetaData', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(default=django.utils.timezone.now, help_text='Timestamp specifying when when this object was created.')), + ('created_by', models.CharField(help_text='Name of the person who created the phenopacket.', max_length=200)), + ('submitted_by', models.CharField(blank=True, help_text='Name of the person who submitted the phenopacket.', max_length=200)), + ('updates', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='A list of updates to the phenopacket.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:schema_list', '$schema': 'http://json-schema.org/draft-07/schema#', 'items': {'$id': 'chord_metadata_service:update_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An update event for a record (e.g. a phenopacket.)', 'help': 'An update event for a record (e.g. a phenopacket.)', 'properties': {'comment': {'description': 'Free-text comment about the changes made and/or the reason for the update.', 'help': 'Free-text comment about the changes made and/or the reason for the update.', 'type': 'string'}, 'timestamp': {'description': 'ISO8601 UTC timestamp specifying when when this update occurred.', 'format': 'date-time', 'help': 'Timestamp specifying when when this update occurred.', 'type': 'string'}, 'updated_by': {'description': 'Information about the person/organization/network that performed the update.', 'help': 'Information about the person/organization/network that performed the update.', 'type': 'string'}}, 'required': ['timestamp', 'comment'], 'title': 'Updates schema', 'type': 'object'}, 'title': 'Schema list', 'type': 'array'}, formats=['date-time'])])), + ('phenopacket_schema_version', models.CharField(blank=True, help_text='Schema version of the current phenopacket.', max_length=200)), + ('external_references', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='A list of external (non-resource) references.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:schema_list', '$schema': 'http://json-schema.org/draft-07/schema#', 'items': {'$id': 'chord_metadata_service:external_reference_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'An encoding of information about a reference to an external resource.', 'help': 'An encoding of information about a reference to an external resource.', 'properties': {'description': {'description': 'An application-specific free-text description.', 'help': 'An application-specific free-text description.', 'type': 'string'}, 'id': {'description': 'An application-specific identifier. It is RECOMMENDED that this is a CURIE that uniquely identifies the evidence source when combined with a resource; e.g. PMID:123456 with a resource `pmid`. It could also be a URI or other relevant identifier.', 'help': 'An application-specific identifier. It is RECOMMENDED that this is a CURIE that uniquely identifies the evidence source when combined with a resource; e.g. PMID:123456 with a resource `pmid`. It could also be a URI or other relevant identifier.', 'type': 'string'}}, 'required': ['id'], 'title': 'External reference schema', 'type': 'object'}, 'title': 'Schema list', 'type': 'array'}, formats=None)])), + ('extra_properties', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema.', null=True)), + ('updated', models.DateTimeField(auto_now_add=True)), + ('resources', models.ManyToManyField(help_text='A list of resources or ontologies referenced in the phenopacket', to='resources.Resource')), + ], + ), + migrations.CreateModel( + name='Phenopacket', + fields=[ + ('id', models.CharField(help_text='Unique, arbitrary, researcher-specified identifier for the phenopacket.', max_length=200, primary_key=True, serialize=False)), + ('extra_properties', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema.', null=True)), + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ('biosamples', models.ManyToManyField(blank=True, help_text='Samples (e.g. biopsies) taken from the individual, if any.', to='phenopackets.Biosample')), + ('diseases', models.ManyToManyField(blank=True, help_text='A list of diseases diagnosed in the proband.', to='phenopackets.Disease')), + ('genes', models.ManyToManyField(blank=True, help_text='Genes deemed to be relevant to the case; application-specific.', to='phenopackets.Gene')), + ('hts_files', models.ManyToManyField(blank=True, help_text='A list of HTS files derived from the individual.', to='phenopackets.HtsFile')), + ('meta_data', models.ForeignKey(help_text='A structured definition of the resources and ontologies used within a phenopacket.', on_delete=django.db.models.deletion.CASCADE, to='phenopackets.MetaData')), + ('subject', models.ForeignKey(help_text='A subject of a phenopacket, representing either a human (typically) or another organism.', on_delete=django.db.models.deletion.CASCADE, related_name='phenopackets', to='patients.Individual')), + ('table', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='chord.Table')), + ], + bases=(models.Model, chord_metadata_service.restapi.models.IndexableMixin), + ), + migrations.CreateModel( + name='Procedure', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', django.contrib.postgres.fields.jsonb.JSONField(help_text='An ontology term that represents a clinical procedure performed on a subject.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), + ('body_site', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term that is specified when it is not possible to represent the procedure with a single ontology class.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), + ('extra_properties', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema.', null=True)), + ('created', models.DateTimeField(auto_now=True)), + ('updated', models.DateTimeField(auto_now_add=True)), + ], + ), + migrations.CreateModel( + name='Variant', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('allele_type', models.CharField(choices=[('hgvsAllele', 'hgvsAllele'), ('vcfAllele', 'vcfAllele'), ('spdiAllele', 'spdiAllele'), ('iscnAllele', 'iscnAllele')], help_text='One of four allele types.', max_length=200)), + ('allele', django.contrib.postgres.fields.jsonb.JSONField(help_text="The variant's corresponding allele", validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:allele_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'dependencies': {'genome_assembly': ['chr', 'pos', 'ref', 'alt', 'info'], 'seq_id': ['position', 'deleted_sequence', 'inserted_sequence']}, 'description': 'Variant allele types', 'oneOf': [{'required': ['hgvs']}, {'required': ['genome_assembly']}, {'required': ['seq_id']}, {'required': ['iscn']}], 'properties': {'alt': {'description': 'The alternate base(s).', 'help': 'The alternate base(s).', 'type': 'string'}, 'chr': {'description': 'A chromosome identifier e.g. chr2 or 2.', 'help': 'A chromosome identifier e.g. chr2 or 2.', 'type': 'string'}, 'deleted_sequence': {'description': 'Deleted sequence , sequence for the deletion, can be empty, e.g. A', 'help': 'Deleted sequence , sequence for the deletion, can be empty, e.g. A', 'type': 'string'}, 'genome_assembly': {'description': 'The reference genome identifier e.g. GRCh38.', 'help': 'The reference genome identifier e.g. GRCh38.', 'type': 'string'}, 'hgvs': {'description': '', 'help': '', 'type': 'string'}, 'id': {'description': 'An arbitrary identifier.', 'help': 'An arbitrary identifier.', 'type': 'string'}, 'info': {'description': 'Relevant parts of the INFO field.', 'help': 'Relevant parts of the INFO field.', 'type': 'string'}, 'inserted_sequence': {'description': 'Inserted sequence , sequence for the insertion, can be empty, e.g. G', 'help': 'Inserted sequence , sequence for the insertion, can be empty, e.g. G', 'type': 'string'}, 'iscn': {'description': 'E.g. t(8;9;11)(q12;p24;p12).', 'help': 'E.g. t(8;9;11)(q12;p24;p12).', 'type': 'string'}, 'pos': {'description': 'The 1-based genomic position e.g. 134327882.', 'help': 'The 1-based genomic position e.g. 134327882.', 'type': 'integer'}, 'position': {'description': 'Position , a 0-based coordinate for where the Deleted Sequence starts, e.g. 4.', 'help': 'Position , a 0-based coordinate for where the Deleted Sequence starts, e.g. 4.', 'type': 'integer'}, 'ref': {'description': 'The reference base(s).', 'help': 'The reference base(s).', 'type': 'string'}, 'seq_id': {'description': 'Sequence ID, e.g. Seq1.', 'help': 'Sequence ID, e.g. Seq1.', 'type': 'string'}}, 'title': 'Allele schema', 'type': 'object'}, formats=None)])), + ('zygosity', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term taken from the Genotype Ontology (GENO) representing the zygosity of the variant.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), + ('extra_properties', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema.', null=True)), + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ], + ), + migrations.CreateModel( + name='PhenotypicFeature', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('description', models.CharField(blank=True, help_text='Human-readable text describing the phenotypic feature; NOT for structured text.', max_length=200)), + ('pftype', django.contrib.postgres.fields.jsonb.JSONField(help_text='An ontology term which describes the phenotype.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)], verbose_name='type')), + ('negated', models.BooleanField(default=False, help_text='Whether the feature is present (false) or absent (true, feature is negated); default is false.')), + ('severity', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term that describes the severity of the condition.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), + ('modifier', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='A list of ontology terms that provide more expressive / precise descriptions of a phenotypic feature, including e.g. positionality or external factors.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)])), + ('onset', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term that describes the age at which the phenotypic feature was first noticed or diagnosed, e.g. HP:0003674.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)])), + ('evidence', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='One or more pieces of evidence that specify how the phenotype was determined.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:evidence_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'A representation of the evidence for an assertion, such as an observation of a phenotypic feature.', 'help': 'A representation of the evidence for an assertion, such as an observation of a phenotypic feature.', 'properties': {'evidence_code': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term that represents the evidence type.', 'help': 'An ontology term that represents the evidence type.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term that represents the evidence type.', 'help': 'A CURIE-style identifier for an ontology term that represents the evidence type.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term that represents the evidence type.', 'help': 'A human readable class name for an ontology term that represents the evidence type.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'reference': {'$id': 'chord_metadata_service:external_reference_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'An encoding of information about a reference to an external resource.', 'help': 'An encoding of information about a reference to an external resource.', 'properties': {'description': {'description': 'An application-specific free-text description.', 'help': 'An application-specific free-text description.', 'type': 'string'}, 'id': {'description': 'An application-specific identifier. It is RECOMMENDED that this is a CURIE that uniquely identifies the evidence source when combined with a resource; e.g. PMID:123456 with a resource `pmid`. It could also be a URI or other relevant identifier.', 'help': 'An application-specific identifier. It is RECOMMENDED that this is a CURIE that uniquely identifies the evidence source when combined with a resource; e.g. PMID:123456 with a resource `pmid`. It could also be a URI or other relevant identifier.', 'type': 'string'}}, 'required': ['id'], 'title': 'External reference schema', 'type': 'object'}}, 'required': ['evidence_code'], 'title': 'Evidence schema', 'type': 'object'}, formats=None)])), + ('extra_properties', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema.', null=True)), + ('created', models.DateTimeField(auto_now=True)), + ('updated', models.DateTimeField(auto_now_add=True)), + ('biosample', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='phenotypic_features', to='phenopackets.Biosample')), + ('phenopacket', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='phenotypic_features', to='phenopackets.Phenopacket')), + ], + bases=(models.Model, chord_metadata_service.restapi.models.IndexableMixin), + ), + migrations.AddField( + model_name='phenopacket', + name='variants', + field=models.ManyToManyField(blank=True, help_text='A list of variants identified in the proband.', to='phenopackets.Variant'), + ), + migrations.CreateModel( + name='Interpretation', + fields=[ + ('id', models.CharField(help_text='An arbitrary identifier for the interpretation.', max_length=200, primary_key=True, serialize=False)), + ('resolution_status', models.CharField(blank=True, choices=[('UNKNOWN', 'UNKNOWN'), ('SOLVED', 'SOLVED'), ('UNSOLVED', 'UNSOLVED'), ('IN_PROGRESS', 'IN_PROGRESS')], help_text='The current status of work on the case.', max_length=200)), + ('extra_properties', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema', null=True)), + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ('diagnosis', models.ManyToManyField(help_text='One or more diagnoses, if made.', to='phenopackets.Diagnosis')), + ('meta_data', models.ForeignKey(help_text='Metadata about this interpretation.', on_delete=django.db.models.deletion.CASCADE, to='phenopackets.MetaData')), + ('phenopacket', models.ForeignKey(help_text='The subject of this interpretation.', on_delete=django.db.models.deletion.CASCADE, related_name='interpretations', to='phenopackets.Phenopacket')), + ], + ), + migrations.CreateModel( + name='GenomicInterpretation', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(choices=[('UNKNOWN', 'UNKNOWN'), ('REJECTED', 'REJECTED'), ('CANDIDATE', 'CANDIDATE'), ('CAUSATIVE', 'CAUSATIVE')], help_text='How the call of this GenomicInterpretation was interpreted.', max_length=200)), + ('extra_properties', django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema', null=True)), + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ('gene', models.ForeignKey(blank=True, help_text='The gene contributing to the diagnosis.', null=True, on_delete=django.db.models.deletion.CASCADE, to='phenopackets.Gene')), + ('variant', models.ForeignKey(blank=True, help_text='The variant contributing to the diagnosis.', null=True, on_delete=django.db.models.deletion.CASCADE, to='phenopackets.Variant')), + ], + ), + migrations.AddField( + model_name='diagnosis', + name='disease', + field=models.ForeignKey(help_text='The diagnosed condition.', on_delete=django.db.models.deletion.CASCADE, to='phenopackets.Disease'), + ), + migrations.AddField( + model_name='diagnosis', + name='genomic_interpretations', + field=models.ManyToManyField(blank=True, help_text='The genomic elements assessed as being responsible for the disease.', to='phenopackets.GenomicInterpretation'), + ), + migrations.AddField( + model_name='biosample', + name='hts_files', + field=models.ManyToManyField(blank=True, help_text='A list of HTS files derived from the biosample.', related_name='biosample_hts_files', to='phenopackets.HtsFile'), + ), + migrations.AddField( + model_name='biosample', + name='individual', + field=models.ForeignKey(blank=True, help_text='Identifier for the individual this biosample was sampled from.', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='biosamples', to='patients.Individual'), + ), + migrations.AddField( + model_name='biosample', + name='procedure', + field=models.ForeignKey(help_text='A description of a clinical procedure performed on a subject in order to extract a biosample.', on_delete=django.db.models.deletion.CASCADE, to='phenopackets.Procedure'), + ), + migrations.AddField( + model_name='biosample', + name='variants', + field=models.ManyToManyField(blank=True, help_text='A list of variants determined to be present in the biosample.', to='phenopackets.Variant'), + ), + ] diff --git a/chord_metadata_service/phenopackets/migrations/0002_auto_20200121_1659.py b/chord_metadata_service/phenopackets/migrations/0002_auto_20200121_1659.py deleted file mode 100644 index 5c991046f..000000000 --- a/chord_metadata_service/phenopackets/migrations/0002_auto_20200121_1659.py +++ /dev/null @@ -1,337 +0,0 @@ -# Generated by Django 2.2.9 on 2020-01-21 16:59 - -import django.contrib.postgres.fields -import django.contrib.postgres.fields.jsonb -from django.db import migrations, models -import django.db.models.deletion -import django.utils.timezone - - -class Migration(migrations.Migration): - - dependencies = [ - ('phenopackets', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='biosample', - name='description', - field=models.CharField(blank=True, help_text='Human-readable, unstructured text describing the biosample or providing additional information.', max_length=200), - ), - migrations.AlterField( - model_name='biosample', - name='diagnostic_markers', - field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True), blank=True, help_text='A list of ontology terms representing clinically-relevant bio-markers.', null=True, size=None), - ), - migrations.AlterField( - model_name='biosample', - name='extra_properties', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema.', null=True), - ), - migrations.AlterField( - model_name='biosample', - name='histological_diagnosis', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term representing a refinement of the clinical diagnosis. Normal samples could be tagged with NCIT:C38757, representing a negative finding', null=True), - ), - migrations.AlterField( - model_name='biosample', - name='hts_files', - field=models.ManyToManyField(blank=True, help_text='A list of HTS files derived from the biosample.', related_name='biosample_hts_files', to='phenopackets.HtsFile'), - ), - migrations.AlterField( - model_name='biosample', - name='id', - field=models.CharField(help_text='Unique arbitrary, researcher-specified identifier for the biosample.', max_length=200, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='biosample', - name='individual', - field=models.ForeignKey(blank=True, help_text='Identifier for the individual this biosample was sampled from.', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='biosamples', to='patients.Individual'), - ), - migrations.AlterField( - model_name='biosample', - name='individual_age_at_collection', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='individual_age_at_collection', null=True), - ), - migrations.AlterField( - model_name='biosample', - name='procedure', - field=models.ForeignKey(help_text='A description of a clinical procedure performed on a subject in order to extract a biosample.', on_delete=django.db.models.deletion.CASCADE, to='phenopackets.Procedure'), - ), - migrations.AlterField( - model_name='biosample', - name='sampled_tissue', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text='An ontology term describing the tissue from which the specimen was collected. The use of UBERON is recommended'), - ), - migrations.AlterField( - model_name='biosample', - name='taxonomy', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term specified when more than one organism may be studied. It is advised that codesfrom the NCBI Taxonomy resource are used, e.g. NCBITaxon:9606 for humans', null=True), - ), - migrations.AlterField( - model_name='biosample', - name='tumor_grade', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term representing the tumour grade. This should be a child term of NCIT:C28076 (Disease Grade Qualifier) or equivalent', null=True), - ), - migrations.AlterField( - model_name='biosample', - name='tumor_progression', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term representing if the specimen is from a primary tumour, a metastasis, or a recurrence. There are multiple ways of representing this using ontology terms, and the terms chosen will have a specific meaning that is application specific.', null=True), - ), - migrations.AlterField( - model_name='biosample', - name='variants', - field=models.ManyToManyField(blank=True, help_text='A list of variants determined to be present in the biosample.', to='phenopackets.Variant'), - ), - migrations.AlterField( - model_name='disease', - name='disease_stage', - field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True), blank=True, help_text='A list of terms representing the disease stage. Elements should be derived from child terms of NCIT:C28108 (Disease Stage Qualifier) or equivalent hierarchy from another ontology.', null=True, size=None), - ), - migrations.AlterField( - model_name='disease', - name='extra_properties', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema.', null=True), - ), - migrations.AlterField( - model_name='disease', - name='onset', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='A representation of the age of onset of the disease', null=True), - ), - migrations.AlterField( - model_name='disease', - name='term', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text="An ontology term that represents the disease. It's recommended that one of the OMIM, Orphanet, or MONDO ontologies is used for rare human diseases"), - ), - migrations.AlterField( - model_name='disease', - name='tnm_finding', - field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True), blank=True, help_text='A list of terms representing the tumour TNM score. Elements should be derived from child terms of NCIT:C48232 (Cancer TNM Finding) or equivalent hierarchy from another ontology.', null=True, size=None), - ), - migrations.AlterField( - model_name='gene', - name='alternate_ids', - field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=200), blank=True, help_text='A list of identifiers for alternative resources where the gene is used or catalogued.', null=True, size=None), - ), - migrations.AlterField( - model_name='gene', - name='extra_properties', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema.', null=True), - ), - migrations.AlterField( - model_name='gene', - name='id', - field=models.CharField(help_text='Official identifier of the gene. It SHOULD be a CURIE identifier with a prefix used by the official organism gene nomenclature committee, e.g. HGNC:347 for humans.', max_length=200, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='gene', - name='symbol', - field=models.CharField(help_text="A gene's official gene symbol as designated by the organism's gene nomenclature committee, e.g. ETF1 from the HUGO Gene Nomenclature committee.", max_length=200), - ), - migrations.AlterField( - model_name='htsfile', - name='description', - field=models.CharField(blank=True, help_text='Human-readable text describing the file.', max_length=200), - ), - migrations.AlterField( - model_name='htsfile', - name='extra_properties', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema.', null=True), - ), - migrations.AlterField( - model_name='htsfile', - name='genome_assembly', - field=models.CharField(help_text='Genome assembly ID for the file, e.g. GRCh38.', max_length=200), - ), - migrations.AlterField( - model_name='htsfile', - name='hts_format', - field=models.CharField(choices=[('UNKNOWN', 'UNKNOWN'), ('SAM', 'SAM'), ('BAM', 'BAM'), ('CRAM', 'CRAM'), ('VCF', 'VCF'), ('BCF', 'BCF'), ('GVCF', 'GVCF')], help_text="The file's format; one of SAM, BAM, CRAM, VCF, BCF, GVCF, FASTQ, or UNKNOWN.", max_length=200), - ), - migrations.AlterField( - model_name='htsfile', - name='individual_to_sample_identifiers', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Mapping between individual or biosample IDs and the sample identifier in the HTS file.', null=True), - ), - migrations.AlterField( - model_name='htsfile', - name='uri', - field=models.URLField(help_text='A valid URI to the file', primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='metadata', - name='created', - field=models.DateTimeField(default=django.utils.timezone.now, help_text='Timestamp specifying when when this object was created.'), - ), - migrations.AlterField( - model_name='metadata', - name='created_by', - field=models.CharField(help_text='Name of the person who created the phenopacket.', max_length=200), - ), - migrations.AlterField( - model_name='metadata', - name='external_references', - field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True), blank=True, help_text='A list of external (non-resource) references.', null=True, size=None), - ), - migrations.AlterField( - model_name='metadata', - name='extra_properties', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema.', null=True), - ), - migrations.AlterField( - model_name='metadata', - name='resources', - field=models.ManyToManyField(help_text='A list of resources or ontologies referenced in the phenopacket', to='phenopackets.Resource'), - ), - migrations.AlterField( - model_name='metadata', - name='submitted_by', - field=models.CharField(blank=True, help_text='Name of the person who submitted the phenopacket.', max_length=200), - ), - migrations.AlterField( - model_name='metadata', - name='updates', - field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True), blank=True, help_text='A list of updates to the phenopacket.', null=True, size=None), - ), - migrations.AlterField( - model_name='phenopacket', - name='biosamples', - field=models.ManyToManyField(blank=True, help_text='Samples (e.g. biopsies) taken from the individual, if any.', to='phenopackets.Biosample'), - ), - migrations.AlterField( - model_name='phenopacket', - name='diseases', - field=models.ManyToManyField(blank=True, help_text='A list of diseases diagnosed in the proband.', to='phenopackets.Disease'), - ), - migrations.AlterField( - model_name='phenopacket', - name='extra_properties', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema.', null=True), - ), - migrations.AlterField( - model_name='phenopacket', - name='genes', - field=models.ManyToManyField(blank=True, help_text='Genes deemed to be relevant to the case; application-specific.', to='phenopackets.Gene'), - ), - migrations.AlterField( - model_name='phenopacket', - name='hts_files', - field=models.ManyToManyField(blank=True, help_text='A list of HTS files derived from the individual.', to='phenopackets.HtsFile'), - ), - migrations.AlterField( - model_name='phenopacket', - name='id', - field=models.CharField(help_text='Unique, arbitrary, researcher-specified identifier for the phenopacket.', max_length=200, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='phenopacket', - name='meta_data', - field=models.ForeignKey(help_text='A structured definition of the resources and ontologies used within a phenopacket.', on_delete=django.db.models.deletion.CASCADE, to='phenopackets.MetaData'), - ), - migrations.AlterField( - model_name='phenopacket', - name='subject', - field=models.ForeignKey(help_text='A subject of a phenopacket, representing either a human (typically) or another organism.', on_delete=django.db.models.deletion.CASCADE, related_name='phenopackets', to='patients.Individual'), - ), - migrations.AlterField( - model_name='phenopacket', - name='variants', - field=models.ManyToManyField(blank=True, help_text='A list of variants identified in the proband.', to='phenopackets.Variant'), - ), - migrations.AlterField( - model_name='phenotypicfeature', - name='description', - field=models.CharField(blank=True, help_text='Human-readable text describing the phenotypic feature; NOT for structured text.', max_length=200), - ), - migrations.AlterField( - model_name='phenotypicfeature', - name='evidence', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='One or more pieces of evidence that specify how the phenotype was determined.', null=True), - ), - migrations.AlterField( - model_name='phenotypicfeature', - name='extra_properties', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema.', null=True), - ), - migrations.AlterField( - model_name='phenotypicfeature', - name='modifier', - field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True), blank=True, help_text='A list of ontology terms that provide more expressive / precise descriptions of a phenotypic feature, including e.g. positionality or external factors.', null=True, size=None), - ), - migrations.AlterField( - model_name='phenotypicfeature', - name='negated', - field=models.BooleanField(default=False, help_text='Whether the feature is present (false) or absent (true, feature is negated); default is false.'), - ), - migrations.AlterField( - model_name='phenotypicfeature', - name='onset', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term that describes the age at which the phenotypic feature was first noticed or diagnosed, e.g. HP:0003674', null=True), - ), - migrations.AlterField( - model_name='phenotypicfeature', - name='pftype', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text='An ontology term which describes the phenotype', verbose_name='type'), - ), - migrations.AlterField( - model_name='phenotypicfeature', - name='severity', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term that describes the severity of the condition', null=True), - ), - migrations.AlterField( - model_name='procedure', - name='body_site', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term that is specified when it is not possible to represent the procedure with a single ontology class', null=True), - ), - migrations.AlterField( - model_name='procedure', - name='code', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text='An ontology term that represents a clinical procedure performed on a subject'), - ), - migrations.AlterField( - model_name='procedure', - name='extra_properties', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema.', null=True), - ), - migrations.AlterField( - model_name='resource', - name='extra_properties', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema.', null=True), - ), - migrations.AlterField( - model_name='resource', - name='iri_prefix', - field=models.URLField(help_text='The IRI prefix, when used with the namespace prefix and an object ID, should resolve the term or object from the resource in question.'), - ), - migrations.AlterField( - model_name='resource', - name='name', - field=models.CharField(help_text='The full name of the resource or ontology referred to by the id element.', max_length=200), - ), - migrations.AlterField( - model_name='resource', - name='namespace_prefix', - field=models.CharField(help_text='Prefix for objects from this resource. In the case of ontology resources, this should be the CURIE prefix.', max_length=200), - ), - migrations.AlterField( - model_name='resource', - name='url', - field=models.URLField(help_text='Resource URL. In the case of ontologies, this should be an OBO or OWL file. Other resources should link to the official or top-level url.'), - ), - migrations.AlterField( - model_name='variant', - name='allele', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text="The variant's corresponding allele"), - ), - migrations.AlterField( - model_name='variant', - name='extra_properties', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='Extra properties that are not supported by current schema.', null=True), - ), - migrations.AlterField( - model_name='variant', - name='zygosity', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term taken from the Genotype Ontology (GENO) representing the zygosity of the variant', null=True), - ), - ] diff --git a/chord_metadata_service/phenopackets/migrations/0003_auto_20200121_1956.py b/chord_metadata_service/phenopackets/migrations/0003_auto_20200121_1956.py deleted file mode 100644 index e8263d6a2..000000000 --- a/chord_metadata_service/phenopackets/migrations/0003_auto_20200121_1956.py +++ /dev/null @@ -1,74 +0,0 @@ -# Generated by Django 2.2.9 on 2020-01-21 19:56 - -import django.contrib.postgres.fields.jsonb -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('phenopackets', '0002_auto_20200121_1659'), - ] - - operations = [ - migrations.AlterField( - model_name='biosample', - name='histological_diagnosis', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term representing a refinement of the clinical diagnosis. Normal samples could be tagged with NCIT:C38757, representing a negative finding.', null=True), - ), - migrations.AlterField( - model_name='biosample', - name='sampled_tissue', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text='An ontology term describing the tissue from which the specimen was collected. The use of UBERON is recommended.'), - ), - migrations.AlterField( - model_name='biosample', - name='taxonomy', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term specified when more than one organism may be studied. It is advised that codesfrom the NCBI Taxonomy resource are used, e.g. NCBITaxon:9606 for humans.', null=True), - ), - migrations.AlterField( - model_name='biosample', - name='tumor_grade', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term representing the tumour grade. This should be a child term of NCIT:C28076 (Disease Grade Qualifier) or equivalent.', null=True), - ), - migrations.AlterField( - model_name='biosample', - name='tumor_progression', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term representing if the specimen is from a primary tumour, a metastasis, or a recurrence. There are multiple ways of representing this using ontology terms, and the terms chosen will have a specific meaning that is application specific..', null=True), - ), - migrations.AlterField( - model_name='disease', - name='term', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text="An ontology term that represents the disease. It's recommended that one of the OMIM, Orphanet, or MONDO ontologies is used for rare human diseases."), - ), - migrations.AlterField( - model_name='phenotypicfeature', - name='onset', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term that describes the age at which the phenotypic feature was first noticed or diagnosed, e.g. HP:0003674.', null=True), - ), - migrations.AlterField( - model_name='phenotypicfeature', - name='pftype', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text='An ontology term which describes the phenotype.', verbose_name='type'), - ), - migrations.AlterField( - model_name='phenotypicfeature', - name='severity', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term that describes the severity of the condition.', null=True), - ), - migrations.AlterField( - model_name='procedure', - name='body_site', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term that is specified when it is not possible to represent the procedure with a single ontology class.', null=True), - ), - migrations.AlterField( - model_name='procedure', - name='code', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text='An ontology term that represents a clinical procedure performed on a subject.'), - ), - migrations.AlterField( - model_name='variant', - name='zygosity', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term taken from the Genotype Ontology (GENO) representing the zygosity of the variant.', null=True), - ), - ] diff --git a/chord_metadata_service/phenopackets/migrations/0004_auto_20200129_1537.py b/chord_metadata_service/phenopackets/migrations/0004_auto_20200129_1537.py deleted file mode 100644 index 11a02e191..000000000 --- a/chord_metadata_service/phenopackets/migrations/0004_auto_20200129_1537.py +++ /dev/null @@ -1,83 +0,0 @@ -# Generated by Django 2.2.9 on 2020-01-29 15:37 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('phenopackets', '0003_auto_20200121_1956'), - ] - - operations = [ - migrations.AlterField( - model_name='biosample', - name='created', - field=models.DateTimeField(auto_now_add=True), - ), - migrations.AlterField( - model_name='biosample', - name='updated', - field=models.DateTimeField(auto_now=True), - ), - migrations.AlterField( - model_name='diagnosis', - name='created', - field=models.DateTimeField(auto_now_add=True), - ), - migrations.AlterField( - model_name='diagnosis', - name='updated', - field=models.DateTimeField(auto_now=True), - ), - migrations.AlterField( - model_name='disease', - name='created', - field=models.DateTimeField(auto_now_add=True), - ), - migrations.AlterField( - model_name='disease', - name='updated', - field=models.DateTimeField(auto_now=True), - ), - migrations.AlterField( - model_name='genomicinterpretation', - name='created', - field=models.DateTimeField(auto_now_add=True), - ), - migrations.AlterField( - model_name='genomicinterpretation', - name='updated', - field=models.DateTimeField(auto_now=True), - ), - migrations.AlterField( - model_name='interpretation', - name='created', - field=models.DateTimeField(auto_now_add=True), - ), - migrations.AlterField( - model_name='interpretation', - name='updated', - field=models.DateTimeField(auto_now=True), - ), - migrations.AlterField( - model_name='phenopacket', - name='created', - field=models.DateTimeField(auto_now_add=True), - ), - migrations.AlterField( - model_name='phenopacket', - name='updated', - field=models.DateTimeField(auto_now=True), - ), - migrations.AlterField( - model_name='variant', - name='created', - field=models.DateTimeField(auto_now_add=True), - ), - migrations.AlterField( - model_name='variant', - name='updated', - field=models.DateTimeField(auto_now=True), - ), - ] diff --git a/chord_metadata_service/phenopackets/migrations/0005_auto_20200428_1633.py b/chord_metadata_service/phenopackets/migrations/0005_auto_20200428_1633.py deleted file mode 100644 index e4cd3544c..000000000 --- a/chord_metadata_service/phenopackets/migrations/0005_auto_20200428_1633.py +++ /dev/null @@ -1,126 +0,0 @@ -# Generated by Django 2.2.12 on 2020-04-28 20:33 - -import chord_metadata_service.restapi.validators -import django.contrib.postgres.fields -import django.contrib.postgres.fields.jsonb -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('phenopackets', '0004_auto_20200129_1537'), - ] - - operations = [ - migrations.AlterField( - model_name='biosample', - name='diagnostic_markers', - field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'})]), blank=True, help_text='A list of ontology terms representing clinically-relevant bio-markers.', null=True, size=None), - ), - migrations.AlterField( - model_name='biosample', - name='histological_diagnosis', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term representing a refinement of the clinical diagnosis. Normal samples could be tagged with NCIT:C38757, representing a negative finding.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'})]), - ), - migrations.AlterField( - model_name='biosample', - name='individual_age_at_collection', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='individual_age_at_collection', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:age_or_age_range_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An age object describing the age of the individual at the time of collection of biospecimens or phenotypic observations.', 'properties': {'age': {'anyOf': [{'description': 'An ISO8601 string represent age.', 'type': 'string'}, {'$id': 'chord_metadata_service:age_range_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An age range of a subject.', 'properties': {'end': {'properties': {'age': {'description': 'An ISO8601 string represent age.', 'type': 'string'}}, 'type': 'object'}, 'start': {'properties': {'age': {'description': 'An ISO8601 string represent age.', 'type': 'string'}}, 'type': 'object'}}, 'required': ['start', 'end'], 'title': 'Age schema', 'type': 'object'}]}}, 'title': 'Age schema', 'type': 'object'})]), - ), - migrations.AlterField( - model_name='biosample', - name='sampled_tissue', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text='An ontology term describing the tissue from which the specimen was collected. The use of UBERON is recommended.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'})]), - ), - migrations.AlterField( - model_name='biosample', - name='taxonomy', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term specified when more than one organism may be studied. It is advised that codesfrom the NCBI Taxonomy resource are used, e.g. NCBITaxon:9606 for humans.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'})]), - ), - migrations.AlterField( - model_name='biosample', - name='tumor_grade', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term representing the tumour grade. This should be a child term of NCIT:C28076 (Disease Grade Qualifier) or equivalent.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'})]), - ), - migrations.AlterField( - model_name='biosample', - name='tumor_progression', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term representing if the specimen is from a primary tumour, a metastasis, or a recurrence. There are multiple ways of representing this using ontology terms, and the terms chosen will have a specific meaning that is application specific..', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'})]), - ), - migrations.AlterField( - model_name='disease', - name='disease_stage', - field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'})]), blank=True, help_text='A list of terms representing the disease stage. Elements should be derived from child terms of NCIT:C28108 (Disease Stage Qualifier) or equivalent hierarchy from another ontology.', null=True, size=None), - ), - migrations.AlterField( - model_name='disease', - name='onset', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='A representation of the age of onset of the disease', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:disease_onset_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema for the age of the onset of the disease.', 'properties': {'age': {'anyOf': [{'description': 'An ISO8601 string represent age.', 'type': 'string'}, {'$id': 'chord_metadata_service:age_range_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An age range of a subject.', 'properties': {'end': {'properties': {'age': {'description': 'An ISO8601 string represent age.', 'type': 'string'}}, 'type': 'object'}, 'start': {'properties': {'age': {'description': 'An ISO8601 string represent age.', 'type': 'string'}}, 'type': 'object'}}, 'required': ['start', 'end'], 'title': 'Age schema', 'type': 'object'}, {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}]}}, 'title': 'Onset age', 'type': 'object'})]), - ), - migrations.AlterField( - model_name='disease', - name='term', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text="An ontology term that represents the disease. It's recommended that one of the OMIM, Orphanet, or MONDO ontologies is used for rare human diseases.", validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'})]), - ), - migrations.AlterField( - model_name='disease', - name='tnm_finding', - field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'})]), blank=True, help_text='A list of terms representing the tumour TNM score. Elements should be derived from child terms of NCIT:C48232 (Cancer TNM Finding) or equivalent hierarchy from another ontology.', null=True, size=None), - ), - migrations.AlterField( - model_name='metadata', - name='external_references', - field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:external_reference_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'The schema encodes information about an external reference.', 'properties': {'description': {'description': 'An application specific description.', 'type': 'string'}, 'id': {'description': 'An application specific identifier.', 'type': 'string'}}, 'required': ['id'], 'title': 'External reference schema', 'type': 'object'})]), blank=True, help_text='A list of external (non-resource) references.', null=True, size=None), - ), - migrations.AlterField( - model_name='metadata', - name='updates', - field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:update_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to check incoming updates format', 'properties': {'comment': {'description': 'Comment about updates or reasons for an update.', 'type': 'string'}, 'timestamp': {'description': 'ISO8601 UTC timestamp at which this record was updated.', 'format': 'date-time', 'type': 'string'}, 'updated_by': {'description': 'Who updated the phenopacket', 'type': 'string'}}, 'required': ['timestamp', 'comment'], 'title': 'Updates schema', 'type': 'object'})]), blank=True, help_text='A list of updates to the phenopacket.', null=True, size=None), - ), - migrations.AlterField( - model_name='phenotypicfeature', - name='evidence', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='One or more pieces of evidence that specify how the phenotype was determined.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:evidence_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'The schema represents the evidence for an assertion such as an observation of a PhenotypicFeature.', 'properties': {'evidence_code': {'additionalProperties': False, 'description': 'An ontology class that represents the evidence type.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'type': 'object'}, 'reference': {'additionalProperties': False, 'description': 'Representation of the source of the evidence.', 'properties': {'description': {'description': 'An application specific description.', 'type': 'string'}, 'id': {'description': 'An application specific identifier.', 'type': 'string'}}, 'required': ['id'], 'type': 'object'}}, 'required': ['evidence_code'], 'title': 'Evidence schema', 'type': 'object'})]), - ), - migrations.AlterField( - model_name='phenotypicfeature', - name='modifier', - field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'})]), blank=True, help_text='A list of ontology terms that provide more expressive / precise descriptions of a phenotypic feature, including e.g. positionality or external factors.', null=True, size=None), - ), - migrations.AlterField( - model_name='phenotypicfeature', - name='onset', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term that describes the age at which the phenotypic feature was first noticed or diagnosed, e.g. HP:0003674.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'})]), - ), - migrations.AlterField( - model_name='phenotypicfeature', - name='pftype', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text='An ontology term which describes the phenotype.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'})], verbose_name='type'), - ), - migrations.AlterField( - model_name='phenotypicfeature', - name='severity', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term that describes the severity of the condition.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'})]), - ), - migrations.AlterField( - model_name='procedure', - name='body_site', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term that is specified when it is not possible to represent the procedure with a single ontology class.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'})]), - ), - migrations.AlterField( - model_name='procedure', - name='code', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text='An ontology term that represents a clinical procedure performed on a subject.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'})]), - ), - migrations.AlterField( - model_name='variant', - name='allele', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text="The variant's corresponding allele", validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:allele_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'dependencies': {'genome_assembly': ['chr', 'pos', 're', 'alt', 'info'], 'seq_id': ['position', 'deleted_sequence', 'inserted_sequence']}, 'description': 'Variant allele types', 'oneOf': [{'required': ['hgvs']}, {'required': ['genome_assembly']}, {'required': ['seq_id']}, {'required': ['iscn']}], 'properties': {'alt': {'type': 'string'}, 'chr': {'type': 'string'}, 'deleted_sequence': {'type': 'string'}, 'genome_assembly': {'type': 'string'}, 'hgvs': {'type': 'string'}, 'id': {'type': 'string'}, 'info': {'type': 'string'}, 'inserted_sequence': {'type': 'string'}, 'iscn': {'type': 'string'}, 'pos': {'type': 'integer'}, 'position': {'type': 'integer'}, 're': {'type': 'string'}, 'seq_id': {'type': 'string'}}, 'title': 'Allele schema', 'type': 'object'})]), - ), - migrations.AlterField( - model_name='variant', - name='zygosity', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term taken from the Genotype Ontology (GENO) representing the zygosity of the variant.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'})]), - ), - ] diff --git a/chord_metadata_service/phenopackets/migrations/0006_auto_20200430_1444.py b/chord_metadata_service/phenopackets/migrations/0006_auto_20200430_1444.py deleted file mode 100644 index 6e32d8507..000000000 --- a/chord_metadata_service/phenopackets/migrations/0006_auto_20200430_1444.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 2.2.12 on 2020-04-30 14:44 - -import chord_metadata_service.restapi.validators -import django.contrib.postgres.fields.jsonb -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('phenopackets', '0005_auto_20200428_1633'), - ] - - operations = [ - migrations.AlterField( - model_name='biosample', - name='individual_age_at_collection', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='individual_age_at_collection', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:age_or_age_range_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'An age object describing the age of the individual at the time of collection of biospecimens or phenotypic observations.', 'oneOf': [{'$id': 'chord_metadata_service:age_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An age of a subject.', 'properties': {'age': {'description': 'An ISO8601 string represent age.', 'type': 'string'}}, 'required': ['age'], 'title': 'Age schema', 'type': 'object'}, {'$id': 'chord_metadata_service:age_range_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An age range of a subject.', 'properties': {'end': {'$id': 'chord_metadata_service:age_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An age of a subject.', 'properties': {'age': {'description': 'An ISO8601 string represent age.', 'type': 'string'}}, 'required': ['age'], 'title': 'Age schema', 'type': 'object'}, 'start': {'$id': 'chord_metadata_service:age_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An age of a subject.', 'properties': {'age': {'description': 'An ISO8601 string represent age.', 'type': 'string'}}, 'required': ['age'], 'title': 'Age schema', 'type': 'object'}}, 'required': ['start', 'end'], 'title': 'Age range schema', 'type': 'object'}], 'title': 'Age schema', 'type': 'object'})]), - ), - migrations.AlterField( - model_name='disease', - name='onset', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='A representation of the age of onset of the disease', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:disease_onset_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Schema for the age of the onset of the disease.', 'oneOf': [{'$id': 'chord_metadata_service:age_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An age of a subject.', 'properties': {'age': {'description': 'An ISO8601 string represent age.', 'type': 'string'}}, 'required': ['age'], 'title': 'Age schema', 'type': 'object'}, {'$id': 'chord_metadata_service:age_range_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An age range of a subject.', 'properties': {'end': {'$id': 'chord_metadata_service:age_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An age of a subject.', 'properties': {'age': {'description': 'An ISO8601 string represent age.', 'type': 'string'}}, 'required': ['age'], 'title': 'Age schema', 'type': 'object'}, 'start': {'$id': 'chord_metadata_service:age_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An age of a subject.', 'properties': {'age': {'description': 'An ISO8601 string represent age.', 'type': 'string'}}, 'required': ['age'], 'title': 'Age schema', 'type': 'object'}}, 'required': ['start', 'end'], 'title': 'Age range schema', 'type': 'object'}, {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'todo', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}], 'title': 'Onset age', 'type': 'object'})]), - ), - ] diff --git a/chord_metadata_service/phenopackets/migrations/0007_auto_20200513_1401.py b/chord_metadata_service/phenopackets/migrations/0007_auto_20200513_1401.py deleted file mode 100644 index 4d794cd40..000000000 --- a/chord_metadata_service/phenopackets/migrations/0007_auto_20200513_1401.py +++ /dev/null @@ -1,41 +0,0 @@ -# Generated by Django 2.2.12 on 2020-05-13 18:01 - -import chord_metadata_service.restapi.validators -import django.contrib.postgres.fields -import django.contrib.postgres.fields.jsonb -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('phenopackets', '0006_auto_20200430_1444'), - ] - - operations = [ - migrations.AlterField( - model_name='biosample', - name='individual_age_at_collection', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='individual_age_at_collection', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:age_or_age_range_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'An age object describing the age of the individual at the time of collection of biospecimens or phenotypic observations.', 'oneOf': [{'$id': 'chord_metadata_service:age_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ISO8601 duration string (e.g. P40Y10M05D for 40 years, 10 months, 5 days) representing an age of a subject.', 'help': 'An ISO8601 duration string (e.g. P40Y10M05D for 40 years, 10 months, 5 days) representing an age of a subject.', 'properties': {'age': {'description': 'An ISO8601 duration string (e.g. P40Y10M05D for 40 years, 10 months, 5 days) representing an age of a subject.', 'help': 'Age of a subject.', 'type': 'string'}}, 'required': ['age'], 'title': 'Age schema', 'type': 'object'}, {'$id': 'chord_metadata_service:age_range_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': "Age range of a subject (e.g. when a subject's age falls into a bin.)", 'help': "Age range of a subject (e.g. when a subject's age falls into a bin.)", 'properties': {'end': {'$id': 'chord_metadata_service:age_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ISO8601 duration string representing the end of the age range bin.', 'help': 'An ISO8601 duration string representing the end of the age range bin.', 'properties': {'age': {'description': 'An ISO8601 duration string (e.g. P40Y10M05D for 40 years, 10 months, 5 days) representing an age of a subject.', 'help': 'Age of a subject.', 'type': 'string'}}, 'required': ['age'], 'title': 'Age schema', 'type': 'object'}, 'start': {'$id': 'chord_metadata_service:age_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ISO8601 duration string representing the start of the age range bin.', 'help': 'An ISO8601 duration string representing the start of the age range bin.', 'properties': {'age': {'description': 'An ISO8601 duration string (e.g. P40Y10M05D for 40 years, 10 months, 5 days) representing an age of a subject.', 'help': 'Age of a subject.', 'type': 'string'}}, 'required': ['age'], 'title': 'Age schema', 'type': 'object'}}, 'required': ['start', 'end'], 'title': 'Age range schema', 'type': 'object'}], 'title': 'Age schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='disease', - name='onset', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='A representation of the age of onset of the disease', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:disease_onset_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'anyOf': [{'$id': 'chord_metadata_service:age_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ISO8601 duration string (e.g. P40Y10M05D for 40 years, 10 months, 5 days) representing an age of a subject.', 'help': 'An ISO8601 duration string (e.g. P40Y10M05D for 40 years, 10 months, 5 days) representing an age of a subject.', 'properties': {'age': {'description': 'An ISO8601 duration string (e.g. P40Y10M05D for 40 years, 10 months, 5 days) representing an age of a subject.', 'help': 'Age of a subject.', 'type': 'string'}}, 'required': ['age'], 'title': 'Age schema', 'type': 'object'}, {'$id': 'chord_metadata_service:age_range_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': "Age range of a subject (e.g. when a subject's age falls into a bin.)", 'help': "Age range of a subject (e.g. when a subject's age falls into a bin.)", 'properties': {'end': {'$id': 'chord_metadata_service:age_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ISO8601 duration string representing the end of the age range bin.', 'help': 'An ISO8601 duration string representing the end of the age range bin.', 'properties': {'age': {'description': 'An ISO8601 duration string (e.g. P40Y10M05D for 40 years, 10 months, 5 days) representing an age of a subject.', 'help': 'Age of a subject.', 'type': 'string'}}, 'required': ['age'], 'title': 'Age schema', 'type': 'object'}, 'start': {'$id': 'chord_metadata_service:age_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ISO8601 duration string representing the start of the age range bin.', 'help': 'An ISO8601 duration string representing the start of the age range bin.', 'properties': {'age': {'description': 'An ISO8601 duration string (e.g. P40Y10M05D for 40 years, 10 months, 5 days) representing an age of a subject.', 'help': 'Age of a subject.', 'type': 'string'}}, 'required': ['age'], 'title': 'Age schema', 'type': 'object'}}, 'required': ['start', 'end'], 'title': 'Age range schema', 'type': 'object'}, {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}], 'description': 'Schema for the age of the onset of the disease.', 'title': 'Onset age', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='metadata', - name='external_references', - field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:external_reference_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'An encoding of information about a reference to an external resource.', 'help': 'An encoding of information about a reference to an external resource.', 'properties': {'description': {'description': 'An application-specific free-text description.', 'help': 'An application-specific free-text description.', 'type': 'string'}, 'id': {'description': 'An application-specific identifier. It is RECOMMENDED that this is a CURIE that uniquely identifies the evidence source when combined with a resource; e.g. PMID:123456 with a resource `pmid`. It could also be a URI or other relevant identifier.', 'help': 'An application-specific identifier. It is RECOMMENDED that this is a CURIE that uniquely identifies the evidence source when combined with a resource; e.g. PMID:123456 with a resource `pmid`. It could also be a URI or other relevant identifier.', 'type': 'string'}}, 'required': ['id'], 'title': 'External reference schema', 'type': 'object'}, formats=None)]), blank=True, help_text='A list of external (non-resource) references.', null=True, size=None), - ), - migrations.AlterField( - model_name='metadata', - name='updates', - field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:update_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An update event for a record (e.g. a phenopacket.)', 'help': 'An update event for a record (e.g. a phenopacket.)', 'properties': {'comment': {'description': 'Free-text comment about the changes made and/or the reason for the update.', 'help': 'Free-text comment about the changes made and/or the reason for the update.', 'type': 'string'}, 'timestamp': {'description': 'ISO8601 UTC timestamp specifying when when this update occurred.', 'format': 'date-time', 'help': 'Timestamp specifying when when this update occurred.', 'type': 'string'}, 'updated_by': {'description': 'Information about the person/organization/network that performed the update.', 'help': 'Information about the person/organization/network that performed the update.', 'type': 'string'}}, 'required': ['timestamp', 'comment'], 'title': 'Updates schema', 'type': 'object'}, formats=['date-time'])]), blank=True, help_text='A list of updates to the phenopacket.', null=True, size=None), - ), - migrations.AlterField( - model_name='phenotypicfeature', - name='evidence', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='One or more pieces of evidence that specify how the phenotype was determined.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:evidence_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'A representation of the evidence for an assertion, such as an observation of a phenotypic feature.', 'help': 'A representation of the evidence for an assertion, such as an observation of a phenotypic feature.', 'properties': {'evidence_code': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term that represents the evidence type.', 'help': 'An ontology term that represents the evidence type.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term that represents the evidence type.', 'help': 'A CURIE-style identifier for an ontology term that represents the evidence type.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term that represents the evidence type.', 'help': 'A human readable class name for an ontology term that represents the evidence type.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'reference': {'$id': 'chord_metadata_service:external_reference_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'An encoding of information about a reference to an external resource.', 'help': 'An encoding of information about a reference to an external resource.', 'properties': {'description': {'description': 'An application-specific free-text description.', 'help': 'An application-specific free-text description.', 'type': 'string'}, 'id': {'description': 'An application-specific identifier. It is RECOMMENDED that this is a CURIE that uniquely identifies the evidence source when combined with a resource; e.g. PMID:123456 with a resource `pmid`. It could also be a URI or other relevant identifier.', 'help': 'An application-specific identifier. It is RECOMMENDED that this is a CURIE that uniquely identifies the evidence source when combined with a resource; e.g. PMID:123456 with a resource `pmid`. It could also be a URI or other relevant identifier.', 'type': 'string'}}, 'required': ['id'], 'title': 'External reference schema', 'type': 'object'}}, 'required': ['evidence_code'], 'title': 'Evidence schema', 'type': 'object'}, formats=None)]), - ), - ] diff --git a/chord_metadata_service/phenopackets/migrations/0008_auto_20200514_1541.py b/chord_metadata_service/phenopackets/migrations/0008_auto_20200514_1541.py deleted file mode 100644 index e5b44022c..000000000 --- a/chord_metadata_service/phenopackets/migrations/0008_auto_20200514_1541.py +++ /dev/null @@ -1,101 +0,0 @@ -# Generated by Django 2.2.12 on 2020-05-14 19:41 - -import chord_metadata_service.restapi.validators -import django.contrib.postgres.fields -import django.contrib.postgres.fields.jsonb -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('phenopackets', '0007_auto_20200513_1401'), - ] - - operations = [ - migrations.AlterField( - model_name='biosample', - name='diagnostic_markers', - field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), blank=True, help_text='A list of ontology terms representing clinically-relevant bio-markers.', null=True, size=None), - ), - migrations.AlterField( - model_name='biosample', - name='histological_diagnosis', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term representing a refinement of the clinical diagnosis. Normal samples could be tagged with NCIT:C38757, representing a negative finding.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='biosample', - name='sampled_tissue', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text='An ontology term describing the tissue from which the specimen was collected. The use of UBERON is recommended.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='biosample', - name='taxonomy', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term specified when more than one organism may be studied. It is advised that codesfrom the NCBI Taxonomy resource are used, e.g. NCBITaxon:9606 for humans.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='biosample', - name='tumor_grade', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term representing the tumour grade. This should be a child term of NCIT:C28076 (Disease Grade Qualifier) or equivalent.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='biosample', - name='tumor_progression', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term representing if the specimen is from a primary tumour, a metastasis, or a recurrence. There are multiple ways of representing this using ontology terms, and the terms chosen will have a specific meaning that is application specific.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='disease', - name='disease_stage', - field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), blank=True, help_text='A list of terms representing the disease stage. Elements should be derived from child terms of NCIT:C28108 (Disease Stage Qualifier) or equivalent hierarchy from another ontology.', null=True, size=None), - ), - migrations.AlterField( - model_name='disease', - name='term', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text="An ontology term that represents the disease. It's recommended that one of the OMIM, Orphanet, or MONDO ontologies is used for rare human diseases.", validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='disease', - name='tnm_finding', - field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), blank=True, help_text='A list of terms representing the tumour TNM score. Elements should be derived from child terms of NCIT:C48232 (Cancer TNM Finding) or equivalent hierarchy from another ontology.', null=True, size=None), - ), - migrations.AlterField( - model_name='phenotypicfeature', - name='modifier', - field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), blank=True, help_text='A list of ontology terms that provide more expressive / precise descriptions of a phenotypic feature, including e.g. positionality or external factors.', null=True, size=None), - ), - migrations.AlterField( - model_name='phenotypicfeature', - name='onset', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term that describes the age at which the phenotypic feature was first noticed or diagnosed, e.g. HP:0003674.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='phenotypicfeature', - name='pftype', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text='An ontology term which describes the phenotype.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)], verbose_name='type'), - ), - migrations.AlterField( - model_name='phenotypicfeature', - name='severity', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term that describes the severity of the condition.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='procedure', - name='body_site', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term that is specified when it is not possible to represent the procedure with a single ontology class.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='procedure', - name='code', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text='An ontology term that represents a clinical procedure performed on a subject.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='variant', - name='allele', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text="The variant's corresponding allele", validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:allele_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'dependencies': {'genome_assembly': ['chr', 'pos', 'ref', 'alt', 'info'], 'seq_id': ['position', 'deleted_sequence', 'inserted_sequence']}, 'description': 'Variant allele types', 'oneOf': [{'required': ['hgvs']}, {'required': ['genome_assembly']}, {'required': ['seq_id']}, {'required': ['iscn']}], 'properties': {'alt': {'description': 'The alternate base(s).', 'help': 'The alternate base(s).', 'type': 'string'}, 'chr': {'description': 'A chromosome identifier e.g. chr2 or 2.', 'help': 'A chromosome identifier e.g. chr2 or 2.', 'type': 'string'}, 'deleted_sequence': {'description': 'Deleted sequence , sequence for the deletion, can be empty, e.g. A', 'help': 'Deleted sequence , sequence for the deletion, can be empty, e.g. A', 'type': 'string'}, 'genome_assembly': {'description': 'The reference genome identifier e.g. GRCh38.', 'help': 'The reference genome identifier e.g. GRCh38.', 'type': 'string'}, 'hgvs': {'description': '', 'help': '', 'type': 'string'}, 'id': {'description': 'An arbitrary identifier.', 'help': 'An arbitrary identifier.', 'type': 'string'}, 'info': {'description': 'Relevant parts of the INFO field.', 'help': 'Relevant parts of the INFO field.', 'type': 'string'}, 'inserted_sequence': {'description': 'Inserted sequence , sequence for the insertion, can be empty, e.g. G', 'help': 'Inserted sequence , sequence for the insertion, can be empty, e.g. G', 'type': 'string'}, 'iscn': {'description': 'E.g. t(8;9;11)(q12;p24;p12).', 'help': 'E.g. t(8;9;11)(q12;p24;p12).', 'type': 'string'}, 'pos': {'description': 'The 1-based genomic position e.g. 134327882.', 'help': 'The 1-based genomic position e.g. 134327882.', 'type': 'integer'}, 'position': {'description': 'Position , a 0-based coordinate for where the Deleted Sequence starts, e.g. 4.', 'help': 'Position , a 0-based coordinate for where the Deleted Sequence starts, e.g. 4.', 'type': 'integer'}, 'ref': {'description': 'The reference base(s).', 'help': 'The reference base(s).', 'type': 'string'}, 'seq_id': {'description': 'Sequence ID, e.g. Seq1.', 'help': 'Sequence ID, e.g. Seq1.', 'type': 'string'}}, 'title': 'Allele schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='variant', - name='zygosity', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term taken from the Genotype Ontology (GENO) representing the zygosity of the variant.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - ] diff --git a/chord_metadata_service/phenopackets/migrations/0009_auto_20200515_1613.py b/chord_metadata_service/phenopackets/migrations/0009_auto_20200515_1613.py deleted file mode 100644 index d6c2acf42..000000000 --- a/chord_metadata_service/phenopackets/migrations/0009_auto_20200515_1613.py +++ /dev/null @@ -1,37 +0,0 @@ -# Generated by Django 2.2.12 on 2020-05-15 20:13 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('phenopackets', '0008_auto_20200514_1541'), - ] - - operations = [ - migrations.RemoveField( - model_name='biosample', - name='diagnostic_markers', - ), - migrations.RemoveField( - model_name='disease', - name='disease_stage', - ), - migrations.RemoveField( - model_name='disease', - name='tnm_finding', - ), - migrations.RemoveField( - model_name='metadata', - name='external_references', - ), - migrations.RemoveField( - model_name='metadata', - name='updates', - ), - migrations.RemoveField( - model_name='phenotypicfeature', - name='modifier', - ), - ] diff --git a/chord_metadata_service/phenopackets/migrations/0010_auto_20200515_1645.py b/chord_metadata_service/phenopackets/migrations/0010_auto_20200515_1645.py deleted file mode 100644 index 418ca30d0..000000000 --- a/chord_metadata_service/phenopackets/migrations/0010_auto_20200515_1645.py +++ /dev/null @@ -1,45 +0,0 @@ -# Generated by Django 2.2.12 on 2020-05-15 20:45 - -import chord_metadata_service.restapi.validators -import django.contrib.postgres.fields.jsonb -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('phenopackets', '0009_auto_20200515_1613'), - ] - - operations = [ - migrations.AddField( - model_name='biosample', - name='diagnostic_markers', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='A list of ontology terms representing clinically-relevant bio-markers.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)]), - ), - migrations.AddField( - model_name='disease', - name='disease_stage', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='A list of terms representing the disease stage. Elements should be derived from child terms of NCIT:C28108 (Disease Stage Qualifier) or equivalent hierarchy from another ontology.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)]), - ), - migrations.AddField( - model_name='disease', - name='tnm_finding', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='A list of terms representing the tumour TNM score. Elements should be derived from child terms of NCIT:C48232 (Cancer TNM Finding) or equivalent hierarchy from another ontology.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)]), - ), - migrations.AddField( - model_name='metadata', - name='external_references', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='A list of external (non-resource) references.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:schema_list', '$schema': 'http://json-schema.org/draft-07/schema#', 'items': {'$id': 'chord_metadata_service:external_reference_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'An encoding of information about a reference to an external resource.', 'help': 'An encoding of information about a reference to an external resource.', 'properties': {'description': {'description': 'An application-specific free-text description.', 'help': 'An application-specific free-text description.', 'type': 'string'}, 'id': {'description': 'An application-specific identifier. It is RECOMMENDED that this is a CURIE that uniquely identifies the evidence source when combined with a resource; e.g. PMID:123456 with a resource `pmid`. It could also be a URI or other relevant identifier.', 'help': 'An application-specific identifier. It is RECOMMENDED that this is a CURIE that uniquely identifies the evidence source when combined with a resource; e.g. PMID:123456 with a resource `pmid`. It could also be a URI or other relevant identifier.', 'type': 'string'}}, 'required': ['id'], 'title': 'External reference schema', 'type': 'object'}, 'title': 'Schema list', 'type': 'array'}, formats=None)]), - ), - migrations.AddField( - model_name='metadata', - name='updates', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='A list of updates to the phenopacket.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:schema_list', '$schema': 'http://json-schema.org/draft-07/schema#', 'items': {'$id': 'chord_metadata_service:update_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An update event for a record (e.g. a phenopacket.)', 'help': 'An update event for a record (e.g. a phenopacket.)', 'properties': {'comment': {'description': 'Free-text comment about the changes made and/or the reason for the update.', 'help': 'Free-text comment about the changes made and/or the reason for the update.', 'type': 'string'}, 'timestamp': {'description': 'ISO8601 UTC timestamp specifying when when this update occurred.', 'format': 'date-time', 'help': 'Timestamp specifying when when this update occurred.', 'type': 'string'}, 'updated_by': {'description': 'Information about the person/organization/network that performed the update.', 'help': 'Information about the person/organization/network that performed the update.', 'type': 'string'}}, 'required': ['timestamp', 'comment'], 'title': 'Updates schema', 'type': 'object'}, 'title': 'Schema list', 'type': 'array'}, formats=['date-time'])]), - ), - migrations.AddField( - model_name='phenotypicfeature', - name='modifier', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='A list of ontology terms that provide more expressive / precise descriptions of a phenotypic feature, including e.g. positionality or external factors.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'Schema to describe terms from ontologies.', 'properties': {'id': {'description': 'CURIE style identifier.', 'type': 'string'}, 'label': {'description': 'Human-readable class name.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)]), - ), - ] diff --git a/chord_metadata_service/phenopackets/migrations/0011_auto_20200519_1538.py b/chord_metadata_service/phenopackets/migrations/0011_auto_20200519_1538.py deleted file mode 100644 index 29216d0b8..000000000 --- a/chord_metadata_service/phenopackets/migrations/0011_auto_20200519_1538.py +++ /dev/null @@ -1,112 +0,0 @@ -# Generated by Django 2.2.12 on 2020-05-19 15:38 - -import chord_metadata_service.restapi.validators -import django.contrib.postgres.fields -import django.contrib.postgres.fields.jsonb -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('chord', '0013_table'), - ('phenopackets', '0010_auto_20200515_1645'), - ] - - operations = [ - migrations.RemoveField( - model_name='phenopacket', - name='dataset', - ), - migrations.AddField( - model_name='phenopacket', - name='table', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='chord.Table'), - ), - migrations.AlterField( - model_name='biosample', - name='diagnostic_markers', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='A list of ontology terms representing clinically-relevant bio-markers.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)]), - ), - migrations.AlterField( - model_name='biosample', - name='histological_diagnosis', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term representing a refinement of the clinical diagnosis. Normal samples could be tagged with NCIT:C38757, representing a negative finding.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='biosample', - name='sampled_tissue', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text='An ontology term describing the tissue from which the specimen was collected. The use of UBERON is recommended.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='biosample', - name='taxonomy', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term specified when more than one organism may be studied. It is advised that codesfrom the NCBI Taxonomy resource are used, e.g. NCBITaxon:9606 for humans.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='biosample', - name='tumor_grade', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term representing the tumour grade. This should be a child term of NCIT:C28076 (Disease Grade Qualifier) or equivalent.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='biosample', - name='tumor_progression', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term representing if the specimen is from a primary tumour, a metastasis, or a recurrence. There are multiple ways of representing this using ontology terms, and the terms chosen will have a specific meaning that is application specific.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='disease', - name='disease_stage', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='A list of terms representing the disease stage. Elements should be derived from child terms of NCIT:C28108 (Disease Stage Qualifier) or equivalent hierarchy from another ontology.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)]), - ), - migrations.AlterField( - model_name='disease', - name='term', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text="An ontology term that represents the disease. It's recommended that one of the OMIM, Orphanet, or MONDO ontologies is used for rare human diseases.", validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='disease', - name='tnm_finding', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='A list of terms representing the tumour TNM score. Elements should be derived from child terms of NCIT:C48232 (Cancer TNM Finding) or equivalent hierarchy from another ontology.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)]), - ), - migrations.AlterField( - model_name='gene', - name='alternate_ids', - field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=200), blank=True, default=list, help_text='A list of identifiers for alternative resources where the gene is used or catalogued.', size=None), - ), - migrations.AlterField( - model_name='phenotypicfeature', - name='modifier', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='A list of ontology terms that provide more expressive / precise descriptions of a phenotypic feature, including e.g. positionality or external factors.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_list_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'description': 'Ontology class list', 'items': {'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, 'title': 'Ontology class list', 'type': 'array'}, formats=None)]), - ), - migrations.AlterField( - model_name='phenotypicfeature', - name='onset', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term that describes the age at which the phenotypic feature was first noticed or diagnosed, e.g. HP:0003674.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='phenotypicfeature', - name='pftype', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text='An ontology term which describes the phenotype.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)], verbose_name='type'), - ), - migrations.AlterField( - model_name='phenotypicfeature', - name='severity', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term that describes the severity of the condition.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='procedure', - name='body_site', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term that is specified when it is not possible to represent the procedure with a single ontology class.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='procedure', - name='code', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text='An ontology term that represents a clinical procedure performed on a subject.', validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - migrations.AlterField( - model_name='variant', - name='zygosity', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, help_text='An ontology term taken from the Genotype Ontology (GENO) representing the zygosity of the variant.', null=True, validators=[chord_metadata_service.restapi.validators.JsonSchemaValidator({'$id': 'chord_metadata_service:ontology_class_schema', '$schema': 'http://json-schema.org/draft-07/schema#', 'additionalProperties': False, 'description': 'An ontology term.', 'help': 'An ontology term.', 'properties': {'id': {'description': 'A CURIE-style identifier for an ontology term.', 'help': 'A CURIE-style identifier for an ontology term.', 'type': 'string'}, 'label': {'description': 'A human readable class name for an ontology term.', 'help': 'A human readable class name for an ontology term.', 'type': 'string'}}, 'required': ['id', 'label'], 'title': 'Ontology class schema', 'type': 'object'}, formats=None)]), - ), - ] diff --git a/chord_metadata_service/phenopackets/migrations/0012_auto_20200525_2116.py b/chord_metadata_service/phenopackets/migrations/0012_auto_20200525_2116.py deleted file mode 100644 index 54a2da914..000000000 --- a/chord_metadata_service/phenopackets/migrations/0012_auto_20200525_2116.py +++ /dev/null @@ -1,26 +0,0 @@ -# Generated by Django 2.2.12 on 2020-05-25 21:16 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('phenopackets', '0011_auto_20200519_1538'), - ('resources', '0001_initial'), - ] - - operations = [ - migrations.DeleteModel( - name='Resource', - ), - migrations.RemoveField( - model_name='metadata', - name='resources', - ), - migrations.AddField( - model_name='metadata', - name='resources', - field=models.ManyToManyField(help_text='A list of resources or ontologies referenced in the phenopacket', to='resources.Resource'), - ), - ] diff --git a/chord_metadata_service/resources/migrations/0001_initial.py b/chord_metadata_service/resources/migrations/0001_v1_0_0.py similarity index 97% rename from chord_metadata_service/resources/migrations/0001_initial.py rename to chord_metadata_service/resources/migrations/0001_v1_0_0.py index bd88f9d55..e661f6a19 100644 --- a/chord_metadata_service/resources/migrations/0001_initial.py +++ b/chord_metadata_service/resources/migrations/0001_v1_0_0.py @@ -1,4 +1,4 @@ -# Generated by Django 2.2.12 on 2020-05-25 21:16 +# Generated by Django 2.2.13 on 2020-07-06 14:55 import django.contrib.postgres.fields.jsonb from django.db import migrations, models From 0b51d0769961a07d670c53d26208cf123282cff4 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 6 Jul 2020 11:14:46 -0400 Subject: [PATCH 187/190] Switch from chord_lib to bento_lib Optimize some JSON validator stuff --- chord_metadata_service/chord/serializers.py | 14 +++++++++----- chord_metadata_service/chord/views_ingest.py | 16 ++++++++-------- chord_metadata_service/chord/views_search.py | 4 ++-- chord_metadata_service/metadata/settings.py | 6 +++--- requirements.txt | 2 +- setup.py | 2 +- 6 files changed, 24 insertions(+), 20 deletions(-) diff --git a/chord_metadata_service/chord/serializers.py b/chord_metadata_service/chord/serializers.py index 91c44e21c..54b61737b 100644 --- a/chord_metadata_service/chord/serializers.py +++ b/chord_metadata_service/chord/serializers.py @@ -1,4 +1,4 @@ -from chord_lib.schemas.chord import CHORD_DATA_USE_SCHEMA +from bento_lib.schemas.bento import BENTO_DATA_USE_SCHEMA from chord_metadata_service.restapi.serializers import GenericSerializer from jsonschema import Draft7Validator, Draft4Validator from rest_framework import serializers @@ -12,6 +12,10 @@ __all__ = ["ProjectSerializer", "DatasetSerializer", "TableOwnershipSerializer", "TableSerializer"] +BENTO_DATA_USE_SCHEMA_VALIDATOR = Draft7Validator(BENTO_DATA_USE_SCHEMA) +LINKED_FIELD_SETS_SCHEMA_VALIDATOR = Draft7Validator(LINKED_FIELD_SETS_SCHEMA) + + ############################################################# # # # Project Management Serializers # @@ -51,17 +55,17 @@ def validate_creators(self, value): # noinspection PyMethodMayBeStatic def validate_data_use(self, value): - validation = Draft7Validator(CHORD_DATA_USE_SCHEMA).is_valid(value) + validation = BENTO_DATA_USE_SCHEMA_VALIDATOR.is_valid(value) if not validation: raise serializers.ValidationError("Data use is not valid") return value # noinspection PyMethodMayBeStatic def validate_linked_field_sets(self, value): - v = Draft7Validator(LINKED_FIELD_SETS_SCHEMA) - validation = v.is_valid(value) + validation = LINKED_FIELD_SETS_SCHEMA_VALIDATOR.is_valid(value) if not validation: - raise serializers.ValidationError([str(error.message) for error in v.iter_errors(value)]) + raise serializers.ValidationError([ + str(error.message) for error in LINKED_FIELD_SETS_SCHEMA_VALIDATOR.iter_errors(value)]) return value def validate(self, data): diff --git a/chord_metadata_service/chord/views_ingest.py b/chord_metadata_service/chord/views_ingest.py index 48c8ed696..89870f578 100644 --- a/chord_metadata_service/chord/views_ingest.py +++ b/chord_metadata_service/chord/views_ingest.py @@ -1,24 +1,26 @@ import json -import jsonschema -import jsonschema.exceptions import os import uuid from django.core.exceptions import ValidationError from django.db import transaction +from jsonschema import Draft7Validator from rest_framework.decorators import api_view, permission_classes, renderer_classes from rest_framework.permissions import AllowAny from rest_framework.renderers import BaseRenderer from rest_framework.response import Response -from chord_lib.schemas.chord import CHORD_INGEST_SCHEMA -from chord_lib.responses import errors -from chord_lib.workflows import get_workflow, get_workflow_resource, workflow_exists +from bento_lib.schemas.bento import BENTO_INGEST_SCHEMA +from bento_lib.responses import errors +from bento_lib.workflows import get_workflow, get_workflow_resource, workflow_exists from .ingest import METADATA_WORKFLOWS, WORKFLOWS_PATH, WORKFLOW_INGEST_FUNCTION_MAP from .models import Table +BENTO_INGEST_SCHEMA_VALIDATOR = Draft7Validator(BENTO_INGEST_SCHEMA) + + class WDLRenderer(BaseRenderer): media_type = "text/plain" format = "text" @@ -65,9 +67,7 @@ def ingest(request): # TODO: Use serializers with basic objects and maybe some more complex ones too (but for performance, this might # not be optimal...) - try: - jsonschema.validate(request.data, CHORD_INGEST_SCHEMA) - except jsonschema.exceptions.ValidationError: + if not BENTO_INGEST_SCHEMA_VALIDATOR.is_valid(request.data): return Response(errors.bad_request_error("Invalid ingest request body"), status=400) # TODO: Validation errors table_id = request.data["table_id"] diff --git a/chord_metadata_service/chord/views_search.py b/chord_metadata_service/chord/views_search.py index 0e6167b28..456efb81d 100644 --- a/chord_metadata_service/chord/views_search.py +++ b/chord_metadata_service/chord/views_search.py @@ -2,6 +2,8 @@ import json import uuid +from bento_lib.responses import errors +from bento_lib.search import build_search_response, postgres from collections import Counter from datetime import datetime from django.db import connection @@ -12,8 +14,6 @@ from rest_framework.response import Response from typing import Any, Callable, Dict -from chord_lib.responses import errors -from chord_lib.search import build_search_response, postgres from chord_metadata_service.experiments.models import Experiment from chord_metadata_service.experiments.serializers import ExperimentSerializer from chord_metadata_service.mcode.models import MCodePacket diff --git a/chord_metadata_service/metadata/settings.py b/chord_metadata_service/metadata/settings.py index bd2997478..250215048 100644 --- a/chord_metadata_service/metadata/settings.py +++ b/chord_metadata_service/metadata/settings.py @@ -87,7 +87,7 @@ 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'chord_lib.auth.django_remote_user.CHORDRemoteUserMiddleware', + 'bento_lib.auth.django_remote_user.BentoRemoteUserMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] @@ -168,7 +168,7 @@ REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': [ - 'chord_lib.auth.django_remote_user.CHORDRemoteUserAuthentication' + 'bento_lib.auth.django_remote_user.BentoRemoteUserAuthentication' ], 'DEFAULT_PARSER_CLASSES': ( # allows serializers to use snake_case field names, but parse incoming data as camelCase @@ -199,7 +199,7 @@ ] -AUTHENTICATION_BACKENDS = ["chord_lib.auth.django_remote_user.CHORDRemoteUserBackend"] + ( +AUTHENTICATION_BACKENDS = ["bento_lib.auth.django_remote_user.BentoRemoteUserBackend"] + ( ["django.contrib.auth.backends.ModelBackend"] if DEBUG else []) diff --git a/requirements.txt b/requirements.txt index bf1e9ea24..e70c8b59c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,9 +2,9 @@ alabaster==0.7.12 appdirs==1.4.4 attrs==19.3.0 Babel==2.8.0 +bento-lib==0.11.0 certifi==2020.6.20 chardet==3.0.4 -chord-lib==0.9.0 codecov==2.1.7 colorama==0.4.3 coreapi==2.3.3 diff --git a/setup.py b/setup.py index 41d421971..aa619a050 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ python_requires=">=3.6", install_requires=[ - "chord_lib[django]==0.9.0", + "bento_lib[django]==0.11.0", "Django>=2.2.14,<3.0", "django-filter>=2.3,<3.0", "django-nose>=1.4,<2.0", From d7c1fc2e09d794dd7577b0db1706a620a50be0ad Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Mon, 6 Jul 2020 12:51:28 -0400 Subject: [PATCH 188/190] add hts files metadata to ingest --- chord_metadata_service/chord/ingest.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/chord_metadata_service/chord/ingest.py b/chord_metadata_service/chord/ingest.py index a42579d86..89edf8489 100644 --- a/chord_metadata_service/chord/ingest.py +++ b/chord_metadata_service/chord/ingest.py @@ -259,6 +259,7 @@ def ingest_phenopacket(phenopacket_data, table_id) -> pm.Phenopacket: biosamples = phenopacket_data.get("biosamples", []) genes = phenopacket_data.get("genes", []) diseases = phenopacket_data.get("diseases", []) + hts_files = phenopacket_data.get("hts_files", []) meta_data = phenopacket_data["meta_data"] if subject: @@ -321,6 +322,18 @@ def ingest_phenopacket(phenopacket_data, table_id) -> pm.Phenopacket: ) diseases_db.append(d_obj.id) + hts_files_db = [] + for htsfile in hts_files: + htsf_obj, _ = pm.HtsFile.objects.get_or_create( + uri=htsfile["uri"], + description=htsfile.get("description", None), + hts_format=htsfile["hts_format"], + genome_assembly=htsfile["genome_assembly"], + individual_to_sample_identifiers=htsfile.get("individual_to_sample_identifiers", None), + extra_properties=htsfile.get("extra_properties", None) + ) + hts_files_db.append(htsf_obj) + resources_db = [ingest_resource(rs) for rs in meta_data.get("resources", [])] meta_data_obj = pm.MetaData( @@ -346,6 +359,7 @@ def ingest_phenopacket(phenopacket_data, table_id) -> pm.Phenopacket: new_phenopacket.biosamples.set(biosamples_db) new_phenopacket.genes.set(genes_db) new_phenopacket.diseases.set(diseases_db) + new_phenopacket.hts_files.set(hts_files_db) return new_phenopacket From 6c41e1f2a52ac18b0a1a57b277aef23be1051aa8 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Tue, 7 Jul 2020 12:59:40 -0400 Subject: [PATCH 189/190] fix for deceased --- chord_metadata_service/mcode/parse_fhir_mcode.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/chord_metadata_service/mcode/parse_fhir_mcode.py b/chord_metadata_service/mcode/parse_fhir_mcode.py index 5f98fdd0f..6044ec7cc 100644 --- a/chord_metadata_service/mcode/parse_fhir_mcode.py +++ b/chord_metadata_service/mcode/parse_fhir_mcode.py @@ -50,8 +50,13 @@ def patient_to_individual(resource): if "active" in resource: if resource["active"]: individual["active"] = True - if "deceasedBoolean" in resource: + if "deceasedDateTime" in resource: individual["deceased"] = True + elif "deceasedBoolean" in resource: + if resource["deceasedBoolean"]: + individual["deceased"] = True + else: + individual["deceased"] = False return individual From 3e4509835baff579aae0c84c370ba708613d8d19 Mon Sep 17 00:00:00 2001 From: ksenia1zaytseva Date: Tue, 7 Jul 2020 16:06:26 -0400 Subject: [PATCH 190/190] make requested changes --- chord_metadata_service/mcode/parse_fhir_mcode.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/chord_metadata_service/mcode/parse_fhir_mcode.py b/chord_metadata_service/mcode/parse_fhir_mcode.py index 6044ec7cc..95e8afa2c 100644 --- a/chord_metadata_service/mcode/parse_fhir_mcode.py +++ b/chord_metadata_service/mcode/parse_fhir_mcode.py @@ -53,8 +53,7 @@ def patient_to_individual(resource): if "deceasedDateTime" in resource: individual["deceased"] = True elif "deceasedBoolean" in resource: - if resource["deceasedBoolean"]: - individual["deceased"] = True + individual["deceased"] = resource["deceasedBoolean"] else: individual["deceased"] = False return individual

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%0ADHKJ*S(ddLij{$EcG~`mNS^FuVfI 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 z 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�_ zZZT9sgTKXVNX^xa4xS~cQWP;U^uUx6sv1AlMBRisXs!#7Uy>52r00?4PCq0`8L4}O zv=* zKLIEHg%H2}3nB1Y;{PHDfn5phTD=m}3a3h=H@UxE@i^BL(ob?fikqmAi5|KRb!VeE zp7Ghws+gx5uEFcHVpS+66H@mmB>@nWiUx3M-$4SW-_<=vjAHrBi^zhH`^j2CGA2Qg zXVfz&Nk@AePCdWRbzq(?t8jdZK87btpY1eelu|g~uRO)iF;^vPu6UlXK10MwU^kyUh`1s zDfHI(zI?-j2wL!ctcChEi2&6a03R(|x4WY~9>aBDp>ADEAwT4q%zd`*9aWJR<{g@Z zo>7f2zpY-$_NFq$yAsH=uo<*4@uVGwIQ!Gx00FL(lmF4#_Jo=vq?K(~(Rn#RUPMHQ9M z>^)K>yu)(vH*8K$&W8^lu7rO(xuxj9`r^-9fBf)&B(;F28hN77D4qB+WG*o26l((q zb+qlEMV(}CAFl`SnFKMve+Rb$J49H-WMxiYdGnMKs~NRZ@oUBj$UK=;N=!Z2y1g))>Q zr+Q_ujo?VWJPN{^{CSNM*=OK{VRacb;SUIOKpxr+)v{dHRaeM{Gstai1@)p@ppwFS zDVS>rGo(-0>&2tMHHL>0CZL8ui-W8E-4=oEX2*iU2``?c1gkTjn1DvAWcId+4I5%9||JfQI1%|5~xq+cRziE6U7#td~w;d!F6W10;YQT==k13?S*CpG?HFx2Xn5K zgCxVcRO*HEvIrk1Zw42dC-VqB*=NKn0pD4^ybQhC+j&tW;5oTNyqWf zyJ*P>G0ExOADPjs*a30QYL?71WM7D|DJV`+jV!*!p6b}|a{l~YdV2a&-jlCUKb_=! zo}K-myu7?yzmTJRZu!#mHvGeIXF+1k>Ksi_M|e@|SPg5FwE6ioEKU~@+2N>40`*Z3 zLam4-ySVLGPkFnck7v@;)5D^pt)8wvi=r)4ZgESC?xjnY#HYqi1-3J}T;D->zxBs~ z>8{%f(xO&GdRP-TFkQ05V}3N#tw%dU|^$ltwSh7q+Z?APMX#L|~gXR*-#$VFy5ywaR8k(W|?U^nz$9 zs3i^MB>QG0>Ikxpy2>j%a%Vh$e(%nmJC|}*Z<1IC`;htoAKxWAJG-2DouI|m>TorJ zdN`A#V@V|5y$py}4vd&XJ%k=}BZ)d6#b1HgFUBmKKo)yQ*EPacV@hnu^ zlUsNCixAH`;Ku;1KX20W zOF%qx4#A)3bo>NJMc-g-FzNY!T@m^}m;#EpKnpExBQrB@q@#I3a0Ti_QgJmUbgIkz zP*rvHnWuR929!sEkpg}4tgJ4Hv~wsjnD`)!H+TbmV49_x0vM0?GbQgYFOCjT#Ts$S z(D)~Or6F z&sg{VlnL^U1PJhZ^BG#S7xZVU6L}Xdusf(cA}<(yTZE>870@(I%~(r~uy;U{L63U^e;{`zMmLmq9sJ*(f-v)*~BLner;zkt4>-gfv@!g^^ICjt}!7p`O z&=EHxAuuB#6|N!^KV?sj!^y{2Z>^GVk`*KrHALGL{u`wQ!H`~$+MR!v5ZqO)r_y*@Y-+MWk@UpGMA(A| zeeTDtQ9r7t!IqYmDk>>;L#N)T`MJ}92G++O6G7f5HXxsiZUtG-8Jme@0p2V3{S@`( zh2&PXmlDYEFEZd(P8-)iYlr@MXt!2LuOfN6#Y+%{UyG|KhqJ%>BeMzL+aki7MBslm zWN+!(bQQ>VQP~3I`*{Gxe@#9aEWNzd;AH>cpvBX;xP^#19Te%;D;Zi@@f|*V_2^%Vb=X-q{D=v%uSER)_lx2tO6jn!ZuZ?PG5N@Rpcxpc(8TS8aul#llzUwI z9_|vqVv;kLNV_9NVSK4+$3em{Oq?Eq-y1yo$m93xKtM^8(-UY8z!`#Cp|XyyM^iOn zD@8M*7T~WWB>e{`L~)f}EMHdyMw?@h0nPP-iF7FPcyNqDOWNL7YngTL7+VK&IuV}y z<|LqHd+2N689*KJnV*3mn1Be`tp8x58WI2K$!`B(JrOVUUnYFC?VN}vS zq;F)pRcr36b0X;eW<{(v(Dl^~8o+;m8CO|>A5;PdPmXWFcUrqv%tQ;M^c89Uh-NUt zTA6487Z|5iY(uA2vBMC9ztY=<)?$P;{dKRA|5PUyTXW7u1ph%J44N7m&$34dTlPCx zRiRj{I<^>wY1+et>K@S4JO!t3Ze2VQ=s9So9Y7G`<48dHac+A3-_VpAXe$2`{r+p3 zPEqT|bfpF0p)2%Ay&Mctlq{aOcnw8A2Mv_YT;-Hy0lB60kq&8NXrE zK;=kg>R5k4@8&EH1Skd{X(h+ge|l-vqT<^@i)RCO^8{-bdALJ_J})G=9R#-eOGajgeK5M^Z7%==T& zAPQXJS#$#w;`hm3V0$R{D2(5%duQ2}FNO@L%(QIIY3;pm8AmwjGfrH|UOC2?I9mp&7e= zy~DxsWqtk819)N>DkAZkr#r11$vw;04}uP9jb^lH<|yt5xz`XGCv{x=PlSzEgMdvs zI0&(CfvB0*?a@1}^G}-`;kSt<-k3`nN5km^qgd@>G#3zdrfJ1T{!(E6rvaa6qU0wa zl7HnZFAC%lpN&m}GABu49m}7N-fKc_&a3|8(LYg-_#1K;O6yvpFBk*{OlezuedE{o z;-UiNFYTepboX7Jls*ROO=gS|YmGmbT@OzvHoohU7ir<|*Yc6ekKAqW8DIrsXSI9M^@O^*PWG+rE5X zXqPPWcoE&S`JfcROkmr-=j8JtC<(yk>Nt#p>IMEf#c*mAhMn~yiSoqM!titY&ikzM z$BvH#t&ID{usgGtpKCaD=PE%mjS+`apv_Hzuybg}HavhRl~Q4ypAxyb^rMWoTE(34 zlh*&bUrZaYQ}P~%%Q*vegSz_J&+VJbU=QWYIZz%e&nMqm^w{@Jv<{)47j7%IKR~KY zXt35D3x+{Y{m17R9va;R7`^UZo{e~5yRxf(p7Sxr(PH!Gw{+=WNZ)6Px z${(@^%(psA3b%0ocq{NGpnJ;B&*O!Vv zVJI7zz+Mz|;ZXTgmv;|HeLOIaDJ};cV4u6qJ|SNzqOB)DL3wL0`2Vf)nd={Chm#(z z&1WNCt|lLCSVa#pg9=HYyS9o#$>!@=Q;xG**6%thx{%V zfj(Rw%Kr&b{tg|#4%+FM;kzLH1)duwQCg|IkSGsH)6OU*L4iKyjw^w2fs(DQU!z$u zI793P`KN)4%7NwV5j6TfC=YW{x5FavCi}|03)1Dzg2Wy512^Is-6ir}Y!3N@<&Wk0lICBo1+zkh z?Z4XFt~h>&y@5SPy!@=Z2XA?|JfDJ{X`n*j?8>?m=gSE6WKs3UI5fV>lW&tBcMQx5 zT+|6{2+9xXs$-WqIbP*)oA3^1JSITg%Yy3Tb)x}V~5SUme zh|KGjp4_{#J=VZ@glI=6`9`5msT)RhSHL*0psaQe3G0F>jWz(W`7rI(85Hd}j4$X3 zKBr#}hOK6ytH|vur~DrI@wyDfPCYy&iyzP~j<6Rk6uxTboJ%zxK0gP;9U*FX?Na1f zqW3u)c5Ng^nwq%D=nE!G*we=aMH<8&Sn#gcXv&u7we+E=rkX_A`hJ1kOHHt6o#Cj;xnt-W!yQ)?#sa@0KO+&J(K;>k|Kq4ue$Rdoq7G~ zJut5+8aTt0?2P#x;+{Tz;l424;`G%IK|I}Ot4Kn{b;{7zR)D{7*2te(CVFmePTv(m zj*5YyF77piZpc^xhWMj2&z^g&h(5dL#hp|PY8ewn!M846tpVo8;vcI(<+d^z_E(J! z|0y;>qTxeJ8R94*SvQHK6FB)$Egb*!;|(<5A)5;tvi(7wdu(DCp0wABPg1Ktq-tND z&a$8Et3mQ>p&7dsUlfHh5)~GcES4LGqv7roe1`Mle*S&!5XV{x1Ygy3Oubt=tq}oFaH$fCobhN;zPfjY3L_#81hMVQ1P?S#Yw@4zm?xr z6ic*{AzieI1%=KNJuvTBsu7$#vw(SYB7B54Qq-0;KziN;$?o{UPPMdFT#qKG72i7; zhBK-+8IlA^3=hy`UA$~vByoN3sb`nJrIQo_iuTwVBo^AVVD3QtHeB?{L)p8E?jDkb zN9$hLT(iwcr0moSrRD&3ZJRnJ*TiX;#=F?E}d(P)WWn~YfRT6n;H3MZ%jn~%g znY1UUO>bKDRa9qHkBB^`;f|-EMOwtJ*Dj`7b$y!VHz3t7zRM%WK#pG{R?)bW5T8!XW_xfsXN~1dIh?HV+CPA0IKN zQNw7@<+-|Tp2FqH@*dgfsLFXP_Y)iqPv52Qod2C^j?iYa7*-QIoUhxgDPKIJ60-@d zO9=e5&NmYPM@^eSBI&81tW>}ObLkxa$tg#3)j@?F8(C=#!O!)|F#MXg^ z(N%g|MdW<+<5iJGznD>(L30aJJmem*b})?!y$z_J^_}U+3R+YO3^*{e_{MMN#9Ow2 z3$;&`^pw8=^d)}Jg+bJH+F}^S5k|t$o7i)nfnEO(dtV(EW!Lp92#OdmQYsPx0uqWM zQUZe_h$5g;(qe!hCEX4xq7o_wN{wP5-CYKVBB3-$C?Z|bebx;#a}UqUbH3|)&ll&9 z=O3?en7L=~wb%Ns+-uR@pFNi_;zpEa^~@z^AKgb{_Cii-eR`3jC+tYT5zE^)(`kN) zJWztJ3zrNW^B6G~Q7*b#h()RtmbKa@Eo$)vmIXAFWcsP2Z-q0y--q!$y=OA43|OjM z$S07b(dg$o?=_nJ$ua_>9Jl`jlU5@4r{flHI*4WhR-RH5E6=1+SrG+OmJ2BnFu4XO zS%~K;BVrL}r9YSn&HOGaXD&J*Be2_P(4H9EL%<1#gE03Ni8t$3JTc@0OjNSrzlwMm zN@eU|d{D>y%-~J3Oh4#fI+;=RO>9!Br)r9L#CfOAzK`2Qv_!V}5NIxn(i{)qD8jEi zLgHn6?zS0I@na-ZBK-k*0SH`G!imHE-mqIhNC0E$vKpv>?p)1DlHw--F?Xh z7~* zwI5d86>@4biR^OZ1Ru3p0?iG1WCKJZh=2mi@$i)C#CCnj+V>Xdabk(n3bg z7{`HP)QQ1Q5a~|kiW!6G2tyach znbg&(nKoQOEs-=DP%6T%EvOx(SvXN)Sagu&r=v>-Sx<%t%;PkpY|_UO35)|_Gw%>1 zw=2jNMZd*uF&BIa8OQgh%-Ykg54Va)N4VdYQwUSzrVc_7x-`uN3Y#pAuFVqx(tyCa zoJu6t)wu(#Tl>6|2z#q=h6w9wej!@vL=jse9RG1(-7_;U^~h8KO*Qm?#%mb{kwXUQ zU|^R?%t(fv59iM+{`&@tVM(?_Z}GMgvK+<(P9(kb-M;FCB+Mke($5aI_VqMUO-iEo z5}~X2qP8|PnWpK|E_WkQW{zfZz9I0b6LGJkKOisE13(jA89(-#{N2A?7bO1^!dSQ@ zMo5jlyCNv(IqQ7DvT^?-5oWXw8n1v$F|D{bGu|(l29fHQo9kYg5@-Tau*k*g9~a%5 zMa?lso5_hck|gtG5fGL$Zy}n7=m{b$Ac?@)UuyCHa@}>=7m&j#KGmjhpH(`}eJSU3 z?v}|{bW6vW36{o{pj%(+7&P_vz+|ppEB=#{Vgo(dkKYfx0#pBXI>?e`P_8`*d)))g>+D$WAeS?H7jS( zXOgYOECWEpaWHUz>^;s(#C%rFVp`D*0djK-4?X0ebJG2Bk6S4jl7RONbBjpUF8vlh z91KD|Ul(ssDm<0XW5mWsXFHKRjJhu(;<`i{U5ZEvP#PCYtwEtC#2)GZ7jPy|rX^(K6mh z)dpSSHlb)rPW^vvOtKto?lES{e9DlD>+AX&nf1emRq@JKW;By;97?SkCiPIyI?qAU zyJDw{Yp6$T*pd*K@U8_uCFpcS|ym2nR`Ne8@U2e>1gjAbi(-{WAw+xL(wDJ%OQ>ud zOh#QKMrrlnAj$a<;YX;u^AbC`Oc8O7CKWLZQ$N#iGnK?Fd<}8JhoWT ztu1d)H_-vHUM76 z|HN`Fl=D;Vk07Bur-GU0d%96YBnr@T4$0%xyr!!pz2@Edp2{8nbKxR^7`R94mVz0q zsB^6Y*Mlfx)=iP@F7Q^}7q-86l|^C?@)wn|2n07Vu%XfoMyna9Js-t{zl#V!BAA2k ziB9%YNbBu^I|mabLf;ONJ+(_{2*!zbz+m-((f4rSpo4ft&g{t&_okjjxwi&fq{OFp zBS>b~hEt%X<;|ZG4OB8Lg~RO!i~jpxvkpE0!Nz8zw4`T{Q4g_!?$J-?@$gB9$DN6=%sx9hy zbRerm!-oI$tF3f}%$Q++bqC}nczJns3s!9;vpz(&ES6=fs_qtV5K}Ivdv%|5qu68p zB2DF4QO%~K3bH24N`j^nv|zlyNY^F3LH@$b5Rh!-w*c9%Vex4oXu3IK+fZG#oZI7A z5Iyn8G0V2S*u7J_^^(WPHge)7QbtUDF}*VIJxevn6Kb@t#a$#=J4je>Df06hHHoFx zWpWmFl9FC{Wq{t0YOR$etqjr18?HU!yo8ko+6hebF*%v?||DL2a37Y%DjuE$DVq<}qqT>%{qQc=6VFVVx`7VB0i#O8hwjs)7 zOWGmwWtt(q&c+AtZ6e2PGO5I}QakcamA>@1Y|a*|I5?uhI#a_yW)W_r>HD4~%BL3I zr_mi4G!;a}UHp^roGn1^Z;PeX?NPQu+@B@-MoNjbm?4N|V%w7mpUSJHXT zB5wX480Z%jtdWx zBOa=3upDs2OM713VY78Fhu;i^&uuP3tXUr?nU+QZ=;<%}sxofKBY34H&oWUbD{wVB)d&24_X7#An! zH`f|XDh$PulWoTo_IlqAt`*wMC@iL+@kl<)B4Woe)!(6_DHWklWClk<()2Tpy>crC zT_$?Vi}Mx;>&7Xr*Jr>~aLBw{zb98@lSJE9MY`{8Wc&~;g|mpy;x$~DKC!PXn!%72 zGoL8pFd9GzHC`54a`74Fq&D)`+k}3~$2XlYhI9gCawY-RAoJ5VeHcAKtT^R>T+Y|z zmCN^i)C%p|e)K-U5JehUi1BBrpjO!$x0?J-vSY_Rr_N`^j3{b5*Jx@N<+_B-_CMdj zNEX5=jG)qr)xoS~cQ%&3&xA=56eIvRHe$)&H;y&RprB?AsET2+mtH5d6wN~J$7@jL z>3W%=+}Wj2IA_XtES=0%Ko?RtMC-iiU~Zo16*B!mGm12ztWah{ZxJl z$q8TAOLr1oNc+0Xh5d45=Suv2h#96|?JT@7p}joUdJLAWt$*NebFJ*mnR~d>+2e-a zbIgmRx3~rsY$Pf^#GdA%gzH6jkXyU`(FQCSuSeJApDeK)T-aTNv$lH$pVly5mZ;Up z!%J(y%8_UQ!ZOCuwH(Q)ST!Om-}7I(@<{^)71p>U^P1@31D5}~@VbORKACQ1SU3kJ zQ1u<2aHYuDX31Pi{81Nk)qCT|kRBHMCTiv-%M>!hS=6sw=RU>7xDu(3`wF~XwvPB; zG7UgQ^FX*w(xzcfb6Ji!8_5wfJ@)?_y|M~-lX{*W6Ef@OBW6+i;bO>C9o@xsq{qU@ zHzZ^BB~&uSV5+86D8$s^B%wg2QkvQMH%2x>nU+qA&l2o+7ulQ>pR zvQM2BOq?5U3Ur`cFnamsrIY<4weA`~qHIz@A$R0Qf$9yV0;9@idL8w-G+Ln{ZY*a4 zwxBK|_nQDITN*|6-RtRE^4}wIH@_YrO7cw1vdS6zooyYh@tK$ZHeGU12qu_nkRAdX z1ha*bfuLiz+%2@i*!D6;xT){y8jnoh^~%nfl8SeiNYt3={rBn8#N8F%rOjN}{;hcH z%8fXr7>BlJ9_9}rH~wHiC*FI5ffo6l6!Lv_<-y-uLZndl3&#cM$oN(92*IfAr_RM& zT!dNyeu~}-{I9Ub1h~0-8lbB@KW16=KjG21gOuT`JG;}4ku#piGx(`>w=Rk@tzc~T`ZJM|Vp-!cldQ8#;s#O$Zs$dWj}KUdy|h{*gZu#&SIHHigYD1su- zS6l}my8X0?t3uyqj9Q>?!EIl@BOku_PTui#KBRD&4HJ0FSD5Ra(~+BzuN8?n$$tbt zEn0L9Gu-5>!-830!^j2tieZujxIDLGR;j(Ua3Nz zXLW-V+Kw3a46e0_RQHR*`7Wv=cZxk-Pj^5;$g#p{QGSng;F2>C z@U^UOtk1VzoNN5HuMC%oq3F#^8tA!ZZ`Q$J#FakyV{APXV*AXkStpJF{2B#^{aspt zkq&Ydg#3|A`%V*rP+{;h7iz-%bS7Yo^0uEu%fwUZW#k7Wa zt&@z+0l0TzhPFE&)3IP1&B6@-M8+RF#ye0+qfEMGH;zN$gC@rIh=TitOdrDmGis&> zO?nT}Pb`1n8@Q$_@-RC9O(_}sB9#rcBO3@2%iyDh>$4fwO~E}Dm4{cyOry`U5xEwN z54$>}HbU}8m+{Jl^}s6i^8@UdgaF^iN*`k1ZYpX(S$Knnv4g^Ft8eChu19qCJfGBd zM{!P3sml_@b2&HDM=;wN7)4|!Ylj0=WCVMeUn>JBHO zn#W2}Ohk0+#v2m|yzII65q1vt;b6xs&|}=_evg);AxZpKN^yk`E*M++NToAfZ6L;t zIxF8eN6rP|#G=AYXnxgL2O*RLKhau$i~@l9XZ6rd6jiB*GL%cx1(*o3aK|( zwz$QuVjDpCClU&(xPLAveE5pl8{_rbf$VnSVZlTxF4^rE4nNO&-%ms#PW8i(^!_HU&P4UHlLE{BS6aWEjzPF zs)K988+X|{D}(|(4vg<_st99AnfNXqo;wqs^jOBEr5l+Tmg0l{b=QNViSXT?x*TovxY0tKmY8Oo; z+(oplaQ}4XxQq%mm{_0HJP(X-J|fhm1g5 zc7iE7`04}LG^z@H&7XE4XD}>hG~*NO*7i2+vK1@JO1r463VQxbdPU4Sf1eubm5Q72 z@s;4^vg)^A+vJZ-`QEUiYlKj`2jn3t@*)TNuyp_nJZ8z87exqb2&(mWf8OVHP;5CZ z+OgUxU|>(oDK|QbJBNm;H%N}il1u53@51`NH3eDWSD=phVyT90~mn*U;tK;oeVA= z0<$Ve0tk2qZw!lP{;vNLW0rY!?Ddb0`wkMei9HKGzWKL%XgX*2-Jw@FEI5<_^U*Q+nPo8| zn2rbo_lON%BGv-@E6&3^4nXkglPPqhUE-)<6Px#+xwW_p&IDkfk#WNgsx`vKb6f4# zh7xP2?j8q|E#5N*l|=OUICo(Fy1}V-WR$e?59gb~2j=$Lw&}5aY*EU6GykNIXr#f! z^sK^-onAcg)15n$=5ej6Ji|r468pJ4CBOR2ySM=>UtHFahMZ*T&=pCZO>71QprbG~ zm{UIJ;Zk_?512dR)`Oa=#+8({5%Nxjq&^f`VD9|x$uM6Ds7v^O_4C5zgt=tdFeAlH z9*?ih?>4mma|zKSW925qEDy5VdHzJlGK1k8P6aagP8|hmFOs>q3^5odmDFcm`EP(b zQpmW2MB&93QAnrGfTtelhIpVA%U%4ZV@vTN7qHnwGA0E0M9dJQUAk?Sz_mDi;2yg% zy)=}I(P#W(0RLeoe2^Uz0dwNp`=u7DMNKN|@{(pEn^5Ju3TI1gmEH9{vyaU>{OBbI zh3%U$rF@N*a;-W5CR@qC1U*t@(bdQ>1*I}s^p`h-03WS9zjIOX<8mntE6bYP9x`l6 z1(U6k&Bl|+@w)+RvZmv73IAXA*gs$neK)9>@bfL@HVH{`Z1Nn)b?%mnU=Vd`h|A$_ zBJ)mg0o1kbgSC@=dV#lctc~`z$WADJc#E+;{c&yjSx~K{ju7YymuQ2N0;cYXYCshz z7U^2G?rE&T;*#!agZbg&mT-mg8gt_OhV0elM4PH)XWP7}c7Z0bUK15mh~CM9?IArl zHGml|8a_np=<%uaCliMbbKuhhaU+q9HV-uZy{JF^S$RiXZ;y z@+oHq;k~b)&_+Fs<&4sW1D}hXQ;HN|Uj={uPnKITA&8*D*+y+} z8VpnUbvj;ab`Ite`8yQh?{R~gzn&qo_+N-8^3zaGmMec72xsFcKB$FlhTY-&2c=~4 z`x>XYZ03?vetxb}V;`&+4_9<^;Q7-?%gexeRW!|M91Qrl z;dF){;ZkJRDPR->?}kvg^aSg6RqV!+*2zYbxzM-j6EEov@)ui7fLHjeS{`DC-z6Og z@#@){)Y;o2F?{kenYa}`p#5(DHOsa^y5(StWd0V++7sQJv=U%lQ$CHNT1j|-{vxY@ zlUHM%2&2If6YV|Vpz)UM^MpJO_5`S++UqH-6qG^0-YQQkDM;s*O>eW%K>g^%N>Iot zavg9yur(FYMM(!Rr$nxcw%jFs@-}l9XKY)juSB}pb!BG0DC7HA9ZB&Qa&CYXDFe&$ zSmq{^zD%P6SCx#pb`TXB&}UEACd2_OybIQ^PcF`fD1uw|0eT#+6?sqete5c|_A#=? zjgIFPgjjnyIPAaWoNJ8?=)K?cn~eM_X@z5@<+laZ)fnq@wSeQm?X9L$G4OWfdpM|Rz{D0*}@AekxK#7%mS!yQCJNd2;Qcx zL^$wmJ0NbPnEIBJ<^^No{EN#u&xy}k4JNpxh$|loGh{Y6)(9}J<$;T`Lg+VDubHfo zpIxQ#ca%9W6r5ljd#_}$6j&RSIhP($V)5{CjZO8)^-lZ3{im-WbvmkI8?YMR2lKE_A z0OoVa7F<84`WZVTTowDKvc(-YUS3N^JIISy9qBB-Fz7s@b1m}#*QPCfG$9U-^3-KL zH^2VflFx$8AtDsEPZfQT2duf$)aEt8JD%PJ+~H|1%|byL2<#C|N225$MBp0KEIU_} zLF@wc!L{>2Q$H}ao;l2YS1MbJo-JHCxscON%zh&ZXH=o>zL&d!``~9Uj$R!lg%8v+ zk|!zFVl>pT3=he84J9&sYYhUwtGZNCE7y1nqDBrs_9JAyqL4K|wQ=Oe8h-=6P-c)K zJpiuoKsKx}dF25DKI-5d$cgpKasT`j=_X-0eeP$O(UcZ=#qa6U>riZlD~D9I@mYf9 z&I2hXbE^w1*Zv9et-q&n91OfCf4A=cP6MxJ>K0}=+_7KIdH7rXjIVe{$W{SVYSn`m zy@1_Dr<$iT=JJHO9x-cOiMs->iAf51nw)gB@Zu==mYj2ptI*xxArek0O0aYJbpV^G z#kDU{ZQE@;cggwMhNYMtB)5VAn%`}U-j1>q*>n9i4{eu#P;+d6uccx|0Gs1Xh{SYq zugW&EPt#&1hi2#68K0q zK_0&lF3$Wv`HQ{~{*vjxSlnB&VBa^JyZXdmR{QpG!tM2e&P6&kxd?4fnfLDl+P+Hp zyPU#NVW6$U3=>fWgK>Eo@x|66G659)e+esmFu?4!82#dmc;)ZqhPN`1T_D4~quMRe zcft0fgLX!Yxr0KVx=xnCW9}%t#Xjl3K?n*;%#NtS{bbsESz_kjXM>XY?UO^k5(Uk% z&h0NBf9%T}7d=aMp8P7t9sWjl?2J5ybTrv87#2pYE7~1!f&6x9F&R58TXo@kIB*s6 zaaWCCCyVS|dMwVHI=KL^XJheD5kCy5P@GY+il|zS0Qw3=huvN=lv2n`*$}bXi<@hl zJubN_Wg^gP^4En| zG7FJaONgvKqX+lyNXQ1FZwBMIW-3f{-_Wlpw5d$6*2&eFB%9RFNCdCNy;*qqTGxjr zEwp~vGv8kBs2RQ$^O41HKDc9@KuM#E!{3!Ae1eN)!J!d(#G|F6VMJ;r>uUuF1zf(c z+W2_}=%5FlKbie`_XN;2Js^gZ2th5TRk+KPPa;;gGGS5_#b>>Qb+^xO%>BMRg3Uu& z@!LB6i65drj}IDriyx@LBpsHzdviUr#@)1$9u$a|A65|3RYX$G4c!VqQp%SUr%5Qa zf#}|Lzy=%2Cvx;65)P)s2mNA#+!qqPSpK}l#c|OkKV>K4#_lc90`71XKOjMmDrH8ulKD+U?6R z&k3^i2wTfyKNhhH_ZGa1HLa_;iM(`vGc5H@lle-{1!p9(R1RO?8P+xfxRtHic6G8%ESYFAm)#N`&)_C6n8o9TjFX4N}x{hsT5W+THXU z!IBZ|6fF|b7izW`&`Dd+REYhuhC7N=?_Y zR$QVkmokgUoEc~8?~&u;+FkO$x>9kj7Ln{96`t?+4nKDa4R>IVLVJ{9b#Z$_T~E2s zwRfh*EVN^Wi!Y0K(w@65#4V-a5!Q5u7J4H>1ka=w`nOtPl z$_*5~YNFG|gh@fGa6-VSDfYTyD>5om$@q3>SB2ShVASlXfO^*)zKdaN$Eii;Q;(#b-n+Dd!eYt>!^#>j zN~)*aV7035@gt`D@5%%E`|OTE>*h#yLQI_2Y-`X^L%h!3X%cnwa%&&gL!TQTVx@xps zi!-h81WOp}!!CdT0-iH1G$4v@9M|iWVg`KeS-~vwFFE`>+T~O^gv|9`yI`zc7 z<8PN|@u5AP?_Lm~n2XyyPs<8oZ%)kJ^iboQ&~6E8HJx;$2iu zDEF(d7uH_!Ug;+)uqUka67b&VQw+0E0YL%+X3v0JF8?;U{#_C&0LTeN6~>|_&0?vr zH}f4lB@4|;eg?Aa|1MqvCcY<`XkQ~`hx*2=02AN_Ap}k}!oPy=F2<;d z2dSP7?|hSvU21)QSGVxyw$m9#!&GYopfrTPfYbx!8w9IV>aI|)B*=GF5VIFxM~4*Ccr)JyaU;YS|21;(-$f$qe~-BRyZrNyh#}(ee?_5xMC>0C1I*(5N5uZ) z`n`Ws(f=c2|A^Q>BDQFR{6}{6zl%cuh}b_O_K%4DBVzyfVnhcMLXZFWV*i9YAV%_s z_TLMg{S)p$Wa)n+y7=!}LH`xu4j(1sx#_Xtpom!>q|4dl*FaB3n2_Q&`{!|iAB=Xq z5Xp7%OE3neiTKMcjK~EVI5mqwzatiC)orKY<&{!4E79 zxHYts>gQI6m>i|L4~ee(`*jIDD^}rrAbWUu*{xRcSm|jV$RF7>xl%K!eC@kJ*5CCo zGNYqL$f#(`b7hODzYD&}gI3s9?U#M{jclgp=vSqW+6B`!**Am#%rOh;&>w#!bgR4S zwc1oxPbmKMdr7El65A%MHPu<84 zvqC3-ix+H-nCmlbZ?s78PqV12-JR4kaolM7+vH5a!c6_yn`|Z{W#S1QLeiT#E8TEs z_2?e&F=8L-tcalhd7VMk7bXp=W`=E~QF#f%_NLn5@=`?CxDd%)vx#I>F>nsiQ1k|N zlOlMPYLL8~t9IOt#(v4RJ!%9IL1TI9xrlS+Xn5Y3AC4ilktfQS|5c3q-=!&wA%$Y5 ze>|H*{WV9!;CxA5(_tiSEx!Fyz3i7}H{7_=Y)yhE(_=%|OYXDn9Dy}4+7q*>Yq#5A zx4Znhm(&^VMk~~n6~B6O6rafD-e-7kSC8~MC}UyTP~3cGDd&XCrvWI+*b}9C=qgqT zLeu0mbCuk%GZ}CDEi3M9zsAzeHir57kX;_?7BgwPegUaESTE4Eu-%RbNVn0RZ$rH@vJu9fOXJg(5;;zVp4%FP-^%Y zBF%iEHix#uXyxH9B{|ohY;cdJ@BwmfV6)cRog&aszEFXXKB7euYgTXLG@$&C0m}aj zN;eQYyNgsqYVBsW#~L&RiP|EoaO;q21!fJA9}g0VMOcs<4@J(;gGJ-6-TaEJGmx`X zLGF&6r)Ev`0>3N*`KzLf>Oj~T{VzQ-czXM|Ie3~Kux1ddJKANr%(bpFsLHX zU-@bl0{U#QDTnH9>1vC|mB@l(EZvnY=fZOkUM#!Ry2o?)j!vrCN%!vKM&@7e*O~T1 z(~SGbJ^5rG!Hfj>o$1g0=a;|@>xERvG#MF~LY4nP0K_TMO|SngKs=6Xo&7DI*P5sd%kFCl=_@oF;EKQeomQ6JYos=Yk=_4ZrZYA`HzMz6 zmIl^q<_lNTefU{)r~T)hJ>f%F5ttOx&$M~h?_;}Kp89=Nn!v#}Xte9^HjwqYU0$1l z8UbGJ012*(<)ID3o))#Sw?-=Dynao0@g^GA#-6bq$m%SP&lzYO&boZ`D%JyccD+E9 z(Hog{qeifo)&p_Mvd5%^pB z^wHx++f$B4A0M>~x;zd+8^0m`QT^Hp9@wN>FrRxY&# zxvsxnNgY>8ENT3gGhMnAuPZ+T3p# z^xW$(#~!m8)dSfh<@Q^$j zdt}Va;%&UeUp_2=*gZQegU&MWpDLU_jh0~3o5e!SL2-IB_i57QMPDkXEQRP%oro-8R31@Y1z&4O|su#MPRxh%!n)^?Nb9aQ(ql9G|@ZNH>Kj@OKbbOxd z(Jt7AjFod=m}x~!-9SH6@Cdj$N?T!K+qr_d@jI^%cI*n;*6YJHGk10%S_xY5skMi+ z&R*2_nj0H#=U9-i%IbI|_q5`vn4sf#+mH1ylhOqG4Ei=ZHVLQ0KlPVWT4rOu2;kd5 zZS+QctkIDjeMaXWJTdK5bwa#IZmvphE~%iM%?p-)2T(*VeAv%))OAaDfOedwCiX*J z{&?EG0xgRVjcFF*S;gTap`CvEb}+wq>cO^+)2>mc^~HYoWY1NJ@v0v<)=6V@0Adn7yI!Pu)N7bSiG%SqL*k%7{kNvu5p^#=JH}y#^;8NAAnq3!y zJa}{19kYEVEIh}kUf#L(9r}$YxljF>e8-ZX__a~?T(RH1g}69xqv(aX=y@rR>CxQ* zm#a3z0Y0caB1SmsbBN(z2Ug^NzHTrP@BJmkpq~VPRK;+OvGQGaS)GiE*x#@QmJ^xS zbD51pa}a-xHhRR!u1t)Z!!AMgs?mhD$Eq2?XkYF8KM#i|Kg#9zzOUXW5G8A6;4w6_ znqw0CvAq>WCAUXK2Ha*Tm@Hakewo^h(kkQczlViSBK_fyioCrEtP)`H-KKB#CJAF6 zedEEri2x91koSAzPZ_lCOmys>R@>2$F710dNALYFbhLvJ#=E`~0pU}0`BFr{27 zCNfD#H`R=%zU>}vBQouMyT|BP+jf8P05?Coa!I~TlE3Z-l&74H6gc=3TrY?3UJG+F za*kqjhGWs%{%+rU-cQtw4;_a&E_Of)-j?23F!SK}VN{bLAg&CN-{9Dz*Z+(K`p z{~^*`G85)XOAnNqRhT!+cHyu69C}X|STI{!5ULTS+&q@3`#xsqp1DVi_eH5G&5_=X z*3O4Ddi9ze;%KRZ<-F#vzn1=SoxubEg~6&$d9FXe0f0CoGEZ&VpRxjou-X2)r&Ju~)FSx9+4$K~&HF?P9g{nmjX!Y7==H?r!xLtBm7jC_Z8{W&s@VjKw66al-m*aqf-YV&s= z^#GV@GRv_7aiJ^m>JT)#g*cF(9}4otzHVN#$Nk58rSSyWc-aP{p!l1rbuk;|sV(Z_ z*`8PFGodKEBv@!qNCA=K>^o zqZbT2wM}J0`?kqGnG)-Qv8Oi3D0W}ba*4>=v9uo#fqLjkklz@M`6A9f^ zpTIV4j$YZRUqO6q$s!0m()oh(DR-dNeI%eb`)s5X$M1nrsv4!(nD&B^u))4Ffzfc} zT^!(mClxX4*-JgR*B@UnKkZx-V_5T5_9MI7Wxxr3_OU1>hg6R+f6s}W)}lAB!?(de z_m=B9+8liaFw;hfau@ql4rumv&?l@;3rVuma`W5|@jK!>9U|5#oJKpHPkiZGn z(t;4YcXU9Y=V&BoG_cEO`1Z3~U+amSsmXF`y!5*9qm7^B|_FC(!+B+C87v1jbYyM;G|CTyM_XsvO{d zJutiNyt@2YfstvLLv4u{u==5$(Jp@=oS54y=CQ9c&3XaOceHhOA;pP9{gj4F>&J!F z8}8eQ;JvTzetU0nGc&ywmGW(&qnnRlpI_aeyzl6Csz*C+QphXVT$p&v^aCR*fA$FW zBt3;KW;Nw8$DL(vR%O!pt5vj{d*TP;d!}n8-PkKcGpr`R>($8cFK}>kU0p>Yk8vz zZ`{1QYX|XLG@NZ~D7MBgr6c}m=my|jueWPqG9lOx#H5+Iy zxig(gbnMrSH1>$1E{WsaXYzrj`)lM)Es6`x?^?kgI{tWfvVHfdh8(Aa8pc#l_RC48 zIXAM>c)7Wv`DY;PaUL4epKXYQF8{~in5_7{tqv44q?nfe`V^G*DA9Gy{Ec=(xK@+; zwuW_TM~@xbtT`*w?(nMw)7_ktfwWC;+h5(FlXc~#MuMS%MYK{NyV&&iq;oef%wQ>K zaaP|mtRw7uYfo(AkiFdK#Oc+H_rA&QT5^x8XDeSrrtP^WQV!9K!n)PpI;PiHxwG51 zY70bR!`9^tHUU5$jnRsWes zzE~E0_)@BE`w0H`%bEOBKiCDeViTF7pTEJFU48>sc%J-g{ zr7aG2d(H~lA=YfQDeUXNz81pFeGIY=QnE7ITFN@ zK>jupih?;w5qav}vVJNXQ3=JF(L(31R|(p2JWr*I>i^}iP$_Ivgk$@jDToOTvP!ZS zd-?X-+S02OtG&aUL0tTQRwd+(>sETy=Ow}LEya@;W4!CKZxr+gSikmoD4?mpWVc5z zJ+{^|`$kqjW29GaT*eFQ*8IjB18KZZmAR#F#HocJyu`SwHPGF+VN1`QOat2sW{%JFFAb@(2p=g5G&u0Ou45o^V!Z1NMN9C~wo=BDb?V$mk{&bsUT8PCN9Nv7EG&R7@|PF=)`=c>nS4-yhe9tE=Rx_a@$YCM2J+ zDr*~eHQxIuOOBRZtuQ-V`MXxf@lMc(_fJMD8l2`iSUCsB;&6=?L8Q(=#Q>R|Q zSw2Yzdm{1rh9ulT3f{-I7lezs987h^*J=@uK73GUNIZMkcY53^K`$fz%TuxAw^{bY zJ46o5sK=ymvhLiu^A@dZW!7_y23|Bly)o77>!DYwVFFVlopdi*2OOROA$EUfd<9sy zNG?@?uRUE-9g1=^ob=hN1|uprc2kf90&4_vG_93NMskvjAxbTmsKxHQejv$BIxbFn z3Pg+DT_@mMc=jC4iVAS?@J-YKB)5FmkUoA}L;WqJv(A6;#r)}sNzheIfT4!pvg}OX z80KX&@~dNi{`@wtSr6OjfQVAiSa01O0bkFa>RqMz8rQ(VKAz_~rqbkdYBJ~4%VU2= ztJGM}F$(FZC!hbs^;wfi$_t`T9cguj44Mjtou`guK?-dTcqyruLtaqHeoT4w^78+70h<*IR(^{tDS}UkEM{)JKx`!Ce?hjz;o_=C0_o%uwMGreuwq0 zF`Mjbqs8`?DVkf)Obl8#=(-345`TWM%kG3;vkgVcs;i@$9NtsyQ^kg5z|R~%bjZ7V z8w~Q~fyaqjo@0ulzpRdP&Cj;)=8CrQC{9Uto3Ls>;{t&s;khbS`@xoGXS$+~A46K& ze|O5CIQU@VVZ%E>Q^7mT!uKI?*co`S=ZWW^PkbHqN#|Z&6u2JgF*E)L0*@tN-R%PK z7He7Gb1K}tTrd6dJ;=T(r(3tir+DU7|b>&~Z37(6Z$5-2yHd2qDyaPicsK`t?) zv4d}9``v5pyDH_213C79@=Xm*9&kG$&dFat7E@7Q8>h!w-ZS?i+&@M;;WJ=U2aK(G zz+&WnQbWgY;N?jCxBfeh0~pYKGH!2G2x4Ani$H+EM~ZHG3My~rwmF9VY~loC3Yd{< z2TV%gtjwe)G2b#yqaV6^wRL`bFrJc*P4(4n6eqAe)L>m#@kVjpe;ts7LdsuX`LY2^ znwajnrmOtIn-(mn+cP4(v6pvlsp3*ajtQ9 z@Vi!*KfmsQOQ-SEp37GwNgy~gnR<>imK%}DoeHdpSa-dFFCGK>t z*TVeV^V^vm5;lcbZky*F_Svxa5YN6Gje8cFZMsZ-T1EL~@n2{ojaogM(k;J5YYLXS zyjhb_FrSUF!My1;T9|%3+ShQ}A`@$O`o<3c2aCp=syL4@5!{zGNm3j=8)8h&daB>N zvg@mU6XeG#zPl~OSUd_VV+2xgpIye4%AF%>q7D?r5vJ6-tZfG_^TU+bN037ZIp8!h zg6C!bGIQ+XYfw%5Y=3^R$aQ!pp^>R_>ePKC==JD*eS@eb*6Z5~<>%$zH7{pmztmTN zLEJ(pNO3G zjy;o7Y_42f5wiahm*~YWiSEUzQjzy8s8k>aB&x*gO*}{mp+c{qei8oW9ajrCQhEx~ z7h+JW**AVwt6XGnMc3aTJT{YhcD%EYis|}{!dE#mozKn$&2uchr$?s4(`&SL(3qBo?mZ+A0%+QpQ6rhcsv!a@){$n2aIhccnDF zC&jk=+sjk0RoP0@_YVbtE^28IyhxPhfOSKy=cN^ul|U6?3$3XsC1}tC|H=d=g8kf9 zw!b+L3L+eu2%&Y9&7Q-3Zz zIbit|)OcyCOAzmg^TmE!??dcMrKvl)oSK$Nk)2FEaaClDsV9l2Cm`>&4 z<^2p+he|{sWOa{?_0))3MvnMV_vFf7TT`6RETF+(#oII0QyUkuUDjQqZT2&$3jx1h z;cf%#>J?wNc}$nFNjvWM#>P_a{nljNn5qnHX_|FGY6qv47kf2Ve||!>j6n5EwFp^? z)-w<%%EQo}%yk~N*zPbcyla;-7>Vb*Ba+_E&&~K>-OTjU#PLjC!MDL054EWA=%WR$ z;t=mru_*6R;m-+_oi0hvv(mXeki)%Qo?0N(7$$6Afr*o6;`#F4LMWIICUE&hx#$`6 zjHCl-QoJ{f9q8;FU;y3W;r7mtfa08el(XUuXZbF!qH^EUmO)98-)ggpi0~l1Lvx~^ zSqq0yZdvvU8%%_UDePAnGI%gtgojRm2Sb^+U;Z!f&;@P*y}?3MAp0#mW!h2A)Oo>&|TF;pcuFa)ytD&X~A_2(-z75Z?+6q^sA;5XsPdX07S0%jBb7G*A}5_gwLo z8%6CuM;B^-#T(?j00;inj}7J(Pzbbp-MV#8HYMb!hH;HP8alw*#muVRyr%JHb+v}F zs_LURr(Zt}kOkN7yxktcFq`PH#!PP+;Bgni3#vTgtmzFWrJ6CCjS(V8)9E_aTDCGx#|=2)6_ zb<{n;lcV>z8ees9bQnKzBRFb)Vf3}!rHP1n+3$I78Hg1opb=#}YgD5IPRg(GCTsDQ zhAg|z+Md%BGS8$!WIeJ|ty>LajA}~CJ72JP)2`XD#rS$;@QsP{K9S*UOwMN=#H?Gs zOVF8fWSJZWd$GP=Nef5N8|hS|3vKVUGWIKi#Lv?Y>ETwTvy%{lGaHzNg4}bd{*(-y5*cg}^O+AkNwNzG+wCK~NAUsRNp? zbTjulNHWYU20Ag+@~ zhRSxwn2h5FG=Hppf0OPpAWQIg0_|7^e6#96cJHXUI!-rL)z&>lE@? ztJ=#sIn*VzbN~}RgwSr&E7>c*d@(f1cActKZkoOo?{2Jr33?#?W7`@hkeIe|kF#0? zs?uvw%!l+P{`m%>6<^<0TB^iul-~b?*lw6XpY`;$)!Sc3raQ2X*(^zlq^Aj>>$`!Q)3GV)|0*Gd?0|?Jem&^%0J)TahL~^ z^x~;i2B!OOpX&tyytA+A=DCjn$^@{d2%`*JcOllB9el8DLoJXA_zY$;W~j3{$LTSK zTk)v7`>dFyeR+(P?+vSG6Ii>w(%9@yow2jb8u%u){Sj-^)-*dsnCr#qinTWgpL~8j z&Qv@v$tv?$ak-|MJRtu|a9U60I9l#Hll*I=sbJKn;O{SH!p&N8vs6M3ocPmGYHC57 z-Kjk>B0tgmY$l6!U`#Of4$Gd;lf&&7KHcB>$Pp~~N3LT%XVN2a;z&rbSa$Dm0>9g6 zTwUO8I;kpkcoj~CYF3N$>S0u8(i>3ZdnwjMd?mBfP>T&rDx397fiaqiThG17B$D+03wk3vHoi$H+*N{zJA#LAf z%otVvMD%rkf6bHEXNGOUW~%ruXkBj^{~%^npKckFYF=|J@}&!IR!}>Bi~MRZoDcL{ zKt+q<@PuC-Do@!egPNw7>J!i2zki?WCLKq4>WOP>Hu@#4XA-b4t8lno-OKD*Cp;Se zJK9^YPalHhIbf%&J_xM2;Ha28bTDeHyZm%ojmrWI*1fK&^CTvsE?V>Wg3g%%Vl4pJ z_uOLU)$~jPr|?*cTX*cB7k8zH8WqF!XTKAg3YxCbemQj3e%zLT{a(h^F{oVwKKE2(cG! zRo)S@Kmg}E#IVARJoe&tAKJ2s1iGl{we$+N1sp+6#RL25FJ#I@wZXhR#ghXtTDEk& zzo`R8jy(i-t5G*z^_dlfs6nMA8^5IlZhjFb`9fUv#EFMnMK6{>A)@f0_KhSEwylX_ z3^)D{bN?Mr_5a3?(}$kw6=Wo1`Lwz9HUq7Wi0qU^o*Ok|UtJtBK& zkMq5r$2$6a-=Ev>cKiMF{ioYGr*odpx%PFx?$_g)hJTj(0-wcS&jQP(z&hdbE0Tw~ z=i`<0GHZf)SDzEyT?4BG7z%h(Yi+$%F_a9E04%|8R>$@UFuE zwk819spojars8?rjjO<3)(kpFo5Ua!iv-k&C*R+^Y*RM7o2lD=V@zd9{@67`;Vj$k z`M45OO7lU&YL4+bIWwCthj2&oqnf`O59=KfJSd02W$*MJpTu+l0!5?`B2dgL*WT6E z^br^} zrEt%>l>pb0bU+ zZV{Pf<;LTwj1xy*8!wNoC}^PwUNzrqh(^hCo0&!u^wZglvC6W|p4izBMQ z65^v_y~>qS7}1Qtg%oiqmjHLQb35X+6UJHI9Bpt{>)3haAJ?*r+{Q9$9fjC2F1CIN z361YhN%v)W*IWEA6=M?^-htC~H7WQyMbjPenBmT$uTMrl9ig(Cv8UiRu6QAug+s(8 zBiZrR#@JPyr;xtQ#Gh7zQa(fbBY?jPZ@!$M=2^Qtt z3sAzICX#(@A`>TDi;GA1;Onj8NbZf8$9CJ$RYN_r9BwlQh*Sth?m?9aKvqTM{ZHY_ zd8P+CV+0+ya$0`FAE2E0(S}$(1cegMe6Ddgg~efdUZl9D{q1G}=;k1g5Y8CigS2P& z<7l*3<^hU$%MeKQ-pqKlZ;UUrP6@Eq2%y^f8dd!&e!KLg-A;Td$+9)*1jCi1V?Phb zttIu8!wqmb^b+TD;@Bzmy3WCA$~=qq@|eL(Q0&NZf}o(79;K_$PE_KhD_)Yo*C&%g z3dUWx!g!>MyOp+*UvNFg@DsZbP`*g#O-UT$no=JucC#qma)vj)}H?UwT{e8-*6kwIZ8A)-)*+_IR;zMql6g z#z`KHivwN~5WH_ZaH{)mJqdk2E)kd2a`KSmUbU95)p^tT#9a?cKCz?UG+jfJ>F;@C(0j}P8Lsl1}}vDcRr+6#A48N%G|kag&R?&%4B z<-Bk*NXm|m;s^K84+vWccy2R zJL0yB!-lXEJNcMx=aqlM&hKr(QV85IcG>$+L0w2co|zulM5RB>pAr3!Ps96fDL`8! z;+m(+8|*MRD%Hy5Px1V1xd)Y>7maWgxj{bR100CFXQe#>9|f?_6ZT?sg2`q43t+0D zpa+ve?G^@7F2hh+lGwgnZfDsxK>XbV9pzN5IUY9wo2)Y5d_Ej7qo*>w1uzd|Q?DSX z4CIJw=tU;ekd6vmr%T%2h#-ue+{VQ=xce&z(>p1eX{{&v&EwR}9D zv-z661CMkO7Ndrb~8<4S+~cTP(V{?TPI_EY`~i7Qve5#dvJTOlMpGs^m+3 zAa-i!m95H)`hvY+5$a7dJ?FL~*qLERt`EDJU8)wt)GIY3QdOm#lqT@~eLL!ZtD-a? znP?T2{W_~-|xp`sL00mV^;paQn-Ahc|Ol(!=n7=(T+?# zouGLb!~{^X()Kt%JIK{-o9=-3!P^@tab-PpibHt#rbB5<+zAl~=W7?7dWX?SY!ZY@ z#tSS4$m~`?`h!8bc+xX&`AW1YBMT z-G?6iqa0SGuW{3lpGrMXi1q;34D)IT6CM}8+++$RiZ3*OWH`OkFd>D)!{r$HH8K9> zzJpCR+vJTadSQyodD1Ztk%j`wnWGj=;-{tM9C;pat%1%O!-VmHwqa25M#%AbS2T}A ziwr<%8VH=4^HRh8v!hO`Ugx#ZmN-8O9@CmsN2qSA0DN~dA5~ggob-mYPzg7MKX^^< z3so?;$p@IORjy4Jg=po~)Q!Qnj4+$-NrlE$Y#LpB1znc{lEJ01pg?6=A+omK@#Y-- ztF*MMZG4*C)2D+APGq_Ro(woCX`A7~n9NGQGnE^A4j`ZHFtQHfHevXsLAEr?UoW1I zUwdiH7$r`!-WJ>oQ-q7GtPchT9M&YnpmIIJ*+6;QKrm8ZtgH)z7ng+RKPb-T+M1|M2p|1a4JBg5Q!4Tz2!L09kj zVO$Kp&0a&hT>$sOIB90(h|4X>ZxGeo21a0StGQLy9y|7VF5bm#AVNj}>GcB;AathM z0N|67R?1@GI3QD60G)V;PUSDjHbtCsK=VgNfTs*8x=Wgk{?vHE5J98EgCV^)N`h)- zB+Nsst*OA$jFk21>(cV_!x>YO28|}7R8gP-HvKNHN+`0TZ}tK|F-=oru)=E&76i2x z5^k_bKbZlgYG()-{c$@+c2xuh0VK)R08yYI2cPy0?bz=6mxKjm-^bFe!-&D{%6q{B zxUp#%@l`?Rp*uQM)<7{f#|s(%{8?$kjZLLOD|wssSz($pH;MkpeZyfmRx-r_2;zfj z>a^;&OHVGWdB{o%K_RUH!=VpQQ|5I(PFGEN>U8WZck`S|TK(DTPg;H4ph=~yi(2sHNF@pRHP>Hcj5*yv#Uy0$$f=fOg%Em{3e5ZT-pn>7J1P+Kxz5d{4H|-MxRGXs8G3wMlbW32emeN;YCrNEeZQ zu_)+=cRCvQR9cT50>ilD+74fML_`L8uONS^$MPiIe?qnN{(X3~4%juvf=7BU*E@pC zb->Y zMlX4{EUbBRCbZ2QA>99QOqIiZ@1^^km9NRGWjo^8pf=1%IrW2J ziLFMO+rMNkw=6gK1cT%`th8sfCuJCW(DN0Ddalt9BhTcVPn=a>U!MKIu32YS8$=hB zfHz&g&B5Rfftfv7Sq_5_pefH#l?Ach&L~wD=t844?^+T1kA;)5- zVJN26ksi}>I&mfEln~algWL^8a-R=(o2f<%;lOU;M?jCN%3Mi?fQhSx8t7Dh1 z+ivE=sJMJNV09Qlg5k&L1v^*!u~dVBxtA=-%0-C>yVZL^-|%|D`uR0{+MX;@#PjR} zZ(gZS%9Y@JPP;eZOYcyynIZ&b@cvtL z-BX}}K&(q#XQe|AePY*qkAQ8KiLqZ87Wp))PKVj0wIK0kw5K3Xi#-ZY)1WK+0k_$d zF2_al&6VK@0Mt*CM4^%{$$Z;@aXh_jynjj-q)Kn7+wTUjY20#>LMGTJsi+K2&$W%3 zL7C5H*e6(>6UixA)};EXA16jWEpn01o(wj0qLBFqLWGo6)wUiJ)**vxgJ}Tb>~tnc?}*QdF1$44S~0%-ji>RxYe(~Pg~k?}_ri_xErt!C$Y)@cg1iTlL{Ptfp-_H$R$+H=Igq*AiPBlkhvWRBscQa0Fb?0V? z1zxRuRc=)F1~ib8;t`L9=`~6hL7#GZhrM^3*{3SdNkI|F2#gBM$R~JfdBaZ)ozVI; z4-{FSc$TZ;fVaMmn{0~lzDA^@HTs!!sHAfDikMf~@zKp9_%{3k$NNt({5*bY8(OZV zdzxf9N__tWY!NZECgO#`q*&H07kDTkBETHI72vNNn>`QMP6U9^FAH%x3gptXU3TXTuSf2TSV^4u0e<0ox`e zC{6t5dktE!6_D7zvIR+ADd&)GAtgm;PpvZuv7sQ(tcKzr1*+Qh+AbtKNAuwbETL_q zxDZXfSa;tly(oU*jCaOsW7BWmI-)n@#esiX>p#zXEJ&4ClJkHu83^M1vB$a~j8%}u8wh&ia z!V?Bn+l`Ex5CK&c%Sn#vMN;x;!`G0872e$UH_3&%6UhjZCb9f7BR7G1bCCF48y`| zO?4d@jmZ@RS3m9?ibc5(klZb>wtN{EutJB&%}+-@8y+SaA#RopkDV!Ecx?p-OB_B8 z1KCvF?%Wo-_#&V(CIpXc(TT)g0;M~-AlUrb#4RU0H%dN*sr>mmJ8LT5SCV(!$uv&? zYRdtdBQroGS-NBD{klB2XJx*j0Qs&2^O~v%R7`*V1e^a64tqF$WLz}7f3frA)pB}@luQ0rRxjgekQ8`Y~3*5VsZ>dXdoWJGQWg#5xMBp+&hxz3Y z(XS(VlJ+$9d`m%uyq>Uq(=_h|07Z!PVnFn;I?PdhHML;A(-{4LrpZ*i5TBjAVstYM zGE{4H30||Nw*}Q9Jby|1D`U!WSd_+zFe0rgeS7L6j7Yg_wSNDP)Qh;fN(cF$T?n!C zE7G{Ukdr^PqkFV+?TQz$);FsI&{~^Gv+LzQe1u_UUQ2Tl!yx<-eCEwf$paq$7dOYn zrhx|ZiZ-jU1X4$>O>%}2%w0~({~93+$Q?q^2S%f<7eFawW!?zXODi691q%<_c>2yP zgSN5D1rNBuvd`e)oxbv}kW;Tix_KQK;q&XsqY3)>6wPfPbL4xf7nD)-3J>%0UMI5r1%<3utJZ4oFohG3&A z?KJ(v(pQ|(q6Mc~@@0zMEX?$NB@0OtTE)cypO?*`hc^ed?8H8CukhFCQb(d9kQoasW`$rGM1-L)?x}#1(H`okezr*RP@lO}3L{mInM6pGd6@b~Pk!L8DK!|DU`@L9h z_ZISs78f?rj6X&K-b zz;LMIGEbK)jJ-(00nj{2qPdZTDtW+_2c(}n#W%te5nzA24$gFz7Q%h|_U%_Zo4lci z_D30%1f6ubUg$w_COu;xEJ`az{fpprc=&|}->AMRso*h4j!O+!C1A!|8o`Ud#*xeb zRUXayd9pT0KP)ga$gra2-r&!-Nf2L}jts?$T9lzZT@jNqLYoS|;a2OX2F!w_m;^0N z*ZUe?JJPgsMMf^0sM;+BvEwpN<)8>CMa4TWv7A2M zWyy0m-n0_Fu!DPjWI@UHH{cK92rL8iib;MMIv0zcc;>uwxEO3anYMtBkcd}-(7zUH@WQ$;l`f|2PXFs&+pR9C{SnN|mRMX%-y%%9%ZeXE-f z;7{fzLD=F!&Uog4Jv)!4d8c^8+?Txm1!PdhwYqrwHrNJPN05{tFn?ko^?Jibh$}az zSN1(0x^tD4l@H@&nG*U-X8CKllAAV!FGq(0AoI1vu`ZY=YACj;qH-yx?p69*q-XZx z)VzgFFfOCkxx)q6%jquWXqBuS*RbDFfL%4>19QipTI{^C;{&+h12k^NXp(K;gKd>G zI98=&sw4|hNy!(PY1}2|edO7Hu7;Z}1B0!sDY4goG@=M{86NUIF18DOW(pCWlug|w z3}^H32?{#!>5ov7R|D87HoV=-Sr%V|3K9x96If|iM39p?Aa7u6yx6$U5dIi}lE8uD z1i(L{@Dmrc5xnaMxxueM4}CS6W`)W2Fb{qPaIS`e+Vo(CT)ypLP8d#h!lkIz5B~mi zz##C2-r^Al{@8HtT0D1>Y(ikoCr$}ipNbo=31DY?{*zihOFvjf>i4j0y{8PV$}t8> zotLn-v{v{M*dy|{7~%zkd+DVjMS#PR?kHv)a%$o8^Oo3;EiVfChsBqu5<(q-`cVbz z2kvIcr=H;SzY6K>q0q<81L2`*i5PKeCIl(-mJOWccP+e+8OSoZIs*R3_uIVSE9~VB zPTs`7=rRF#8(U$@-KdfgLq#sxh>8XwAg+(FQt!XQKCgixsF0Yh+Kj70P)k3jPqr{uCLO9?{ejzri zcY%s}>LsgfOBV+OsIS!fRMJHNI+@K{2-d8_gy%AWS`5s#D(C)5X7wb}p z*X`okgsP`5S>czSH_1gk$03R~vw;1G+t$ABXIO8D2XhnsF#*=hDAYbdK-jJT07(Ogg^_B9 z1EpZ)ZxEK{k+_ee(GQv%r+ooXeHc{#WK*~LZDoF`5^=FeMT$v|w-k&*Zk_}Bl<2*C z?{s?$m6xtfngaRAlizw(1PZDPaGFoY5>BUN@Hh++sslN681rELlMRd4I0*(aZ()`v zx5r+#iHkg7J2eDMSvpQW{M_j{xNTT?(VB|QiblY7qh}&cQ|y$)M1u2!-V#VFip=>l z>*ce1ox#y+;n=GLg|~PND=>W4_&d@dRM0C^@qU$2Ttp<9i0+A{TFy5js1358lKW$0 z2pYH47xB8%)~OV-ofKAV3C-2tp3kgmJFjd@d<{x&ot*`=r^3YsA&xNntZUb}h!OL+ zka-Rntq{(8%FDoj+uxw?JuQaYs1Pkbi(^-P9uDO*qx88IfOM(>;M0J`F9jaSM5XKI z18-@=K~At`lBhEUt4OLyhMwD8oBp)O@woWEyJBKKfO+)%6wjJ5`w@!O@|I!I3R{gfh#v(EADzZNl+EFs8;gFEITA z2bM9aTFp8EBdV3lfZ+nouQ)(~5z34Ca=-HsEa{!l>U9YC4nbNSwhI(5^$<~fgkSQgUnw?dy8yEa4HKZr9Dp!z8Mx;vL;Flbue}Xo=62ue9tkY3oa?! zYzSQjZX!bCRRB8iSjcZ=!b4TE!EX#6d6+1C`a9%=+Xbqyk{C|J{iVlAIoBBBn)b}d z$FKE#3Ep02-mfUb0z{+PHaGouV;;e8q+Omp{>&UhQHbNd0cU(kfOe#H#w}M|x9X7R z!LX1^pr`PD?4_)Q>k9vT8uMwN8K5fbHdtS?H~-$4L)aG&=r#ZZcscU(oZ1~|jy$1d zAidp2e)Tyuj8lCenBIPo*?KeXYd2I4A7R0w_>sh_g7gzj4ivPxfCT_UgHg`(91Cusy-J;(4T*4P=MEblt#h zcT4F1=z0X>j{q}Yho{P7I{_EVVL`{gjOSX`fq>fANICa^Miyxy9uIx1-^;Hhmm$K> ztTtjA3s{6559H>yvunkS=u3a4*)prIMs;T}M^0@pWAebr|%iHB4O-$m(<7+#wY?o2}gndiBGue%o5tD^hXMORyh@?Gd-NiIa;_Cns zNxWcdU^cAW?w>1QJS|1KOV$*3#vK%Y9Wsi&su@4qIBYAj)$KRy!jnkpp{mF1x;qb5 zLbz%q`gl?ppf4g$LnT0m&(cQRL`w)CHh2-N;Q!CPNckOTX&H8Mv8ePdu>Hg&g|Jgd z_YMBm&m+K=O}kv{?y5Gl1=At=-K2kO28pyo#*PB#|ZL65^1L1CVOp7GJz0*Zp*(ouD%rbg= z%qls8ad+%!n1ReO3;4t^hgxv0;&TUVP$sw^^wB26OTXphyyjv7SFvzk$@h&7Rll68 zc(P(sufMWVEI0SvFQnQ@)z*t9k3Z*`Jm*jyO_r>PrxMQ5$^@cm?@!b8ELpPI5_7#) zaq5-6wbj@)=zXiZqsJ=XZGK3~dL$Ui%uD<;r zn+>kmna#0H<)Lx>-j?7Jj>$3Oahm&|$!X<(ryVx2UA^zOx%MgE(#Bs`HRo!y%~T=k zfRdKploBOW<`OE|bxHi(gg@h=ucaKl4Q+8A$NVz5^DX*p9cit-2*yf;(V4$7z_A z(bvsZ*~(T8mYc=}br!X0%=j6>ucM!E{Q`mkb}9rsLGi}UjB zIijMA;T)T*8)m7w1|%!>QJs!sw9TPdM3OVLi%fI<>qR(cJ04$~eTmO|;rJ=xiUgvQ z7nqszHGHI`q=uYX1pJZ&UMT`L=`BPmr1###;Cys(`KS6uLYHJP7p=bjjo0ook!%#0 z&Vgwa@Z)ZYL1u7yz)rC99`xA0?o;D$6bVHBrmdvCV|83`>;~3>_Xiu#?Y!O2jtTO7 z$H3R!W_LTy0||PgEsVT;3v8eu-17hT6y)45yzTVAZ9K2L4nLG*Nnj%)W!+?qqEazE zn?|(3#8p~SsOKrC=x~IsU*}Z9>BiL)2E7bNsdP>nQ+>fEQsP>O%jdIpuv%{%2rX~R zd4MC=Y!e&RJRMAj5c`4!%x_#{`pnd+qQLl3f&Wsiuml8Kil2ejC#6G=+y=Hat~~tU zt2Z3*Zt&kT-v8?`MPtW;!Fcg_ju^Kw{f_60^>CPXlwdl)b6 zdMF$^0f_(OQ}S55vO9h!pR1--&4PV-9$(-LdU3{6*1q>>a)3O9fZpuId8yi zmESzN=%x+~$ibnJ+}m&z4%0}(UG|7O<;>?2^hB$aneb(j6}d5qpsp#Io3h)*7Y#)j~43!xB> zP|6FKNnj)30T~%QrJ&URzs-ZsMPsZ1y?O-60^i{C;Ye{w`@`4X(qm+EJG(YyX{cCd z?Yq@ZwPpVZ**1xvq#OU|PgICOPP$2h1PrPL%iFBFICmqCZ6b^^$&G;{`zz{OR`nivqtTgse|+n z8Amwc$ann3?3tL4_4MX6hqY5K%enh*pAg;|vS&;{4-Xo{q4sEgtG1J6{^|LVr%y$} ze13IE$nH?m_z`GWlbo`D2uPbx4BzZ)6$NmS08O>%!?7zEePW;@ell~;*)0UOVm;SI z!2iYXHsT}JVn?!N7WO0(^hVc+ellfK-MO~eymrxuKE15N8JXU_HZ)1RP4b1kX*|(uE%!}fM?fW;di=Ak_-28&{7KcPJgFC#{E2q; z4kqRUAKvmd4Cy_-ghY;@L)&2nK0AU;h;ldGFeX2Ugb7F?-MG84hoL{5Ew!C)<|)|X z2(XQhPB~w>RguL_Djj-BDp!_Ii^s@oFmI(bbU1RHcr?`EPuL~VV0~-dBvGeB`2xc@ z?Ob{ela}l~k7=X(+QNj@O;GxCA!j>pkTt#iG#xK1)p0ROX zAKyK_cxb1TlE>86ocS_)~(=#dNfI)!n*! z=jAJ5MPE|1$;8!IQs|@Xyxo5OAdFyNm>uOvP#MW;tQUG7Nk8H~H8hv>!5& zs~DlN*aIq+e?YRswG+u))|44sX2%_pna&diybwO=*(K;Pc?THwrAc}5uNx# zBx@)&#oIU(VOtg4qOI*7VYzvP;pad&A>!=FGEKN14Rnw%g5>`%9!7`+nt{P%82cR1 zxPpUWEb^kgY{mz1uvgkf{){`eMBs?PY2WT^-2F$soG>|JaYj2{as76$ULO^l(Muhw z7~P!!$75Wvj-yW-EOb`v?CA!~ovs=ytLM^M&qlAEk^a@^;a6WhOD-)N{dlC%V(1-G zuH_4uSrqeE^UZh$t@c~4yy&Hry4(%NounDr)@w%0_BDLCsCcc@GF)F$XU*`Nn7Pgv zSHc=q@Kq`4WV^bEyh9Fn##gnFAX17riPl>5((wz|7K3E|F?h_Uc19pVF3r0V7(cEQ z3Bjxx!HkD6yM!(@GiypxlX&N?cDBe&t+zmPXy+9q0^7kB7!9gZBMh&D_(>E@*x=YXy}ooeWGgT;OUsIIo1RdZ0*0*W+*Qi>yQuArjyAN zQQ}$rje|KaK5+2oK_@mZA!&c{BGUfZ`y&Gly~_}F0%&S)>;=((AGt(>2ndIsUBQUH z`GB_Bn=QTzz>H3In&|S4o&5W)-SX`Wate!^K4bMikOZ_yP`(kMg@sYR#cv!s(0yZ4 zA0o1=Hkd{8^hY}RRE?${jqV$31)C`ixHpq?nb%icTcGHvP&0LIiG_^%yw3> zZ5rz1+eEAQL%AWAp;I-mkb6(HuJC?hbFcNIND9vfVg{%f$=` zQ~6!wL@rD-DRx3qY{~s6_s|L@+{SJQc@&Nf;-D3Z&mdXW)mf|Xm59!>kR{h?@jD?L zg#R*CSK+GYV!SH(jDPxlajz;SswVC`vN}0gTB0#+1tIH3k5G2~xT6QJpOuyzN}3$< z8jf;Os<0Urkge=JW%a3p;1+j&M%-4So4?0E-zpk3i1|c7SKkY5dDK!3Y!oZ)u1+5Mt3SsgM**MN;3v zf&Pg4PL|gm7w9vxsHB!%jEqX7!D~YywekK{d%A2wGnnnN(!!rsp_@bxkGZy z%09=XepLASkK2ZAiIkoVqrg|T8A+9^Z)wusp~maNx(?#s{a#A=Qpf4F=h42n>o%ztIS~AxN*Xa*$jNK5 zUb%8yzp!{W(}`bo zwQEFk10!%o7qb5OMnY2PH2%4pdp3+2@{u6jhioSB=9`OQoD^kmh#cS}uDrT$>o){l z8AxOWXzX4s5fQ&mro-ERuKM}`#JBG*qHSnB@5KE?fe&I=K49gR{V{R1?F?SaOZ(vV zbwpfQPm|gA9@sv^FUrYKhu_X8Dnz?GaFkZN7s&5*7BTsDIpKYp3qlQr=X3XIt*knl zVP#rE@o<_+xwQd#f1&Tkf)I3A(OHxBytUEu_F?+pZiTh5mKgUtxlU7h##&cfjxb2# zO!Znk3DQ;O?$uvXm^|8Jns=3)PoCkbK4JPKxIh(3oRkKok|Cy3Bj_&8y7}W~`Muir z%7axyqN!6WPg1Ri9yak85J=CAFZEXDzNoMCGbkYZ;E|itdP+jn`^oQ%mT(Hp($L}t zp2d?c7rUbfU%V3=mU|4(1pI?(ptIxlKa{7TA$I)q!w19G(M&5Z$!Etf+t9?jRO2SK4EBMr=x@Gd`RSNqZ>llEStbDTVgXErUL-y(U!eBM^K_fz|EhWOAG zfBFwUCmzTPX!#5~;tdR>R*Z)ouCl*BeSIK|TWhY1D`w4Dwekbgr%C>TWYtWPvg5ef zB1kb@3*x-({2}Half-!1cMdW6ba^q1!IZrSV)A$Oe(cNCA!s~8BJ-ohZhS$StSFtV zH*f#NBj_N?oOs$ugf4uBw`U>4@99pRh_ZuYnxB_T7Eb3cHSyCC3#Xf#(n&#`$!Or6+w^&^l_}ta5 z6K%(Y0OFal1)$LuOzYhXy1#A5wMJF(-!Jqzg*xO-+X@D{`_qv}y0O9_UEBw;+$VLe z2%2%f1q@qqxh`*Ta`e>TMo!bgx-`U9)4bqLD)+Cm=@|_*-u_<3So%dM#D1=?*z4t- zKra`iwBh=vg9_GT6sj`W8;J@g5xND3-3BIc^mFbH0IMoTCJs5u*QRU9Dc<4_{mM$8 zO~Q5V1`(%+(gtgCxO=ZUsyqlTj_?s@nBzNonP zDZRr|t0};OMe1kWVEvs$b+Z2k*E?Ox;70Xkd*{|Ez3fs+z@(T&%v7Y?+ER%BClOt5wi^&U!hP^G;Xq{>|xB1 z+DQ^W^BCngPrJWD#z;X!VzY5uc4-<`LY5Op*V%|^<@%3S=ki|CwB4j{vP7mWx%q!3 za?%uP#cUH74MtBTV7=iyO)sOsU8i~uL63c5?hrFiD^Vffyx_;uYjIOY)j`WAPEd%x zH#z{Z`Yx52KxBSr%IS52fopMrU%q|(2;vz{Ik9|+9l2rB`Yq!6n^G$RSce|MnqRMY z$cKHr-rrg3f z!{MxaDnq_n;rqVa-K*-W^OKkTs!+#luk{s}wHZ7=GsrB>r3*AS!wiQJe^qjMqEc}cy-!M@cz=U^&M z)Ihd4%YCdfi_0scDeY61W@n@&%DEsbZ}44wSe!iE z^Vo;i^b^K4Z&Mb`R?AO{GPg`+R|cXMieG0>Er&^23y@HR!h9 zr7Kfw;&*Z>6_;@j0fx`nzcaNv!zYVq6Rl71<&I_C9+zvu$*~iB4--=9K**lupQN?+ zZe_FY?lsiqaKprxT@(>}_e!gH2*AJZ>Yq!3c00Fu3%!j~WhyA>n%R)L^Jyo-nRR)8 zS$dzH_n_abO)#^mc<{3#a%-K?87z&pv!@|(G3gvwI4xo+xkg5=N7lPiTjoX4tXyCJ z%gBaQO-~ zb9&oRQChZ@fc;h6VC zXm8&xc1H=jN?n25$$IwoM7!;3LogB)%QnP$9pe^-3G|Yg2eA~=TxEA|NlN$LZn%En z@4blKDdrEvMLv64HB`6A(E;yWk!C{Zdn_${Z3)X|Gda<5ZAl@v&CaAE z;_^-t#f}D>gVYF<*|GepZ=#@s%>K_&X!86>aWLZ{AX4CK$)a{^M@%; z=24iv{HwI;C`1}eG}|0(Ryu?#FsL!NywjrKee@l)ObKbsZnaBYw6GdwW4vUET9I(O zzCpq|wcZq$C{WwO@U$qFxeEvTC0c7iW2lay?6>#+m)anIg5F3A-tOIsYETipuW&7l z8{-ybcxb)hwO_b>ih9c_6-0{XHIH&MJ=|vrp*KNq*U)z{@{!-tT2+2$f;FLi`r(XW z`eZ9mi00`RSu54JsnBS{Sjii;@o1>V@2d5Ghi z7)PhI(zMh&~J3gK1O#wDUEmlw@R${?hC z$o(-`qCUwP%p1-0nx>X^S_mhkXErXSjiugwNKWNq?aPitbKM%I7o$QOu_~w-mdVq1 zaZ5^2{7kRZ2-jpx`kARc)!?3hAOGMoRLowIA;WMPOuwi|fv${#Qy3$brk)15t&~1N+po1V6v(rNfCnX!Qs_!RECkj+DW>xEE3F{<9~;&PTP{4a*|$qRSU-w>gYM zblRp`L68rpk<#jO77`2BJH>gfPvy>ZNmEz`TZrqnOxuk#{6@X+t`(L1mhKnzBl)2MJq^woJiyLjt+lv3y=iKHx#L(bOh{%Gp=+3R+-D@l!>H#oVH=gLG>GZJ*`9gTH7bfcO51ykDb0;~ZC81sAKN^sTvd5B zVlbN($Lt|&P>_aLQn6b!6cA$<3sp;!SC%@O(P76l0x~YFLKHZ`Qq2 zpX^@AXB~1nWAe-H)u#Os8H0LD$#0wA@03NEt!uZKFuwX0SI2xamZ~eD3bP7}^fWAdpF)A44Vg zsF7rF@8?6&3)`0&RDht8*crx!@h-S^r?=fwFSiic8zi+Lk*HTl2*;@_@}0hqV25M< z{2A#haAd}-k=(d}DjIz8vb2qF&Sm1|$KGG`Rf?Tl&BrFXkRi?EV)XmtVQohg5{ zSZEph!Q|OYNX~TWh?InGr%|xAuY&#xm*L>T>?~7eEVrs9Nlh@vW&P!L))%N~(b%4l zVYoRlcRpqgNi0&36nd~GlQH`LL7#8_TpsFB`lMCS;0K4}dzZJIs)`>kUN^Mr?Nz2E zpO}fr)R{jmJu~sTs1#>yqH&aLk?I-)f5C6L&P2G2zzbHZUOw)8e)(}b(WJK-SzU`8 zI&%cl$&bHP9$0TuwZ48yVD2%m#rW~r8rA7{oRRsG86GXnk8Jb!XA{_2T!>_q3pyga zOi`TdkUjl36Z|57G?!CqjT@bm{3+vC=YaR8?lTdDSh`DY6b&&F3B4;9MARqbqke?c zktt;0o1b79NxWH5XaF~PYKhGI|6uWHX@)6N9}9QD+*c&*1O;@;yRQGf zaqoZC>7gSOwKm$RGT?uHe#O$@p$@&>`CNYcGWI37uQnOQ;!fY&xe_{jaNxc|B^;4+ zd8v@h^(Kn1pN~BhB@lc`jan$DBC;)dChfeSyIjUno>iBBg?PH1J(2&}W3l|9+!IN9 z(~mb4#^T;J+a3K=<4-ApT1d=ZNUX)R)|<%_+*eJnFNQzLAN~;B<7bL$y9^;E z-<MSK_30*&FT=fKc5VYwe+-@r{I>~0c(BV#uH;Z&2*PTo29OU|raZ1(w1`c3w02UW{ZmB5cS5B1A~+)Jj3Xf8+WKy^h;_F`yl5k19Sj9S^XJ8x z=|o@7#URxm{!J32e>0C;F1g!i;(KVwt%^9e6|k*DQ1za z?;K5Q)M^pBoINk$0mVkl_c?*wI*ZxvUKL_d#vFCG_5K61ji)?JUrp-M9&;Z|-oz2e zXqBtYc!07-VzdB?jbH!z(6ayPa;bDveBR_@!*tM-lmTrWV+-q!4OJaUp_7rbHLvr2 zvwXKb<>67l(L$!3BO>{&ETt-!-1y$Fk?ETSKa$$Gv=+UAop~>j-Vw@!dww`XK2Nd~ zY5qqI5nTjtTKz<}ecN-$gMy-v^q#-Hrhtv{k(HBlod*uAG|C;{elgSv>$5<0o6%*A z-quc#6el0$n=;;tzIpsSPSqk0U375|ZL%HYw~l#OfMOi&C*M5NMq&91-3d_IZra^^ zx*!2X1*!3^79Cr|g~^mf-%i}bR8>1?=y#*zs)vic@}@b=`c#xBoaa}->OG81JW`@wMBNLGO&sASN{rh z+#n7ll)Pe_;(!PJ{KkiLBYVhe(K%5>NPnLTH+O^+m^t?P_kC4?{SV|mrTHnOdnR}q z>F>{VMn1=|+Bqhq7?|rm-m-HY1yv{p#+QEKVH9Q5g488zNy=z{*C3|roDL0jV4F78 zQN2YyDKvQ-m%$%OOK6kJXQ9bb&1kB8f(rSyI*&xZIF-)$PzSjXbB4s_Cn&^PPUzgj zA|_URI-6~|Y`~rWY_eYO)JEM|!pjQ^;|JTC*`ptW7qXhQ#{18%#vzXTw4<7(Op!(W zJ*R3AMH1hVasI~Yc*|zh?l3I zMX#UquED337-7k8xt;n{8+D|_mlHGZWQm4A`KM&rkj=fQEWKZO2GH!jV$5q4jy)l;;&oVU&~}>KK+OEhScVph zh>?p0n{3CyG9HEciP5HkF||WKN);2wlhZFe{xaNHosNTT{C8zSW#g%bY7k*%#!(yX z1cS{-mHlfZ6KSF{h406rE!1d9E?A%4DPEmNCz3Z;IHS5z-A5Qieo&e$+naQ|%8+an zkxU5aN?Tc!CuLfi7vQf?$46f6ogUo0+M8n0rkpf=Q+Pb7ZFxb(EN%6pX;SWcK}xha zpO2>Y$dkJN|DLl1sZkJi=Xz;~5OHi_9Tm60oG(pW!e|RZGth9Aj#<-gB5i`2pW^w9WS~*Pa`z zRL`IkjAd)gY*gBtR_QhI@}M(-y;*p%BI2@>q6#@pW1Fm+zc|qp+}iHK=WTlY z?i2Y#L^T%Uwb{>p60*+z(a{+jD$n|b_V)OKq0HG7cTU&Qq(Tak#meKexFfGE!CfbYmh1N)I`>_92>OC(Q=2(CG0cyN0Sy?XyZi0}Ms5HYm89SMvQI&h zj?g2J&2*tl0w6HuFb+znYfUN^tckpHYqNhv%l32whS><(OpQ`h*R-fbV%0``B4pRLSq7F z>5%ipCsX;9W*C!n{6FlybzD^Kx<3AbV1Ocsgh)w?0!oV1V1OenA<{~SG}4j-DyW2l z(lH7Of)dgVN(f3Q-6#l13)1y{)-bVn&wlrQ_xYXk`v`_H?(4qSvvjSG z9-Rim`$oQ+?fNp|8?*8^vN*$+E9h^PXS6L&;*y+&ry*)Le?!`;&N@{&dh6iUw1u^C(~2qJq@F6u#ObsR@$a@;XYjv&jbU4DD^tWt*byoTPd51XeKem&5vo#4s5 zUU@WG(SwJdIa5W^`^QV=S_O)U0qfc3vBKYinxmdKM?M#L&3^e+!6K(sD*ChIs+Why zo28Y0sASeEm7ZG4?T%ME8t~8uV-z5k zO}=+n8+iUpall`fAfFKgjF&X#1w_QL);6dSNFCHk5NJ$Z+uFcHtD}Lo+C!3Kew6PK zRxPUZL`@0zZaq7&0ogBD|o4YW4X2 zxYE9flg=?W>0-JHh`YDdLAfj;vF-^9ygbsAlJ>RMeW~s(8Pi7~(>_`H{^6tEghlj{ z@SbE9dU3XNsp*dzB3=`3$N{~)^nh8)LSoJpU|6WO{T^+6FyrhDSz0usf>Q3GZM?lK zz_iYq8B>$KNg?GdXINiJOJ|Lr0rb7+Z;6}QZwk5@;tYBKA(J!kI%9o#c}!@QTc6C% z6-PIwmv_gK8^s+@B+_(+!tjKt#TES@LuVd)r12lcV|M z?MY;oYZIqLX&JbY>DC7lXNl|P98Rw`O4b@<>7|D6E`PdMo_0qeS}Nx5_1@p}vYNFz z0E!zosW$@le%b7|wpCVYhm6Ba-!K0}iU$xKve3Okm-Jd}YEx;=RG=L}mG3v-a8#^{?P zPM$?IrZOd0CVz)jW>p&u1an5{i?(^zeOt$I`Jt+)oPSEdYPz`2VzzK%@{g{ue8T)8AGs2x3{-M!J1DoYA=GnIb) zUrV>>Hb1gMX^=tkZ_RkBGtKtLTSuR74O<(h(Kt2r@uqXy^VY#TDn~V%`zQmR-FZu+JJ}{6y;99&{A@uq&{X>Ox3R3w8`dJ+{(VjH z(tMj68Kd75yjL6~U8kO5%J91D5ItqPKE9r3pXT{>H_e;ug;9v%A3AUOJ5|&qX+xQF z7pMQ0t@msN#lbC=bU?G-Q7riWA}{m4@G;Ulse!ZZ!1hO_kKEO$9e|V~Z&(7#;z*nL1QgV+h8iVNPrs?v8(GorCB;-`2vs+1GZbHh%sSy~FD9 zxc>0P!S&$XZcB0E_C2?_YxU{kd)nr@x5JZusV`Z7oREagX7yH;p+IS-!os5X@3GEb zV?%)BqqD_$i%a!dR*!8kM8Yi)t?KKjit!sjmA&pwefosW`t;Y+UdHw0r^hpiey#7K z29^(X3=Gdsa%a6yO_RT|Qm?L36XsnerKP_WmQ}~^4jvPnrn1E@w>j(6N<@Bip-;Tb zZt$RSLxc31*T*Ue+j1sbMK5WtK4;@~>(-Y~TDzX9M1u}nNsjCR{itGzj#n$eFZ;?t zje2>_bENqeDUvxhs9_9l^K|QaGQIYt9;^>DkHYyHD7^cMk5Bc)V@Rw^e1I zthRmJxM4*c1fhB=T8-whI5UqOMn@23b2Knro$L&J8y&=SGmvpbs2T9)4ZR=-2;Juo z3H*Dkx&Tvlt;WRRg$})lW(QGTywU6!1e!dsQC@oKd8*(c9x$uLx5pz{{DRaiLn%M9 z(6Ycz?~KU##Xt>-p~Q(dbORexrdc&|F^;aDkjM^cJXREiIL>HU`6}c5f!Ne`IsKe= zTHP8k?~NX9rwx@?@!$Kzvxi0u*HzXRzLKjCWY6B4x<}j#(7N??(rccZmRo+t9YZP) zIKy>{EcVTOjy;m-AFmoF>mKH@dZM@n!v2!8{!hS-r+_ah_SO|oVMU$1{C~3YyasxI z+1G4KyPVORRWTV;hps~rnrPyP#%#?~x*M(6cYR;k!Rnj-bEX|*Bv@pvNHm;o>%o+e-_+Y{NTH*Rl@3q zmw6Q+Uml%KPPo#>+Te0>YSVfF?MsrqcO$jik&Y76Z@%n6LNn|d#!D#FcNouh5fmJg z2&4J0k%12Em4RNEi{_*tx+wOVe-YK8#X*%*fC?pV;k`$ID!{-I2Z;NrlE~3$nH*F~2V|b>xozQ%1$Dg=05sYGsO@^6z`B zu!PipWSL~iSf8)6=_}VP-}T*6MuP7S|GYP79w3@}-CfqdrJV298~=qoN4Cg#Tgzj3 zC?Vj4SEmdF%0FDcs_nEeCi6WNgBp{S-_zJgx~WF^Nz%r%LUTH!H**@K06k8|OD~hx z!tA)T7QW>5vfq`TyOp{7_kCa^`gCRiR=x3pR$*vd2Fw|2fmU`-Zn>s7E_bFcr7=9ZF zI(?Jkf&J*lS&^HxvZP3RT(d{RY1*NN^jBADdEd|6APU|~TnQi`EYI6E20)WjJzM2p z5==JpfW=|QcSe6IFK;yqWEltXc-p%MKu-ZZe2@@zt^5b5^lR2UG z010A0kZ|=_7%xFo!ukLhx6~)7ENOC8J^JGEb3|C!qW?rb2tL^0<5^8xbL=s^6DgN9qRsS7TnzD{|Y9>1l&CujAKy32=D z(@r!}vJ`NOqrHDOSK7~*ekhV`?@s99T8)$VzVTs&j*&IX)ndJ~;8#*Y4d~vt{m2SC zn%8@?yz^&dW!8J%wA{XgK=J7k!|JK>&5OxHoc_(;&`=B%P9sK?YP^4ak^dg-p!^6l zrcSq&rg>Wojm>{ZG#=7-4gr1i@eOy7n?+NMHoZO?mtV1Fq@()y8kpl{V-Or1{djSt z1(rO*gRo&eAmhfPNjO^s(z*tK*vHHLB?K$po0EwN4&StNa3l$u5(*AqxGwGt4na_G zAaRty1P2{#a46NMF|P5U2D_9TD*3=V`}7>$2$V)tSQm=zh%X>`i( zlU|JzI;o{+9i*ILsij)E&UR>2EV+d8pn(K0bIC}(4NI$xcL^R9XRq~VjqgrKRX|kw z8qJgmadQQg9jl3VvWT$gvsx-O&ZoZbIKBmDhpPM7y{M`AS(t?eJlNaHdQxT@v-&D` z8TVP|>6*N)TXL@2bia5gv-!)-*FVBTHuy`}+PB`*sW;@$UPNxaQi?mI5?Cx1U$`{w z3QkM=UMCo0wsRzx6l_bCHaN7xf;0e z!4HLxcYF_V%pm3`QD?!}%Uhw8Zf81lU0D1XLw3KUx#TeGnV}>WG85}!ulvVcl|16rXK%~9 z=Gh^9O%6J{U17X1-Qkr8y|@VB>m-}k7@`of0R%NzsxVcL25e(bFJkcZ%Zzp$;qoyJ zthB^gP9OFu5I$8PeBCOHN#2K*u=qOfd>f{ma-zZ0)vKLV+7|a{pmDDN&5oW^;d(YJ z+NPxkpfT7Q`F0(NJN%`JzxB+b^0xBR5>^}(>BV2P=;Fq2p-Bk+xEI156IXBPSU>;2 zRM=q{?>pp42dE-tX0Yv+A_Y@LcQ=c`v+HQrk^bVdrM59M6ajs^4^C;(pNk;lovmKg z92_}b`WQ-3*3=rRAE(mbO;5aCfftVpfqQ<|1q2RW@GH=4UsGAiLxRzu&S6aJoS4Ed z?8i{Fl(Xwy#7a3NgfD`BrSJkw&-;WNx&-mH1|@PQ*G0K=1+nwk3ceL_C#T!aFm75%Wz%kGU?xw%bz>SsHE;VdO0qD zM_N5XCT4`_DvL`)bH2$#+(XkY-)OQn>B=Hgg9Qzf3{9uYa>byr*Nb|pve_>2D{#dS zVxtej66)~>N$Zf}y&Y8q$wT5b5q!lEhu|2C)7rmTbYV31fVgREs-He<%K&Pdyf#xh zM=HG(G*hEBAkC_^xqLQj`1Yq#?*XV#7uwv=k#yNI9C|*H9_Ss&5@YdsA%B8;;OxAw z_VuW{UV=6C^l7Hz*|n-BCPOF zf@yr8VEhb;$q4|GbiK3jZ$RdNZ~*hAqP1NoAuT;t*;d=fQE3et=!b;WjG#F)7Vq2U zl6QK7lwueB)Ti#OKkfw39XxQb_WD&zUoTDSxq*GHDnK*UU&}u)^r*}2NJ6j$nL^?2 zm5nZ@ZrILH{6JY_#Ni5X%{<1KT#~vyW(}BCCW5I>(aY@x&?@n|e#{Nxo!j_*9gH@* zw$oelB$F9tfbrwY3JhT`zBmh&!mm2=L6Vj$&-SmrZ0S|<(}B+E+y<}3Xsf<3l$V3S z^kjFVrT}cU@N0ea2cF^aTb73K=5203mAmk%QNq4b4Vs!nMAb8qG`M9x1jsbIb3FRNz}8OF^zQ?dIZn}m>VFPqt`l9F*|ugj4h}sy-R<;=kLGP^Fx6z-^0Umtm{`) zh9~=--*$(cb(A(PpZIYWjQaq4SzhTV%<~+E_p0E6Qq=-mG@UAOBB(wE&_5Nl@VtcR z!|=_0LjkK z<0Xv7KZX#k;);?oA(0|1F;f!llP@%J-Jyrjl*F0RE5~=N+|dxMd}*o|lSUz3L8B?( zXRe)qYzlRjOws*kb-}M;S*YY0ru+ zt=&Lbr~1BU6eQi ztKB#u3HWm9vcZJh00w@Dh#D3}@Ug364}hlKR0PKXKdTBgNKMDyi9nSDLVnu;28*{f zf0i;5x@`dB`713m0&mU~Cvzo3IYYGHZK-oaV&GA(=zt)Y33?D+<1qh%c-8bOBpI~t z!`EQkYle?~M>&EE@Ib+W(8Tlkh_|qLFfqVpI__%VgRgE(56XC*;20{bR=dbtH$MaT ziGKrn(aZ?8&|oi04sCuI3k&B84{Bmyp*VFU;1^5gnz)VYtNgz|OO6ldh<88R@3pbu z`s<57-Rs}RN4>eib=`g}k8!s@3bCGxg7^q%+U)|Mhw8dgb@CZEEc*I&t~me~&?q6L z5e7F-{1v^E^AK!b>t~V^!43iWjrPTn-?*JnfhkQy6F%0yqXGRSfK~BtK%Ya4gG3%P zWJL>^-pAx^_a1g&NhKg0#0Z3y_yF)((b6CY7-k0Gt2{|y0q63Rz7WL5AOOtlSB)7V zgd@<7m{P{^K%NoS)Dev!){ne7Ru2zOpic~#5bM2Zvd~)5tk_cKanNf;f`>Fp}$`%O1FM<9spk$f2aiQp=7I4SBO*8x( zn1C??Eh#wr8YDH=gzY6gZt_gQ-HQMu6NLd{AT^HYsu2+cfzt=(9W;bI$+|@Gx9tG@ zR03pIb2J7;5H9NLK8W@BD$?nTQYL< z+?e)IhY>k$esfq6sIQUX1taWJK*#|gn%>W3z}%Zi#)2IJ6frjvFPehGv*K&>|0<5qyO0#VUy!qyr02F7{J zgTazU&cxh}L2qb)Q%YfTE4fGg@2uI&rV5W?C0LN$@{G>L1zq|8KS4e}WXj zXa5tVAZz$fkopgj`VW%Y&h-BWkyJpZBpb8EsS8@5NYmE!`?+5Qa#}vt$89r4T8?0Z zJyeXq^`K%Qm@W+X(nhgV{InebPGKMnLK)&Ba!%L7uvjiYFK;A_ykHNBU`XPA(1X+H{c`coZNbX0CL9n+Lc> z8oePAnU^l7&z*u+dsl;F2S5RB2a#60!Oe5aa$3@Ol(+N6$4i5yijVDNed zi{SW!TZBcJ@I$xcj)CX7xjS)e)iOOYH9!*+&>FaiBGjc;{mlP58oQO|v!LUrn;fA9 z@4cYd$T|F1mVyID84hbVzgZesAtFr(2>3C0vD+74M1MS@OX)LrUe!&31=hZvs3Qlts#&e@=$~V<_y-Wgo;_Vu#Vj zpxh3F;?nCQa#f+Z!835J?0t$E-F}+W5RNMzJ_dmvN^j1&V{kq5`i!2d5B3QuXizw3W`Uuz@!pgbV2_r5M_T)z;+hVo#k59x25`MLQrh zCx&I@w9Ws4cDO_<;MdpgW|r!s8qHr9QqU`-a%r_8HD@*Vb;7 z=B+bE%lG}nO&1Ha>p48x zvX_lW!SI6E!mWt+l-t~J=beeSJ;m0yFT81M`=y8RM7+?gF`2XnZA>fiPoyWFwGLwu0SMe8d((*(5mbhH<#9x3!5MrHCKwDpS z3-iHNPxu5ND+($;P-S(!w=4k91~txZpj~01^LSkk+c@jn3ic4mL*N0!4(<^$ho};e z62TL2Z$S+1!fa*jUu?yXxbE9#sf6rL8?cpr?=;fG94|vc0yrEiw@qpIBHEzb&kfvw zc@~%x#I56@*HaC^B)t6POLlpcG!rgn_&QV*D@Uiw~9Yql=gnOSAhJY^_wEXWhg&_uKgYolPPPQ3n98dwHofZM_UFizd6pw zjI%QQ7m@l0PNpnuh$$f|*WayOxNxXo@H_@E&#|da%orK8Kgpv2OifArwahIaZfu43m%V-~Qokoqr+WFI)b8X1ZXg z`@~vk!0bEKocpN}*iZ#r0aXIgI6fr>X@0m+tOY{xP9hvoH|4~2$F@qLH}H{Q;3FnDA5mhRnlb7hpSF<#r1XhV7~98(A(D(_N%_jY@a+hj zuqy~(GD-_5u>F=LvzQf9J+Qf$zgy`R7>dA~-yDA?couC9crbaVL6ok1)rMP!AuObT z`1l&)PCni``V@c98(L>H z6JFq^($Mo;?i~dWS5TyME~k{Y0+z2`@OV+ zu+UQNaryJI!9xk$332f?@N{(Q>jEyg@c%$XlZJi((wDq!yoVk{rW9OcN<(yQQk+a- z&5%%o&4LJG?l}aj4-|Xx^RIjvMOsEj#ZH^<9i}AupvShnwBpm1v#`DOQ7zYBEjt9B z+iEe>PDEv|=2GLh9S?CKAqQfl|FHfS5^p38{lmvzTz;II3W@$7S$F%R0Pno3Tb@gO zbe8FbW~hi)jxg6PV#7fWi6YaD0N0`uLxqI-btpDVW6re6Nx;SFf86^|CFeNyM`@a2 zcpVLdnbdKZ2{gC{P^2`&Zr~XnxL@!=+=uenGJs3HcVEG*7Oz<71t06I_64h)*|Yce zA2|e`bZm?Iag7)z@mqv^R{Z-{z_MfXX+2oJ1HZroH9G$QXK7-;^_MG`{bMKC&ERxem?R1D z9et0)ARyr4p?$j#FdX)0a@Hp2+i{SaxyoR#LsQW6^Zt2D30(43I;}f?2W^Ha-h?8F z@NZij0*5hDC&cWC2KQC@IJ8>blV78sN<%(d& zr=lzODM- zC<7`-gg9Yd2N%EjY1CV`?`4ba!XC@Bou(1`I#z|@Fc+-wK!FEu*P`|?cT#<9r?Qvf zg9RSYp9jb3?ICI1Z-$=_Lq&sTW_#7B*^bYdze60NXfCdE;Fs)M4!CrV8Vk?VNa`0| z0Cc(76*W!}ZnFv0gtC(*RqA(rwIeOScEQJM=W@a0`nTLEoQLYa!=D)enT-Ua=$3Y`(7xB2S86|GXC>wIu%}LR?qdV+|br*_*)ktsm zJpUO}05OUGNA@sV8bJT0zdOBXJhjtCVl*tze1L#_&{f31hg~MKo1~fc?GFfebosOK zPq_BLF#-U}q$I~>;Z44XAIyPFm`&qS7RJ*+?=AGPPydl;r=;0JGfB3A7y#k#Hkh=* zEk4f4I)t4Ht3jTst13$^7WW$evpZiP9+8(&Qd#ns)RcLy)c~t$|E`b^8>aq2-Dz9C zabvPGYl_bU6v3L(R6CEB`9K1loBDzE0N%bIUk0$4my3=NGQbWH!6cz*ANwP1EdW5G z|3}u%c$DgVkM0m>gx(>E=>p?N0s}Dm>#4la=g8Cc6-0X+a|UIp>@D1c-|RTZ^#F#5 zIIu?qbIUa(TSPJ-h^+CZgE3H^7l=c&z}oLYRW1h>!9(Mz-g{^@RCAb84RDqQTpClW zgZrwI4%}oQybaeft12<-CHoB^$C6o{{LUMX3RXs96c9$RJ1**vWlp3Q*Z3Wj?(Dx| z7!n}De#2Ru<4q_$NGVZ4gDOh0BKvS&gz$&fPO;;v(Aw!wrPtkCdJw?D`aiJtLnveR z8|LnoQY*t@Lufw=-hFdbikQhr>nNOCOvB+Fzk-jx zP_^D9SH(?hF#lCSfXSub?dF@pk!a4LOg-Exh=|{^V_?^Hf2JFYlU-AMs4wRh3c@QTULAmy|MpqQ41xQCa2-bZR^c_h~zk$^0aZ(4hBgSH?^iyHs zR0$K-fyLZyFe4)@2YyvnRYBCu#j1eC_L3f(;_9t6@2w0I#&7~;Ks%9?ZEeidDv9;A ze(~LXq3Yt>Y@HLhS{q~6HW(MR8Kl86pFFQYN(vS2e@BQFAj+~4BV7?QyE9BZE^s{G zyIo$OQ?MA|#k2-&0?#}X0CX}?b?mkk{s5~O2x155B$O=hL4ZJh{^GIo_b+U1ZYb2> z=ptnZERx=eW}v5&J+w3P;z42$&mOo0IC&Ig&eK=5Hb7avryCEJ8o*mY+(vhZ3<1TDKiow_Vr~9G?xB zHFTk;dV@&;^u6(wTrpOM#0ESZeouB~4_^o;P?q3mA&6+!cj*)1Qi^U>%i@-=daz>n zV8;Pis@fRr)hTxzH(v}40}$s)Pr?afM?ma{$T?(*hZu4;rKz5f9K&+uYykbtRDhF| zOLW@^Lyl+^1iT8suuDPRejJ6pBdbB{uj-@vb#IYz5dg#G23s#bSZYUYHTC!H>Ct?) zv}kad zk4la^-S4I)Ii`7vi*k(8@D%-V(!*T)XqisEY6-oSxjsUF=UI%yjMQknLz9$yM$<-1 z@uJSeisVG&n^N|smQiAs<%&JGC*I~$y0Ysozl!Dd z(ap^csIO(aEH#?v+0A4LUudIjPBC(i7v@c=am!U8ybtp+(4R3y35Dv3X#DYrmaqQW z%{a{;%absJ=OaxonX5mkU*uTO*{FEiiO-MjFxHzeHPWgAI)|8O0Q7;;U$%3i`d~8ktO*iN2@L=>26`LlRBa8#Vm#xs~tU;ub0K!%L~GIDvYe9Ng@xt;-VGp;2>P^cOuNn;wZjQS@0;Hib5GY%A(^L&8+zcw_$1?e zsyZm2J2%xGaR$CgXSX%E#n^QWb|sR%Ioz`i91cD1CPu$gIfBggcwrWch$FeYM)5{X zHcmN1o3MOGHAJHCR3rn9<1f`H!bt3&5}`TL$SkxtbnT7N9D_pdO@Q6pfqO{${zNHY2$J{q5OmLvl^rcqo<0@cma%s`mEiI zU{!(@r(g-5?9JxFnYW)7M@1xk8LPlX$S`b#Z23upnO5GmE`Bn|=G7B@`I&juVNs9Z z&zM@C{PX48YjzD}{mV6CBP|J=j%J@Dzsx1=hPiihd_|Gd#N3wL%r8lU zksJqe#EN0Vzd(e|oKUQEe9$0_7eeA;yDdh}*jj>s!RpW%qznzaNAe0kK<+?+bBBFc zcZf6kMX+=r2w+affIECe3F1!h#KbrFPd4XOeCF7CYBFx8!mpcmk~_iUc25|Ab_F8- zMru>=?fyx<0y~aZLCDQBb=^;PukO5SBQOf`=bHUrtMJR`+gIS7S7oxp?DlFXqQBS~ znZ~%s11${Vv=D>ULeD;00*#8|-v?>o3eW-*PYYpJe+rq2hk!TmAJi3pn?#_wAw7)kOTCDH z`#x|7$20gpp%>^z;-HaT4)S~LfELtoT1dxgA)Wi09zhFd4+GdBftDj>V&(`Th!iiO z?HBqqXV~ioa_4O6{aL(R8ztvIGu|0{uHWZVN4Rh+akWDFjGVm3D85ckTyA9lEO4#6Q>Bjh26w%k`rJP2BC82|3@}Kcji>x52RVX z;C!sO!(7=?senAQo5_g}K?DO(y;}i*`m>|9C8yJ_X4r`Iiy#}3bE;_v=?R@^C{##M zL-@1Fi0=X}a5QTGQ>xq{H*gc7a5Uv-Cbo|tC^hWInG!kHloHIc@B*M5LqJKw6(k9} z07=}TI6Hm~6aHUUgmTd36umcapL#~EeCC+&QjpO@Kk zuqpfzE(w}UQ&mpaL#wAGjZW^c&fgjkqLZr5rN_NpK>;#0#ZF^x)kl8`F#16$+X#S> z2@V)t!vdof;YWlL3$cfofySz~B?awwepqb>Xo2kpxS|=?weaBpXRTW)p!$f zrUVwngZ~qHfhGaPfN!B6u|6mUs&GNJ7#n2Q9?-%X1!|a7r<~}k#>^T*&=PD57%QMT z!wJsWTeNa%6D#`_6eZ>EOd-8I$mrNC+hv{K*B$rXl^_96_Sw%uO~VocE6U6svz2eI zWphEfPd+=JJ4`Qfc=e0qA53erYeENmK9dm9LM%JDvIzx}s z0wuHO;=MOlth(NDHc2h%F(~a5weEhJ9Dj#5L}t^K*=_iV<=n;$Vh%OcKYJq7N7>YW!VqRpT8wyL%`flr%=bOeOLPMg0 z2UByrdf&)H5hB0iS9VB&CNgsPJhD+)&`Hnb2L3J#BH}j6&dA}190^MAav5~s)phJj|(A;fIS~VILx=uKp9K{Cd zINmvk?4jhQ?_c#5bE$ygu7!P0SGqt<2!72{C=sM|!tm;}(k;wQ*?4oO-vjL{a90*5 zZX+aKN+90Z<316XQDJ}_;SlGjt3;Xw&N#n;@en77CqgA7hc`LW!X&>uyQU;)z0$&=PX6FpB;F)QqW4)T&K*RS zJvHI`(;blaATP3bA}otbLcE})y7q0j3){L@y@tx?6hB+H&atK`N_Lb$hBX&eup_lN z3fb$#bi6-*`IA&Ab?P+nrxqPp6g#(6VcZ5U?DtAR*m-AQccbYd<367 z`U)jQRUNNt9LYE)MzqxA^(kNjyKZnAL)35|a#OgjwP^QoeuCim_Tsi~O+Iwsr{1>f zJ9?RY#$OSE4u`_BN}3#Lu4KP2Zj=Tm5&W_Z4t1WWi{(@1DwmL>x*Btb8Es3-Z{g$YCFj@(7a|4tI{I(BWL9Gg9z) zT_e(YZiYu0U%nthfb0GGK#q)8dB<7jfbo}&I`DXIGkM~mcY-kj2z??m#AxlX_&5dQEKIxdL4E8-?3t`}@?n!_ zyZKc5JuRGjT((M_GX@&K8G+Ja$%>znIyRNPEF<)Ve&utUzHcm#x;mYsK1XJ1B4;^? zmjg;|Z^`lvuV3%UyB62IZ%%mI)OkxSS>>${%e-@Ixi7iY8ZsBgy(!fsx$0--RrJNd zs8yzkYsE42M`C=XU&D4aj4IznkPTo&K=l$=U4Ck17a{QM)aMS{`t!ejXF#^LDn{*$ z>j3kd2jp3xiVQZt`M{GRCSqg2-f>aF z!;4ELs=YC>hTsY*Pb)^0j?R6QIx**$(f6cwTEA<$e@6M;sk&}w7U50PZOmW60uQ4~ zP=;uHp@x##*>%Ik@;@*3Py0Y@VMrctq3buX{{cra0v7ONHqUl)KxI*BhYtH{C3`g- zU96L>-MZHA(Yr62I_IGoBTn>)wVm$}9kb>9kHU;jica_|JPqCG4jOz#ii6f2?mv`pzjn0%> zW}D}8t^}Q<_d4=Qj+E0>_YL6+f7{3qZJ0EYyWS~)l>*uZ;CZJ{DcIbE%A&=Ta#uc> z4!F&Hj1_8ZF!2A(G$9p*A8ztNn3Bd9cx!4IdR}AJU&+8zTA;1o2ZYT(QFSWL1qpeq`XjYQ8NdL$&P{a9~P=%5LiolI2 z9KOkZ;P={h1{x26y1+^|nWf;isM{5&;9W46xJPDnXZg9F^Y2)*FBuARuR^tjd3lca z+`!jee=`n1E|kiJR<&vqG%LrND#`-t!bo^KW4kbcr4?mJn_F7La~%=VgDFFEjUk3l z>(pNbZ3Mp|2n;Jr%xoLpZ57cVn4GK1RCFj zDbI`)jlcjEYuzp}{jeU|II+;`8V>JRlc`I)$TvMf$I156Tv_GH;{!>g+QM5 zCq5Q&p@Fsf2!96);S*;B>vSS9*?hSaX~Xa)bEd>)`03I6Qs;vyVD2G8Td+AiG(T3K zA@XQ8iNi3(eoW%-D>d2wwG5+<@(k;ZGZ|No_@HtjUrnB2h4u@{Qg_@mEZCCZ9 zB_cdM3fww4SNu3+CRV?<#btu-8g-cDz)5{_77yVtXLo%kpoWUR?V3Rn8rc?KE{Vf(zl>}$h}0p}$+OFVoG0$*&QykLR>BLM^p)DX14ZH1+= z#tJLqkim==ZN?D>vDdic`sbCd!Hye1MUYF{wTKj2e{uVe)%$Fi$7UBXr`MiMgKRFx znbPqQ#RTubL;_y%L0l)ExY`>p5bJg$JLPD@biW3@JP5gbL*H@U`cD@7U;dTKp?U-5 z!~3Jw02e}>03Gs1SFRe{*a_4ce8(}me<72LG|_YckGTE-u+04VwaOpJMy8my6JnMR z22dHu+uehTOwQfnZlf1>D6ChD8c@3|7m6b7^-}@UJt^t-SF;fRWvy`g zIu%aLNxPR`LB#=ojr{_Hn?LX3z7+~}rVm%Ck;_Dh-}C}!UlI(}lPhyIRl#BOUC6=w zG@&rPzMDPFAyZG6nUt+4Q6b(#lTmNx1Z)v!~;@d9pBUVAqgWzfP8E3wk2T==^hD%;#?eu zlI6S4SYIZ)XPid({rW=%mMh=i6wQS`_qFlZlH{p7Z~A(Y(i-Su36MM4B?1NBo&e-) zEkD_j<48npyC47XS7hNGEDe)X3$9tbB=Wy!K{+Sv3`4sbnD4X{2R9W96sPyC1j0}h zpr#DZF132hoG@gzXoIFReub}E7hWmfwZ&TJ|L{iHdpZp@M*;qlp59c-w zX5Jiio5nd-hAL@lvWCgEBmYp|c)|a}7pV87m-#4FUAyn$Mn34FOktmxe`?!wn)eVZ zgz7b0u@tK$Jju4zjk>BF*}SJG2hUT0kKLzUuFkVA{NCy2c4wpL`F;IJ6Z^`CFM&5J zh8{fFxF+r2@M*2wRkyNMb_}k@<#qR94A`gr&54x05?-@t4is{F;>KSwZiFxY*R7_q zsal|XQip4$|H7qncuxt{`JF!?jm2|s8%+s*O+-9&GV|v*|Jz+(9Pam2S0dYk=S6ER zSAags`Rb$^nzs?H;?WEbZCp+T0!1lFg`+?aN~FCqmeJbR zv6<`xS#0QVkr_M1nel>H*B?sDut0OHNV`|Wjk3Y!$`Q5-DH zflJY1lVS5(Ud7bvO@soTRh04u?;t}%G+6JR?s_3dYW&^;Ke7pnh5vFh zh^r*fCTI|0cunMRf*}&xh|2BnB9utHo|-gOW+_G%IDn{`FX&dE6MgsieuBh)U~Ge~ zeClmi-olDmrA*ZD!jIQUKp8?|lAR|(wTek|il|!IkAwx)W9nd!Z=b3C$rcAWEI&Q& zJbgKc>~cd%0nV@qABG9;9~&5o7Yx_2qRCdWnpMBrSOLH9!yP2|G z-OLhJQBUHR+zERiS=BRYa7y?zrND4?>y?c6GZVK2|K_QwXPZpRWgOXi5~m$HOG>sL zHN*g~x+BheOPa9}UP?PH?3R7+C z6_~&Sq$pRH)2u-<({gyMr*6v)5t_Z9Xkk*LKWSH;HUIA0b6YV#H_^IQ4qfsqCpIKz zk0aA9!)IE`9o-ktn%Kz7%cc-STu-{X#nXcI@eh(wurX*&V#DClu$iHE}A7f&f8J47Ww>!q)6RD?(StxyRu!} z)L~YN?Lc~caub`=I{B+;HtnXy=%`ipK$J!+OAfKWp`;!~7 zPOzST>7Y0@A!NvTD4UzQvczzHx&irVggO=0P8vw36$IU9GZw|I4EndKdm?f611_Pe zj%q3&?UZ!3ZDIRu8Oo^BlDWtKhs3P&6H?@6(Y3~qYbBD|DjSGnwX(2o&-jVVk)oy_ zi=07e-Yp5`m*16?eaUBzk|TYqdjz?YZTWlK7|R7_QA(SS>pP*X0F-z)FK`F5CDn&u z7U-ZT`C{qSzF0v29j}&+B?#&vHAMem!rsF*5cbFsznGS-A_yx-B>A`jbt!a&&YE%`GMZ zBEwTi<)3-sKaTidqjxyb2t-EGTRd)ThWhwen1DJ&BFYhU_DnMt3%LDx{clRXe_#JJ zaPpQ}=IOc|VmpsLUT&e~@xuXTZZgh4tD~I0{v>7GcMRqW5f$5P&4CqLX%}uc_fod$ zlMF#*XXob$2jTr2@;5q3dKlC=`mh>@7ObCZ+X%rtNC||wlfc@R|>R1?R_>>%qB z)|JPZi{TK+6*)LRBQ6@!SNtxj76Qf&JOd~f#aS@4BK&D>y5;8ZWRKPe;A>jfwf7RT z17d;HE6Z9p{fx5#L_DP!@hiS7SbgFwMhvFICT@wslKS6sXJB6lF`$aESFZ~3-;g^5 zx{}C`*r6~mFuo3zD}HCNmZ2Haun9}J=4v=NYAL$IVjVek6-?q-T`VY;L!BF zw{iL5J;)E_GJm|^uE#{^K)-gV;l{Q>3BDcqMoy6GAE+&AoIUY8hU!LRz5!GW_fiGN z0P<^&+-PsMbSyjF$=AQKC~*(y0Cj-{IgV~pl@;#ygC^K>6B%RrEHzGvrknkDD2u(0 z=Mg9$8=h$VTsB%0gjcPXf!N{HR31AJWOmznEd3OktCW~YyCJzq$a6kmrYM5h$sCwQ zde{DBuEH|E*J~SU11SH-<|D{-hVc%0zIBt4*p*7JrVKnb0mqgo8!c!?j5M0Eu<%a< z)x@P#%4ZTg5l8y9P8jP`St+)VOBe((3ah`Zk@MA zhvOcF(@>UC!psQ_^JnnuN4jT}mK>?iMJi`xh0j!c;9i2L8j<17mvlGVkxtUgBGN18 z@DsZ!FR{@nPE5}#h_2kh@`y)-FNm%Rk6-<_nx}y%$hmziF5?^ubqtsj6ZQr+Fg7@# zAZTpogG2XmL$vTodnJUeNB<7O1uaAXer7O}3Ef;2?p_*tyhBK5V93?`^Jy##io&DF;Vhw0BNK# z_MCEGVk*e35-rlkaL6A<;}^a$78)Tfvyb488G(Xf2g$Tf*_pKT5|e@z27x>B9Bw(5 zkJ???g2*gl^_~;b$zLXB+=B5cd`73rik>@u7^iad6_6*MU5;qLuGe~RiCCSIU+oMI z*q;u1j{h=v{5*1SH7ukl@kDw1$)vDQkLSu+n>r`UcIC%+%C!_@=HS`k1b>cn+7sB< z11}HK_2hx4?!-pTc$#*4_kfz{iHMD~7;o`6X5b+cNn9`{@b9NTMShvHr$;Jd>l2)X&8W>*bn)tT8ak2kn6O@~#VXT-SP6$UMdUW-6v$nremi_*-ga zuKhl;5~iXbe|!&PXa8q8SRK~<4wK1cye4A-W!Y!fBpiL1F>DwW&MkV1f#wW+{NQgU z4-m{ZX@Hiz9gOj<`m!>#uE&t`C-MW8JqIWWlY!0`h)|+{0T7T61y80 zY;Z1(vzULT6)MY%*>k7fOLiP^D3#kQfkp`Yoef9(oB%RjTed@)MWw0lhJ7yK5dT{}CG8tt67 zN_wTvenpfIfV((|lL-;-@9YBmndWjRiQC#t24de(l?O(3)=U!{s1WA|-5VYv)RIjptB#um_~$A*0Q#h_ zv@u{;FleWSu-qeP7zQE7SKkdv*o}^EfMggb+>oT?nY}<^y%~D<{E>J|6=j~SYxYKfyZw)1mP!f#=(sZ*J|H9#2&_I zzIPp(a5$Xc>Uy3_`3}u7m^tGI9F*3mtdLhaB1i{N#SZ;Vm*!lMa&LYXf)1leq~iFLA{hwKb|J=35tN19fOvlWmIlEo$w1fVqjDi!Qk`vbpqek5EmLz zQSe0+`E8$}U%XpQtr zd?i=5%S9jyI{@>l_rEg$Qv${#mG}NTu8-Rf+8YA!1HGNNXK;c0NWdh^=hVpUT!gw` zvBlN(ys>_c+dIDG+zO36=4m3Bex54Iwc1@!~Qb28jM zNQ;jx%Y2FnkvIm1;Vc(U4-xWPtDMpNs{h+BuKP_6Q zv{*tYDoV(nb+jM}h3reA#x7*vk|VMfMRuj4h>(4m7W+1e?2L?zEW;QK#+dK(mSOsA z=bWzJ@A_W9>(_sCos*gO`+4sBb-(V{ec#U$k-x7aK$)o!e>;bpZayaWL0ITI?ES~u z#D9L@&=?T?(MRl!D3}%tUg-L;QQOKEq-^$kK}h(fZhP#Iix5@N2es@wvPXB$_LQ_+ z&9J-KEOV{4&c8r`4e27YE5Q*TG1mGmyApEN_VUp_3sIgGUwPkwt%bUhT*gm|*xXv_26UnudkG=jz6RKME+g`0 zVTD78Bu{YzpJ~o6paUd%jw&tG?IS0_IY_6)VdV}CQS>rk-cOxJufOhalCA1HHU(=n z>KZ%Mnp3>!zXaLpv+_?mahD2@u3Bn}A&5whPJ)grdkU_G0+YH)*Kq5^aQ}1Q$eV6j zX@*bl*op6~Jj{8)(IxU_Uvhuw`a)arw2au@RER_#?b0tDSrk1e=yXB>#>~Gz>uN> znwD>E|GYl1xOwy_m~@j@{PJSF>1z_Dhr9oTCF8pLc^?Cc73}bWMr6Ks@y9db->$#8 ze~xbbwuFE1e9yr1J-QgLN_q9;d?226wRQa%u|0Sanpq2{Mr5cTbpO@M@wo(FZhIBS zTagROb7Q1w?R5eTW-yc@GtWCJMcU7JIn#+VP6>}({R^1*&rb}Z?UT;`8EruzKS1@> zEm~lwH!$TNt0CB%XCHQoJP9)oRv8)I{HR_QBE4E&pi75)$N$~ zS&nfO!u1dA4s@zq*$`k_T5m~dt^UJr4*)p!JwJ9UB@66tn^@l9x>dJ4fqdh(m8OeZ zbji|u<>9S|7s?NN@^o?6Jv^MPnyV72tn8wetGTy$Ot|7Z#cnp=Wwcm|Z}@Kw>3j{# z%u{DRQQ_==UeUF$`{@Jl=K#}&rt(^f1c5#T#x*%cDg6;1hf*$|7JT@6y=VdMAZBIU zaah&1o;JzEfGVefPUtb-BF#8M-IKKL^FRIUzoX67aR_Cl4MZsBxNRKptM6K?TkH%P zsZ+9I8JX82rr$2e9RJXY&;6J;L^XD^d7IYdcMVq=^KKyj_K&YWj{LEWv{liOmJSrK zUxd2GVbfQce|#Be=NCObQt^_7vR)uVJp(MyPxO_lUdTTFpCaX3o`LIcWCc^#|1Fil z{*|efGhE^=i0N)$GasN2Z~3u^VD!{N>rzK!Fcqu?Lc8+EE7h*Adef^O{Nop1@q&j4 z*2{yhgUH`rmlOtI9V&HiPf^~01gxWr)BVkA_ihRi!Z1`DHM9eRc`wiU z(+6i^oV>4?f6Ut9Da|^ffCrg__!;Kr z;`AKW>}L1KB`8qH=O2@nQQ=q*4M6*1q$%5>Eo4otDF1CJeaHnGoR7sa@vOdW3Jin- zA~(Iu>TBLI0l$ikk6Rxvl;>X|BB|XE^G|)cL;b4qo&ZOC@WQ{;KG&iGT-NiY?fUV7 z1a0^0tDNdy66Py-iG`%-6MD0b5dXL)&o8ZZ@wBx&Lv=wtg2fa{Zbp z@D!I6z(ZVVjFVl9PX7SOpKs%3Fu3rdilVuU&_l{}t-sOgcl1FS1o6dEhS<)!zOzz}k`o2+Leq=&$U`Ho%6TbW*cnFqz4+x+SB>!Il__@KMbRvV$md@(=J3 zf$%!?`XYhDD4&9dIDVrki4qp5|0lI2{|9^V(*SXv|48c~{@*3?0y_cW$fNB=&Ac^m^SyPhiSJ_zW>j@2#s zO*f!&`fs|i7B+shIYZ@NJp`+n06>U8EG(~TwHIjl^77S3ir`O&rq zL5H@T;Tn#+v;Ts@D-O*^(HeiISI?weagwce<9+IVp{d*Cj8;I3R@(mZuG609Uvs)^g?Q-BZh5`yJMl12@2MgMZ2nH9&TFX*!-{_vl|)m)mfQ&awG7}j zlpSWA>z1St=CDHpq&%U)HB|`*vLA+@l!NC$IY`+Ha%GhVpLYF}D|>B}+k*ead?-B) z^madta2>{7zBb2Z$GuFbODkfkAzxjMsuaI|D>ZVZ1&nQ{ENBrC3TQ1k5T8! zlgW;&*(EQOD_;-GbXuc;yVIfb8EHYTEH?aecGwaMwj^H2>X(!QaKZYSoY#9TF_eTz z^zLl}vN~ujJ*)x=(WNqK#qlAeI9}NQ<;4jFMd_b3w(0Hj`x}2%M@a%YO1mbNj?xeN zM$u8UfxFhT>S5N@$b*+s!2&S?9mTrwUz7tD8m}GVW^F&IO2^CQqbR)q)G>k@_5L-l zw_pU@V{qhiGbM2b9siTbVSCnOgkUIfcB4)ed7)JC(Mf>q!J#fcX|Y55ri32+Fa%< zu6TeVKk^y_9~t~fVq$p>yK~u z%Y|ZVd#w~z)q)igtACS^RlomDK7Nyrl@RlreEb1o+i&u*Uc~u-kbL|m9KTZT{7pEh z$!W@O!a-G9k^ilBP-QEAzts-vO5bm_gSs01TkW6@zyJSGJE-57I*3qV#)^sjRye5B z@ZW@k+E#uOj-P4(zX=Bwc>E?DREq&Fi2Pk%p^7-a2?y0;Z2SKN;kdsolX*{wLM5k4 zC76J}cjr6rdRIX(RFQo8QvCXZd)KMTwwt$!-UAiMquU?aQp#x5v6L!Tqo(F-U&{{_ ziB<0QJ)u-2`;LN&WX}lykK+_r)dm)V9y9Gpd-&{{pCZl97*p}J3vA?5grK^0hsE!md=$Qmp1~XpY}&Rrfa|R4?g%Af&$0zpfF`7Q9eZxNMd3eJ$^KBKRkcc{9Gsz zfmtxy&dLOI^`}D#1YXW^f^pgO%s$*_vxv%&2ss9F63($qXrk-!!snU#mWq(s2tVr` zVO3jphMykGaxr4u6G@xE{ZZLv8{{hha6#p5m^z{l#rMN_Ab1cM^up(ly! zSNMVTlLW6dyjkR@PV&F|W?+UF6d8kTk5R1B4-^QtuNDZ3X_6v>4mpuHn|j}XdPU^7 zIdCc*yDtJ?;5KyEdhn&R59(#>tZ}+YX2q8dGqfz>9;5h@#?1Ja3{AXS&>w!?n|m?c zOEN}A17F#)?q$&Tz#dr=oNB7yB)VisVbS#c5%FrW0!JedZ%2!s2{k7%8!>It))#Z` z%R10vL!7uk=d|zqK44#5&&03m=gzxQo)KcF4=;hL$aeqH^MChEsccT(66*3)f5y$^MUh z-^;~zi5{=Lc>SOs_~yvXR+=UB)Nf#M8kDa2_uo?bl@{7KMNqnavHejP#pf+b!2!qkJwng?S>j(pZWHiAfy5I(`za7tyR)O3KP~x^h5sFQ_1x-clL-5SoBBI+ z-SZbYKL>BtKL@PvfxJYmP<-*_)1MpBSRqV}cTB{+S(k2pm;GY;sdu*2@%8W~L)Fd4 zpt@Nx}6gXfZgXBB;aXokMmHvWB2|h0oBQY;y9d#G zZYCQXZQJS8a>GXJz9`;pshtfS?$jLG@j@-uwip-WEq2`Eac1rVVW>iO?^O&+_-K*H zda3=O{6U3xH7N|gbb5(s7O1F)v9U>04oG=;8T1#hq>la=qfc@1!*H9PziY`_3~dt2 z#?GL#%eWR3bYTtW9+4!E%&eZV@=J)$uR|hc{zh}5N#!12oNYt)fQ|s7VSBCkTyfmX z%cl(t49b=&Q*j7*GKYE}j);6`rPb2ue#5m1D#ISRYo#gfDldpWe|j&z_nuR6Sv z`TegAq4J}(y^`+(4)vNXKVS{yxjqonL|4D2d*6PRJ!r=kQPYp$BqYbpZ!X_eE4SA$ z^c?aN!ax7sm;3yg-g~t)vP^rAL&wOcJb2;w`=^`hxd!B7jzl)OhTpK%n!>VW*Yue+ zM-T-#_iv z>&WwE;j_)PbdAzYmT#pkLaMTrXxNl}Vq9JkZMNlOi2W?u|Pu-h23Nvb6Xjy?(`^IkSD6bCW9!gU|1-^4-<>d;}4DE$?f(}$*h+k^7fZrWv4 z03MWj?t|QqFmFaH!@1|IRj0n|3Vh#v_XYncqTf@{j@?^h>D#nWar6Z6AYJc_?3xnP zlG=&Fxsp%zbsL0GOLUD%=%@%-u&c2zzp>ZM6Ssj;lNwFeCAp5?x&i)Ol2Qe7gYzR@ z89G?FtbEE9nqG`{n3q|iLFyf}>GZrx#+K^?tY^)TAp#*A_+EGGzRzRZ=nN60JFmsB zv*VjJep&(sumoXyw-f)Vv#ofYm$Cd4UA?u=E6^)6nY`9HWg+MHV2@DNZCh6*$m3|Y z8S1EiYHw$X=)I|G=smw7Hs%5=gN=+SYJOU7Yn5rkt_$7*dgyOF9ARWx;*8-=xF9fznJ05qN89R9CG3h};S0>;QBv)H zY}`Bzbpr`$#tZu{y8YA$r78%T7r&n!`Ga~1{^py5tnqL=-_^hmZZ@xh^E}R)BdQn(F?~Z^z{vi`&f6vH~6~?8P#(W>&vInRh8k} zM4v*AnGPH?XuFj<^^1&{LXNq^=EpgU{|O=2pz_~8$nS(4^EPJfnnAx_@On;B?myz? z+XN`W=}f&G`?T_Q{@OFQY;A3YEXM;J@Rl|3I4MhwH~3ggRWO_Hk=FUSE#6Yr+Pxyf zOGUGdQ6f!3Zk=~?Y8KF@xc3UPqqT1{Pj8M>13p`F|B=T>&>JLGwo57HB-DxX&`Ogj z`pVJu9x5<&h6fmGlcd{q?4Kab>MOQ*fcGjeZcL<5=9Wb8UZt9-?vHEGto*f~FySZFqwo{D}lmUV$m ziimPu_)e4a3F`@GwchQg%?M(;*mRh?T^KE^h=7Yi$|oofICsvrUQtDjNFA!>*+Exc z`-ZN))LZyBZ4q4h49$8fpIX08@zw=^K53mYKa5ddZ3Ra^Xu2vuzE^Vcs=~#@{QI|Z z+r^cV_nS!1*FyNQN2La&0|(Ib-F!DHL}i^TcDk_dkxuf^KG9NWKiV0-#S3g=87{&S zw?y*27sE+5C=;LUax?4BOEy@EEB6?6?gcQJa77zgC+fO1Ihz(g8B<8ET2zYQ=ra%> zNt7@S!C*<6;Fv#lTz4L_62?)1aP0jSXv?rkTaRCxI2xTZts@@cqq1YGJMu@WaU7u_1!$v?q?vz-Awx53b=;De6PAv{k!TFR3?2$R@3C`VynCc-1epragH(ME2`~XBPD8`RBQ#z1vgl_;uDya99h^ngEuHwM4WAh>3$4TRA`qX8c-(^+elX+_?SgY+sRz+)092msp@;M3StN zr?%(89G@u(G#ZKadB8X9%{<2W(S?IU`P#I8UoOLG6I(pL*?e)#In;6pQci6z#y|}w zlvIKyuyY9J^bAOIo92<1uFY`1^}24D(Tgd*7I_Nhh_=6hns|2KCn@=GyR}`Ox7T;i ztxS>r*Nah!3*J$>c4ZwtIo~T#5dqP^nI<9DrMZfWYmgYOJ91Pm_uA#{dpQbA|rY!2EZC?x7ag z>BueZITXsf^Ek*$PH!?Z_7xANIL-z4TuGv9oQF&!ASuG!0%$v`KN zYms$_sLcJRl3kIwoh0c`x5=hh_kt_VcWP75)soJ$5I(?XWTjI1q>fd^|dF|q< ze}LnDOuGS@r@1p8wE9PWTlRw1fBE-sejH=cvUwv?sq(%QHZOW*--Ir?$9PQ784OO7 z0@LK88OVKv=`pj~yYbJ$o20J}tF)qBdC0s>>_xrf3o$^t$r-x;$jmQ*sb2aEyqbrm z(4LsAIoPivm}Qw1&Vfbr4s`mojp$nU*x#_zzF&CX-PEb|SWWf5LQ>YSh?nK$Ceu>1 z&jGrb34>NcLD+&tuat+@#bRP}+;A-6D6{4f z8x2C6;{A6;)cBH1ix%IIG%@iKF+H9GKjfSVudn1xWSX5z(gyYk*)fm0$qHG)k`s|f z>D_&M1LvxO7d>MnII3Nt{@38d?uUu$-0Bpy0`jC3OQ=2DcEsja)GUh)P)qi~+HeZ| zS{wwlcqhXSgCF5v^&q6{IDeFrZiT3n-8QXgaAUzxRF^{EWXV)!+gFgO>Ft2o+nmUq zdDoT`cJiR5#?7&jC|Z79RDW^g4VuyVDJQVnS~Km|=)&8;X^&=Elyo+Nb)OO`{XW+i9xKw-p$1Rb zmtk9RCF>}y9vkDh*YE-^EyOp8&jiD7XFN=l;o(Sbj$@=ZcKtR!%nq_%HGL`PMHE72 z`d#PT<0x*+mPkxm?4;}GX9IVm!h?CpH=UBWb=ZlIyy?TAYa8?Xcz)RP(#cBeKH{<; z@sjLoE0SkS+}kY-&|FD?rfvNhkp^>W*Rj|IC1rcoF5jde8}+~WuQW*?nq+AYE!CvV zpqHosTI5=1@Z+)$(d-rNDZU^%4N5Qj7@obo9(4w_PhCcsqr~S-cgy_e{;idBJV_ko z9)gdh3QD)W&CjnQ?eh^UP4SWSxeE~xn5zvGI7S`y=wf~G2xO>ojG>mrrzvwM<@cno z7^A&)N%-Z9RLX)oXOo!E)SI1V9mz%qR9;U9G$N0v(q_T1Z$6X{_shfMc+D-f-qgP^ zsI-c=XDLkZF&)Mq-6X-0V?Tb<>Q*D-YH}Cy(X1Tuv~qB+ddbj-B6M%^Ez77ol4ITu z5f5;)VAFwwi#!l>qZ~C^fm<9W;wx;ebTk63weDww^}ipdAx0`! zN$v}J3gKE1kjZX2#!?+~ykVp2L69SDzhfTqw-)^8-H~sAXld>f*PE*%seBa?DMGFQI?riggn+!4Ehg8oX`G#+ptnVhn@ z5r4DAkM!m~FQWa<7Lr}YWfpr!@c82MPe4)w@NkEpj_i0({0^i!SIlY|!Fb%1ObE(hGyW8odbfd;O zDd$M39qI5-i^UOnKwvZryqs#bOheR?y&ipL*r-YLX4sP)j6zq4sV~HB##mAZk%-0X zI`$JH_(`u}_nc2tlL`?WUm8S@(2Qrp=2}V^-;k#XyFw?zVPazKb=OM3-OZjtC(zzW zQM(1@7H@{aEV9qX3AGV1Y&GOl;(g@%Ba$eumnsry5>~CJ_;lVp#xA@`-{ zSIj@|MuKJ!iNbRV%t&2kSkc!|5YG ztoR3j_3Jj%eVa@Jc1qln3z<#svlxJsrH(W?HhR+BXc75U!yqUlyvRx%>X~w?0jXBq zm>LlV#akfJ_*Ogl7E3!d0cdYKHxzosuiV3gTWe?z!I_TqJ{4S zxlKO??LV+b#X`HK_x)SXjYk+BmqOuYC+mQcud9)vd#&8dQWe^~NeLg*6EK_9wwyl6 zgU5Pdt6GXqpCYPiu_hcQ$Jf3vERaX{*mWjKl;6#KFyG{9z+h=0y)8$% z_3l`N^a+mAg~FF+Q8HbSgx^`{8evQ~W9RC%H#LUC`_$1SuUi*==l$9wDe8vnDeQ8F zV8mFskSqYNM$hxoAD41};siBecSPvx_H6gv|)Xbt40q$Y%{j{248(CUKeY>gb6uMT^ zn}P_dtYCe6`aAESkyi*;JyWZT9oqpw=Uj=;9sAtaNR1j6x`2J`WMgHc)J3Sz8>9%@J%^Bo{w8LB)c3#ox!ggw!=56xny!L*vm~(lgoL~x6 zsKNKvHEGx-3i^x|m4(qlvZ;$KF(&8p(fAixF!akB^sBRT_jHoIrJToO=Hw0*z;H7o zUp(KxGD|KeMBv6Y&0Vx@yU!p%o*Qv8>$CSxv%&{ThN!Mmm^?Z)9ikq{6{=mAF(051 zn#?wbMf3+{M4UR9w16gSCvqeEBi&F^i_hj8zL~vhE}qh8sO&e(3VuIkd&$`l&dPY0 zs2cp#9wg8YDq)tH_lM@_=+(lZB8q+4Nq$8`FZUDW-K@!ar8WdfNyE>)XbITj=@)l5 zLMjAmp^heJhPnM>6WRQoCqO<_vb*Eb?5hTi8Xlo8Qi4#5WgOCBd(4*{I)y>lc{WwShu!y|KP)U zP;+{;bt5)#txvk7^Ho)$lTJ~%bIu+=U$nC!b) z$KycMORF+sPI^?*@oe|hkv8&7SF^%*Te#~jQg~$_kQQk$Yl2xqUmD-%?G3S0cSDZT*pP)$dJ&d+D zkQ{T}eOd1M0@x=yuussy>X}TiPw;peYI@n?2JMsJQI}uR#2#~}T*@?DOB}$EHvtKa zyvyV1G7s!gSFGIF_L1#^k(NsNpQ*|xMo#6)gbWDVy^Cx)c=cvT*{m(=fP%FE%GYgl z{tMZx=X~738NFLMyJ{+Ckt{-ATS#W3-UO}0Y<&?wEBYJ!CFLKLQ{fVu9~0AnN(Oe$ zcaR`X#z*M|e%2+Ef0RRP7 zvS2o{d9%L4fcl;schA5tz`Hb4Yyz4}FNj(n8aokCfM*a!4Ug~(zqaas;J!8mUEi$=aZW@_R5J~y>Si@3I$%VL1Q z#|$}X)(!ePIg~egwrI44G}xu=dZlm9N9SVb6^WjwABFUscw(=%l6-gfuzOx;lpt?R ze0sdcJIN7gXwQvNUDBD`yqNnWUsD(r})u+`b1l_N?0O++kI^B~W9q^P5zx zBzMRwC6t~%{e|2NZssN^@mo7!&C|A;5CZq-emx3yKx!g|QYUG3?Pu~W#YqTFWc#Wa zNdmCT3~2NeY!jSgg9$}*7F-@JaiVuaFT8a4c8DAOyoTfDqG9xhCf5W)6r>SqcA)Tg z9CDf^T$R~%hHhu>{KG{W@RVf%&mm(e>(evrYOJ!Q@8y+XdW2Y+e(W5f|EK30NbhusVG zep=)(a16j3oT;hOT8#e(AN(Xi^|5~Kl3Z}bn;SV3IwV09A)l{E6R%LYsmb>o2s;N; z-@OgeJ%sMeI?!(Hf9DlBQ56)qcPJGUr}aAN80L0_$8OT#4;n*N-{=e8Zs-iLyJaqgsy)3(#;+-yDZi~!j8*f zs_VL^4mTJzc?jair2!T-11vgA><}LFRv~)ovMcwMjPqAmM99b_h zLx$c64&FF|K5>T1(*3IU0vyuCyM z#n3+%W$llAe)@faGq?slcpJk>4A7`6AE7u|KzG>*=YrP}Qilp;W)MIK9J~!RFk)tP z(8VwmyLzvSri|+(=Sufu#TO9eOH1B3UdF={)ADFKtg8^{vgFDMc%t!`s2B{f=xf7P znk;g3@kEJksnc~^j!WlG&kOfqG8baB@58Kb=P|ugAC{hqGccyK*;gp@C5stbW*#Bah+r?w)6zykeKAo^biQ!Z{!~v zi*#W6sm5;x&YH2wbnD#tZwt1za{v9qRXYgJH&OodCp-t;j)IEkJgUAAOWfSJ@wLE8 zNnz8Oq%AZ~1!AKdt}QjI`+(IrpZYx1&XBPvaOw6kneq~bdh?Xdl_$DrxHdR z9#V%Ee0|~3oafsloK+R^{*j2V?;emXRs+!WF46m`nRq7zx?j(jo}~tbHS+!!AN-)x zf+E+xk2((raD%UD*}kpq_(^^)?R4kJ14!2wzzV+uoZMi9e3B=(bkI(2;!h}%)reuw zeP1oKw4{;>dgwH3hS{e;NwLbg=D^;FKF5#aN>Jd_?wYX%HEa8tdXr_V83V_cBcHfU zUM6)Uc^(DD(aI80-p5YHVYJhV6?k+*d-_#UVqi^pTB58pL7J%ff|z;Ilc;y;p$fD} zSHfrkjTu&V)lz-bEPmi6oC99xY-REB&9g&i2z^VL(iL5~d;SpY?;;fN>ucBd;qO(YrD?;Jq3e@?mDEX>L{^hY+K1#Dy( zio@6K#dSidscB|O7gKEYFw0Nex-?=ZN5QpB(yVWK-9K?5W>%c?qWy~V~N`*E>J6nE-g1Y zkg5N|_p2Xq@N>CqI6can$2pFlo|N4=J!O`P@xnh3Z<0y@IpWc<@{_=5=i8K+K)q+b z`**RN-j-RZ1Mvy)4^N0yjWI9MnssJwm)EETi4`s@ezE+4m^=$7ah2SlkBR4*@D;w^ zq{W*&TYH_q>N)hNtA8JAt&)w_{zJO1W~G7U{Zr7S^-T_Ld*HtIfGp!OdDr|riGZ#q z%N@lgPi3?z)wiS{93u&Q&`G`1&Bj`eJ0v3`-C_WeOx0?aaz!o9S2ai;w8r62 z2-Owe%KAEMr*$csxb&T8B}g$TpAEKx>BYsEZ|^L%F9X%C5)&Ee?^&-tw)$*kawbc4 zp95B0j#9}gattk+?-05fmY;XBKzcYF@3Xfs+1XrRENQ!F5LOy z?W_9dGF!R=Gh{j#chU}@0FjA#b7uOnp6+f_Xo6w5H5@7WjVbT|y8)+z*Gk!X-pW5R zpDcrA#+7O=^R*iG9Pl=^aj1=WsuVHTFegJ%kwtZc)1kUbtzvwol}VqKi`gB}?7G*R z*>0fnTAj5Al94iu09K`q zz30BS9b3T^AM9h@g@*XFa8O_nq7Q(^~A0@8A} zT_AIBIUmPy7!Ju2Z8*J< zA*W@pN@XNp=7A7?Y2Ii<%SVvdoYU#pP;-aG6y8r-^tQuh27GfpWqIo`aV#r2vshCr z_0CYj^&%u7W8$glN65oNRU{n~4oH2mihK6@%a?%GkObHmDeK#=a_S_#+XVns42S1o zfgaaFs6zeNQadNsJ8YS5>Q6s!O8lrV&k30AL6-rV6 z6(u@f4F`_n_`e-Tam8_BxAwTd;Mhs~YS~pBG*AbdZ}_QrjTAM|7$Nt-mt>+%$RV-S zg%Tr)9VTH=bth@*V^i!fr3mNTsXGvcNvnPo7#jL)gpL?&7f}xf>PO~cFOUvHWCe>(MC)FAUSH&j za+A%Hx`l9a`D}D0S+ybFkPTZokXhUs%QeDoo3;Z8`#sbQe^zbMVYx`xYwEt*IbJUH zs3;>JkJC19iT&(Ztv@!cu1ST^FFET&PpVdXkl^_~fVplWKEKFE*ufQ$wK#ZUajJs& z@!AsPkbMGBBDIXDdj2rY*XY~S!R`g;cR!p}k>;bGnn}3^!QMq)8_L|(-$A>ze={Ws zc+-*p$(w3d;?6hk^hDgO&83s@1s9YPIPvdb9E}=oFqIx)$y{)(pej;y3^0%|`=q5P zU1vOhs#1j4^;?l*N0Z#lp}bD`^%u|&nM0H`Gi|ahn7rxLerJjThK>^pbv^d?8VH<8 zB^|k9$KA$~MNC1guW2Jh%&DB<;6mcnkLx+lU9?U44)!(sGj!<`Fd5^;wTjscI%a;|T!dfKonSRvZ@=puiy z){KFvJd{E}jVZ72H<}un8BS*x+{{B^hHRAmLx%{JXFJr-hxI`f$9T?LZjd(KmV@8* z*pZOvP<7h#In%g$Ts(+o)4Cq{Vp4{wwTc@j_%()>>L$N#hxJMryWMoWk_XyvkIt1{ zHOwr+={R=X;QtJRYlP7XgbYnuDZzlEc|Ev{zBsaHKwuA`#~gSy{17;*mkEE zf)-kj5QCq(TmEi7ao-4N2;79NV9jVDa)5;fFJx}*KRx8KGT%@hB&n0XCougZoG#aHQ;O$DB)#$?h{jLd4 zH_F7FFLY1UBeD?yNmyPwl|pF z3Y~zxQZ+2a7-wzM1 z#x)bXlTrBQZ;1{2k@)b=uj$B2C-THXs$8qRh~LC6*irF*^d|4@#EFFumVq^bX}+&9 zA}@iDHCL+35nmR~G$c>35JwGcIqfBfLvUrx%bv)buG^U`@5JCq;=kuhfBj!B@wQVg3IX?VXuc!m zsX=u>1keU1kWaR4W7yLv#`S{P{c2KA)Wmllj$=r>t3O>1a zX)^4~y)#KK3TuQj%7vK{-=1qkyxVwIQ)amnHu#k|T*(hBzM5V;U5-f9Gar4jWm{>& zNm=8Penm4m33S#m;d>go+6fuVd0_E7X=mvfU|s+{;d4w$trPc5<_y=b55_bb|C3!VqrrbfU(0UqY8yM*k5&m2fjeBzpX%Omscr(xxC6iJ*| zdq-f}2U+p`?kUWazehzp-V{s@mR|NFe2vXE=^|H*Er5#d1yHH0bq9y0&OD3#sZp~D zIyAMhtdsFq!t=p>U?U~h0{4I1$RZIgZQg15bD~d9zwhO@*1B}k-T#0OA#@D=wj9Ms z-hx;w~(0zdKvv;0q>oiA_O#K3qq^&~9UPE7t7H7UY)G>!o zWSsXEzYGf~$N;2uZizhK0+a2#bT&bHHWk;C zOe(@8uuT-W+BkK7;vOD-$&-Ab9(lw>Dhngi)j**Yv%l+N`|s+iRWpv4x1`O-w+PZY^zl<6ip)1cR5b2mT8 zXG`&L!sg&@10}-0r)Qj^I3kIcX?Q7TX+mdAVcsGQg5?OC$3;!^CfK9R*=)4 zT%m=$p8ElvHbLw!A{2^vhBs}=Ecw=+^NBbYR;Ss1FCZDw*&;&rnEB*3fGr&y=1gYp zNOG|HI!`?I*2wwVx8uX)rYcNL>{e`->&Lrw)>-@5uqCd*ZEoO}j>K>r-%oDar(TJ| zmh;?L8X2Ge@{(uvI&tZ_F#`o6Qio^M3wsZsy6g)dI}u-df0OO$sh%XtXO`iZaz+MK zc1%pv)sPvT?8ruj_(c0^XLw?}PJ*EO>|zd{Sv|ap2jSM}ytOYq<@F@KESmS7Fb~TgRbV_xfiMmOj;S1>8XX`k1TW;7_AskSb7d_J-}5$jD0CCR zcEsyla$HQhBgLs|gtjR4YDS*q-OLxI%R-M3lhFJ%KF8Yo7Rt}kOw}y1+)BM;V{hw~ z92np4hSfV;HA7w);r7O)hx#GC`w>;~5euf73!Kukjm`zejFYG`2Q48oHvz>A_q8F< zH>nHo`rGt6?Ra(hJ#2Yy;1YKbOI`TRwS&PD?u#`t#R%D@u3BhFH57WA5aKd9JrdQ>(3LN zoBZa$9SJQ-hF!<}?yWee=f##GXa}+`zdxt;{&rGRa6B2Cn_Mj!$i5#y^_4M!+@mzo z?5<}{l-FAYw3iPzY6iwJ?ihvp6UJG3diOB^;q}kSlhT(=H#?I4Vg2{-VPcPRSR(5Nz8=IKLeV?a#g8Kjw z9iAZ|ICnMTQ;1-pS7uq)*kDImM4!aaw|Im=|MEiApu0-h%{CS&Jbqic!X%Q+d;;$? zhEWS;>_8Pw%Cvtu|K$9p02{ojrPfL2T-8bZ)DU@W(l>&W-0=X{pjpvPD1Kb~F3bhR zQ?NMFR>7*#Xp~k@5Hv`AwUDRCB$4E)y)@85Uk&5*Hs`WIx`|tB+!wPi_N<=8jZJ!) zHM(#8l76tK4MeHEuv6pj9BRr}!{v?iLm$gvR{&jZpOVue{J*^ir`Ze-QhEXYaz7yR z=f0v~ujzlM7-y|@@p^=4g5bbalXxJ`wh_ zIqKCJE?!E8Cnu(!-1x(cv?KTO`($5Nm4pJMCzrLGa8k0ftB?Is4G&p_$?#bP_&vFD zdfzY+x%rCuzUwieu>~>q1=q)ysx3MB?3ps^}$+KlDrWD=V8%CERV zeHrH8jujWrfn@UDi2K^X5Gm=miSWxp=V;M(C6j{ zPs1Bu#0HyMz#URT?Mt}wA8nO=J=+k|yV zp!Q?bNEYS_^9@USP$7m01{&+*Z6&czH&N_}5GR=X&0d!UWPPuGX4!Y^K-vd_3%N1O zafSB)u>H|JEf?`8X+~4M>NSk4oZvfi?YTr_Eo+o{)ohv#arm*X9}Vrdl5j%yC_&m< zdn%(8_~lvKlQJm-aS=HmOA)a7t)OC#xL+4|B40EZ3bQS~Q(4!-TIl?ipMPKOTV2`G9&o!b5V zy}AVUYCGjXC5R^62YYo$z<>KKXj-h9bHg9^=pf!l7OWPR;C&o_6zrAM8$Cb#RyZ=4 z1tuk(MSdUaFli4D=PSuPi0`&{_sk>>Cz^eBY=|g5Nje___bi*pRT(VfhKXa(3}^fF zl$vFLaaoLD@l4C!u7sGRDH8`ATu$spS%!VS_xOy7Nw|XXNy|!yE6l9f9?Qk2Nf54? zB+GfHJ5-@Ky{;O{kN%-O>s6_({)8=H${5bxk(L zAw+_~lw8D(Whep5P=_7KIlia(I7ztqR(=Ua*=Zs7Cg;1X z;pRJyhvV%lMhbPwJqWkyOJ@E#K8uOgG67_WKRA39QmV|ZaCZFq*kt-^Dj`qVVrw~BiM_D%Tx-%8m+kKBZx>t)=N92%6&BmeyJ>5a+v{9 zNCzl$13fJvo;R9<0++q)dqwco@Es;DvtnjB&rKK_vgD3L8<@S;oqJ?m_3V0d$ii?k zLL)ZwCY~|P>qyq}TnACWyka!kpr+ar%gmQmh|8OcVN8U5leNE>nKEku(-R_6@>}j* zFH2h7zToNUN8fv5!^fJX__JiuZEjcRGS4g+-ugV4)6G^8wzI!f=R|2Y>+|G5NI2>6 zA@rQx$yl9Nn2*YJvRSI2znJcRrWG9D6947}0-4I5BBCO9iluS7 z42n=kOy;JyOI&=pNXqb7cE{!B=eiOB@{f&?+&}SYN6eyTD|F#CMy5{`n48i@!S<%G zF`Bda44RNb;{|0T1QtHz1jO}T?DDDU#7QnMr(!2XXgOna0a(XbMbu0+s9N(8w;mt^d)ETna+WY1+nX)guqIXxl1RD1rn9Y zOhY6pP4zYfEIgz+9=tq+CI=&ymgkDluS_b9K#~eb3Gxy;*gamV%x%Ki4EN1sZ&X?) zdl%UlGEB85&IVVG5hE7*{o$z5om;!@jhb_78xi?rt)Q#ng%bKlC zt7awcUDq+;PRbiNLlU@VxM_2e&*FEtAweh8_MIIh`z&~K5OK1M#G){VNqQesW>sFh zJgJbatD59FB*6E7*!%NvDEB{b7|%qu7P1y0d-k%0Fd|vP$ey(n%989mqf-=G(1I|f zkVKa3%XG9!m_lT)gsf%XJ@0#%ncL@_Q%=9%_j#V{xz2z0#mwBV_iNwZ1 zSXHn%1h}`CnYWsmT1o@FZWCXXF%@0^9uP9?flWNb>Z6AN_~(I9(D^aWL zothEa#lv;`6FJppL-1zwm^mqV1u~Eznci4Hj+&7WHTRLRWdQn}dtZ>JuN3 zNJqzogLVe#RR0l3B8&_y$BgqtXCg62a9#xFFzswI-|5)AdIKSS;TZn3AS6 z7{N_%T(6EM>zC)e%YyhTeMx>F2(+uF*l^@qUA1n->^Aw!%8(rkm<@kEJeU(gr9?CmuHb>}x1 zll0r*Dh+u9Q0P&kv@G$n|Kh>s1kf$IZ|fU~BKHDZULOIj7{m+$DtIiChbJRTpG?MtkYgb%A0TaH>5Lf}91Uid}P^ zr6$OvTuTF2o$~(Ah4!c;AfVa%n|H5yLO0RD@8xS(P6Y5@hHes7t1(58ZvqMJXBbJF zK#~U80%qm7^dehhw{MXf0=+c213~fypRSQ8< zLyQ~ZKtlp$4F^^PWx>Y`jN)Ax~lE<2)^~ce4fN{B_ zLb*^0;NVa7&B0FU?T`mU_KuMR%27y~>oZzo_i(uGCGr{gSxi{SRh7eQz$1O@=hy)b z?S&I~MY76N_XWF`;!YU9P{YUrZIY2E4emVq3XKF-z^~5bg)S0K0@o_X?0*+Z;CSeO z$i2YvfAPDQOhen$AxEFU;I7Lao2cjnq4r>UQCn&{)_j1T^K~JU1my$Pw?hHafxTc) z@I+)QGn_3l0EJ8+WEGP>OQHZA5MS^3X-`=QUaO!T$*R+Rvzj z+X)~sj}c-cSYF=UfN|A!Qm9~Lz`@U?6aXE@2YE1vdwWQ~37k>DxJFO+7#!$6LF6-t zakl}+y|M;8(zkw&9pKP{qynrbNJAea6`-gQ=?S!np}nIW*oqIaO#u9oWew>1=>t(` z4EWF8qaX&m5V=o)C$wshMq4RAf5v_sf&3~ zpjQHof;a1h%uwfR1Gv1t*0_*36z_}cf|vQJP#fmt^mgE?fFv|cHH%YQw0J= zPk!Q82Y|1T3B`@A(4z5qcR>Wg60(WfADM-T>1`JaLdD! zN7>=a5>T)t7ttEq zNe`!x2UN+Rj1eR3CL+rJIvz060FnZJBN>9_)i?+k_mUJ%C_X4{RCnGDvSGko!jP*O zkU#;>C}7-2ARFd`eX%BD8e>1g&}{<+OK!|h_muO%1-Bmq6w6kA%h&Kf!oEg=c?~x3;36GC#Okb* z;Y7$e0+yV&-+r2r93PaXN3{YS77OK7sKZl9zX{kIFs?CR+#ewkgzO^zEQ)|}<+*8;CZ;_&D(#z+s)Phl0NT z5kaWz61U?0ayc+5@H9~#Bw9&t#K-|APKcpZ_^m9^X0Q|ELmctv z{-%-C0~$6IJ7u0pVh9>22W0zD$E>@Zm7utxpFkS|&Pk2dJV%tzfAO#fuucNx_OB8E z#Mbln_#AIMqyBpl2KnbDCiGV zfQjP`KN9kffF-BvdEX0CA)o*ig}=K)Pz7LRJDm;*6yTTw#?1tb`xwf%w^f@gT7mh_gU%-)RCUz+oLphJrqv zC!j(}5aPKW#uC7lFKq2a+7*NM%iYdA-Q%d>2@va z2%-8~S^BS7^tU2JhWM{ov{u;y*Y3Y!5y^jpM*#m7i~gc+SA^>SGsU8W$txo4^p0xJ zg}F~W2=~!c6Jep(KJKwLjXWg}sS8>NG65=Ff`hsXbTb{(Lul#!>C< zrSOmR=Sy!9njGyYG>v786KY3#C=Vx&9yYChT>q(clB9w-o+mKhQ4-)wkBy z88@h$k{uO3%za`o-1Op}h0o}#Eo4qLiSEA#nxH6ur@4~r|JNp{ zLAiH>>dzIpKR>45Tb#uAn3TuHTt|2xem>3pS%LnX4v%-sb)8w(A&q+%H2;~p|A^WA zqo5RQoLZYosH!}MdT*QuobF(epv1~bReA_?NTq>t#FszDS&4=o-3S!{B*AS7Ps0KW z!+osPoD7He*}*E;T4-za=gmAP{Di%`#&qUcoqQ3_q#^>5Gk*D?aSQ->zd?BaswZKXPStIf!mp}5r=4e2 z_ zk0?bicYmK~_|XHlaeP^x@jo#2UG+!5_2|{G-gdTabxBOY<%4y%-CvD`qp6B^2Nq=sr$o{)c7otLNE_L6vUy|joAte{86ljpAG3JCX<4%Jbn{Jx*cXAr2 z)$jJ(ji6r@1!`<$pUsJ#2O-E9h?DX_IZ2MvvoL%=H`s6^-;w)B@6s*jC#N%?jBLtT zXwAOz;-*zr9oj$%|bkmK@V>@9a$~U);@o<#PD9FTO33 zi=e(dR9`JfAk44G2tO5;3)J9u{Q>S)T2(q7Kr)70Mh@xP8~|D{@^GyrIFA-Mj|$bps{zdqugeOisq%kp0MQ>|f=T0}X55|9Vq=EFE_5?lHda-9u*Q zz%H8?XT_HE+ovM5BSsd}=+Bv|Sr|;HeDS9R$8bIg=kLGj#DDmU4GNU_wrX{jto|SE z0`$6<39`Gi6GxzQfyhNpeqe7=8Ur{yo zd8T1ry<{vO%rj-W#J{ksD4-$i6<;l(7l4Q7b9hjiB{a;PnCNxsY1f2IE%n?X$twhI z;@X4GJpF94FF=!F3bCexP{ViM) z8_)ur!3UHo-qRjFHv1#o)M0~phOK5WrI?=YGxwowTC$y$ z#9N7Uw|(FF{&;jYnZ0oF^ZTGLi>K6vSm4{1_9IiwAHA*v92KF|I|4qKMe`&{qCY8jlgOIr{8IPVppHq`7G|FzlA zsB9|a>Wd6>mk!7DU|kaPV3(<((d8d`;=5S#T#B+dV;HzkEC+4cDG7CL=>ASSI1*~x zB$xVS+h);T?$pi6Udx=89AKw~@$92GQmKCf+L-Dsw1O%A96ku2A52JsJu`JTxD&O54A0>t5jHwvr8KolY9-F!ii?Tw`4u31j&D zgWWf^#V(&6St__bHP;aC6DS-iSOLkH%T2``Ewupe`}{uRx`i*4X}k5tm_by5Itca-=aUA`}1mVV#tlQCBPT zp2=5WU;Nn8t>}NVGJXfaK!8&^T)-NoB{4!V(b31+Wgk;*@Wza)~NF%OlC9y0)lm7ExfZMqqBC+qFKMWW! zMsX^_d0VpaU$jLZXQ1iA=N<<2)FkpMn30&RFNU$`rVDcPR}E znV0SMnty5bHQNs6@sS_fEfLt9`1ymjILGPi(M#cHpK^zPPMGlSUaI{N6;Au=%+V*k z!9$E^=i4v+xI@7*6l}}`BQEPLEV#syqe~HNll;-XcezdXL#9nac#)k&gva;YQ8e!u?0gojgBqMP6F+R|VoZ<0N1dWE8iM}(o04}B=XTB0194WQ* z#JX!|glPE@^h0`eY-u6w_EdZ(^J2Ji1aQ&!JeNDTd1-6ALQcft=k+`%-Z-ds z`Lv{+Xei&>uNXrQQ!$3zuCte{FVm(5p2#e=S9{(|{{zX#rr`h2uHUP>Y?c)#5mvczT6ctkKBv~9%4`2zUd}WBsUgLPH3t!L1*`O4Y}>Z{ z-PF;7mHup|2LttyEPb#=gpqjN($C=jAM#zx3_*}pryAR=A`7h!#rXR~We#C-pcr39 zT1GttD~J#{^qiw=`6Ivo5K9|}(iS)u>|)rz++X#XMIc$?XrsVkwKqoni@SV=@`8Z1 zm2IWdp^=}8I9Nm^MifhbikZsPr^4Vb8+UYXk``j+s(1hssr3?o450`p;HpMM>ECtQ zGqi|u>+ax~fHc!av7&p9%%4fkmN;Luc=@?6MfSas*V(4q7L7M_d0plDFq`=NLGa`>`~y+_k1xCCV&>hVvM)j$6W z_@fB26@PRENmw5c&%R3O-GS%;!>^dqViq}c0nN$ELKn`{<6#zZl2Ght{w4P;2`F+J zngjd&joC{-4WuYOJ*w8O@A^h&%d~g*^}MIXY72U5k)|o`1bI&wbIK-?gIu1^73{Kt zR$c|=-_9GwcyfQfsC#!}N2WSO;K3a~T|bLmwjH&?3}p_K{s^W&sv^3{Ok=mEzL>fO z!#xWjZ=TB`CmbH+Gpk(UHp0@B1xFc%lfCG7eC~GrgoHzE$^Crkd)Mxy z!=)2SbBNnWW#gFjwv840?^NH3ldCTtuFo?zF;STRIA5sjuU8l6&M~!eeL6vt5UEe) zfy9!L?Lx}(u5viWhHO-s)+)e2K!S!$g(3y%4*b_$EJdQ#SI$4W?fY_J=v?F8UE;cA zBver4AGY~;kHRcqpgL+(sbaP6@WXqyDKqRvdl#Q-PMw)ZN$hep?ewVo^NQ$gG>l~( zbh{%2-o6pFF7`-&+06H`v^eR_8NPH(=A;a4?h*E$zd{fH3#O2qoepO{_}~~JcFCso zS^hXEoDSwGndl60R%cV_xo|ttdc1PyTul^)Bzexx|M~etB!&gX5hYgLl4q0RZZwNH z8}^xL>JH3NM?OX-Z&YqL>k{3tGsi{o)Uhk(Wm0Tc*kolqy-8s-h}yBXgCD+@#Ywu2 zUjC{Y<>?nK_mt(m?M5o0{u~MevHwva|IFDWQAWjvY(!i><`P4YJ`W4*eEk)1G=gd( zHUtxzbWH5UcCPRDv5%rHnlx;uCWZF}{mF}hp=_cU+baEN-`V;-szeEY?4rX<)l65^0$`M>qzZ18?_YTA&I@gOZaoMXG57dY5S07)4(?PwO}A-*@um zIRS5j#51t2ZOkV0gTC7O-~cC=lq3^@J-f@__F;pI=B)L`2GrS3^zInXd!_%X@d>Rb zEO`gIa%-4HUw<|TTd3<})3h?^n<@WLhzYea-#ZBleEtb+l9a5TnvZ%v7v_GGj0u6m z`Y6hlhZc6GhaknN0hu2(9R<@#dMq<)(F~L^PC3I{<$S;C+=u;6zdL$X(yMyW`>~T$ zOyx!TVl(R=mDWZtEI1Z`DKNx z4=u`#U$56fJ$qf-PVB8+_b~u}fT5I)h*mylb^_JWTG)BLdC;+Zor+3`_0r7(aw?A) ztc#`#7H#4Q+A$rxxBL8SM^ZgcuUSFZJ?DH1z zeb;?qXWJx@5Fq{x+UKvRXASMB6((8@u$*CH}U7e%LH*L$SQj#1i5Esb`_TYgr zG>#r!U*>CPY?YV)t_4y^JcphJqmnmnyIrq4c3piT>}}kwa=Y?Ji^8^Q1iS&MJ|^Va z@u%L`+~Owl<%`YpF>f8eg($YhVL~}9_n(3(h;`HF5EWpv@KZ9H1U74?0*Xvp`z{?q zaW_{wM&PFAb&xUJ?dlPIAu7o0EsJ=Km;VSQF*^ikqr^}&K6lpsS;E#GXN>crEgJ0& zIL^o`QR>R*=E_(1p1;no)5Z5n)P>bf|MirF4(T+De6F2ucbIvO>qPNSBBL*9a(v6) zaWz0Dlt$g9_jhJeL%>oMpqIr?e<5Pp5YZXj3O4UXsv%L&H1hNLkQg=!V@2N$wVaJI zzAwq-=JIZN*_NMyb)nmAMzEAmK55UHc%!apNcll;r)qP#*Q0uL%tuY7_YR*rbH7Iq z6)mI73sMJ&S_X9eQK8$ik44wl$_EH0PYcAE$R}Y!-+PD5{tj6>M7l#lT ICA8|# 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