Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Human Readable Condition Name #2895

Merged
merged 16 commits into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion containers/ecr-viewer/seed-scripts/create-seed-data.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ def _process_eicrs(subfolder, folder, folder_path, payload):
if response.status_code != 200:
failed.append(folder_path)
print(f"Failed to convert {folder_path}.")
print(response.text)
continue

responses_json = response.json()["processed_values"]["responses"]
Expand All @@ -124,7 +125,7 @@ def _process_eicrs(subfolder, folder, folder_path, payload):
print(f"Converted {folder_path} successfully.")

print(
f"Conversion complete: {n} records attempted with the following failues: {failed}"
f"Conversion complete: {n} records attempted and {len(failed)} failed : {failed}"
)


Expand Down
1 change: 0 additions & 1 deletion containers/ecr-viewer/src/app/api/fhirPath.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ compositionEncounterRef: "Bundle.entry.resource.where(resourceType = 'Compositio
encounterIndividualRef: "Encounter.participant.where(type.coding.code = 'ATND').individual.reference"

rrDetails: "Bundle.entry.resource.where(meta.profile = 'http://hl7.org/fhir/us/ecr/StructureDefinition/rr-reportability-information-observation')"
rrDisplayNames: "Bundle.entry.resource.where(meta.profile = 'http://hl7.org/fhir/us/ecr/StructureDefinition/rr-reportability-information-observation').valueCodeableConcept.coding.display"
rckmsTriggerSummaries: "Bundle.entry.resource.where(meta.profile = 'http://hl7.org/fhir/us/ecr/StructureDefinition/rr-reportability-information-observation').extension.where(url = 'http://hl7.org/fhir/us/ecr/StructureDefinition/us-ph-determination-of-reportability-rule-extension').valueString"

# Clinical Data
Expand Down
5 changes: 4 additions & 1 deletion containers/ecr-viewer/src/app/services/ecrMetadataService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,10 @@ export const evaluateEcrMetadata = (
let reportableConditionsList: ReportableConditions = {};

for (const condition of rrDetails) {
let name = condition.valueCodeableConcept.coding[0].display;
console.log(condition);
JNygaard-Skylight marked this conversation as resolved.
Show resolved Hide resolved
let name =
JNygaard-Skylight marked this conversation as resolved.
Show resolved Hide resolved
condition.valueCodeableConcept?.text ||
JNygaard-Skylight marked this conversation as resolved.
Show resolved Hide resolved
condition.valueCodeableConcept.coding[0].display;
const triggers = condition.extension
.filter(
(x: { url: string; valueString: string }) =>
Expand Down
3 changes: 2 additions & 1 deletion containers/ecr-viewer/src/app/services/ecrSummaryService.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,8 @@ export const evaluateEcrSummaryConditionSummary = (
if (!conditionsList[snomed]) {
conditionsList[snomed] = {
ruleSummaries: new Set(),
snomedDisplay: coding.display!,
snomedDisplay:
observation?.valueCodeableConcept?.text || coding.display!,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -734,7 +734,8 @@
"display": "Viral hepatitis type C (disorder)",
"system": "http://snomed.info/sct"
}
]
],
"text": "Hepatitis C"
},
"extension": [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -556,7 +556,8 @@
"display": "Viral hepatitis type C (disorder)",
"system": "http://snomed.info/sct"
}
]
],
"text": "Hepatitis C"
},
"extension": [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ describe("Evaluate Ecr Metadata", () => {
]);
expect(actual.ecrCustodianDetails.unavailableData).toBeEmpty();
});
it("should have rrDetails", () => {
it("should have rrDetails, and correctly handle human-readable condition name", () => {
const actual = evaluateEcrMetadata(
BundleWithEcrMetadata as unknown as Bundle,
mappings,
Expand All @@ -80,7 +80,7 @@ describe("Evaluate Ecr Metadata", () => {
"Detection of SARS-CoV-2 nucleic acid in a clinical or post-mortem specimen by any method":
new Set(["Tennessee Department of Health"]),
},
"Viral hepatitis type C (disorder)": {
"Hepatitis C": {
"Detection of Hepatitis C virus antibody in a clinical specimen by any method":
new Set(["California Department of Public Health"]),
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,13 +115,13 @@ describe("Evaluate eCR Summary Relevant Lab Results", () => {
});

describe("Evaluate eCR Summary Condition Summary", () => {
it("should return titles based on snomed code", () => {
it("should return titles based on snomed code, and return human-readable name if available", () => {
const actual = evaluateEcrSummaryConditionSummary(
BundleEcrSummary as unknown as Bundle,
mappings,
);

expect(actual[0].title).toEqual("Viral hepatitis type C (disorder)");
expect(actual[0].title).toEqual("Hepatitis C");
expect(actual[1].title).toEqual(
"Disease caused by severe acute respiratory syndrome coronavirus 2 (disorder)",
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
}
},
"condition": {
"fhir_path": "Observation.valueCodeableConcept.coding.display",
"fhir_path": "iif(Observation.valueCodeableConcept.text.exists(), Observation.valueCodeableConcept.text, Observation.valueCodeableConcept.coding.display)",
"data_type": "string",
"nullable": true,
"metadata": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@
}
},
"condition": {
"fhir_path": "Observation.valueCodeableConcept.coding.display",
"fhir_path": "iif(Observation.valueCodeableConcept.text.exists(), Observation.valueCodeableConcept.text, Observation.valueCodeableConcept.coding.display)",
"data_type": "string",
"nullable": true,
"metadata": {
Expand Down
4 changes: 2 additions & 2 deletions containers/trigger-code-reference/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from app.base_service import BaseService
from app.models import InsertConditionInput
from app.utils import _find_codes_by_resource_type
from app.utils import _stamp_resource_with_code_extension
from app.utils import add_code_extension_and_human_readable_name
from app.utils import find_conditions
from app.utils import get_clean_snomed_code
from app.utils import get_concepts_dict
Expand Down Expand Up @@ -126,7 +126,7 @@ async def stamp_condition_extensions(
break

if should_stamp:
entry["resource"] = _stamp_resource_with_code_extension(
entry["resource"] = add_code_extension_and_human_readable_name(
resource, cond
)

Expand Down
32 changes: 30 additions & 2 deletions containers/trigger-code-reference/app/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,11 +205,13 @@ def _find_codes_by_resource_type(resource: dict) -> List[str]:
return [x for x in codes if x != ""]


def _stamp_resource_with_code_extension(resource: dict, code: str) -> dict:
def add_code_extension_and_human_readable_name(resource: dict, code: str) -> dict:
"""
Stamps a provided resource with an extension containing a passed-in
SNOMED code. The stamping procedure adds an extension to the resource
body without altering any other existing data.
body without altering any other existing data. In addition, if the
resource is a Condition, the function will also add a human-readable
name to the resources valueCodeableConcept.text field.

:param resource: The resource to stamp.
:param code: The SNOMED code to insert an extension for.
Expand All @@ -223,9 +225,35 @@ def _stamp_resource_with_code_extension(resource: dict, code: str) -> dict:
"coding": [{"code": code, "system": "http://snomed.info/sct"}],
}
)

# If the resource is a Condition, add a human-readable name to the
# valueCodeableConcept.text field. If there is no code, assume it was not a
# condition/observation
if resource.get("code"):
if [
x
for x in resource["code"]["coding"]
if x["system"] == "http://snomed.info/sct"
and x["code"] == "64572001" # Condition
]:
condition_name = _get_condition_name_from_snomed_code(code)
if condition_name:
resource["valueCodeableConcept"]["text"] = condition_name

return resource


def _get_condition_name_from_snomed_code(snomed_code: str) -> str:
# Connect to the SQLite database, execute sql query, then close
with sqlite3.connect("seed-scripts/rckms.db") as conn:
cursor = conn.cursor()
cursor.execute("SELECT name FROM conditions WHERE id = ?", (snomed_code,))

condition_name = cursor.fetchone()[0]

return condition_name


def read_json_from_assets(filename: str) -> dict:
"""
Reads a JSON file from the assets directory.
Expand Down
1 change: 1 addition & 0 deletions containers/trigger-code-reference/dev-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ testcontainers[compose]
mammoth==1.8.0
beautifulsoup4==4.12.3
lxml==5.2.2
tqdm
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ def extract_condition_from_rckms_doc(
# convert the key to lowercase and strip any whitespace
key = parts[0].strip().lower()
# split the value and take the first part, then strip any whitespace
val = parts[1].strip().lower().split("|")[0].strip()
val = parts[1].strip().split("|")[0].strip()
# if the first part of the value is numeric, use only that
if val.split()[0].isnumeric():
val = val.split()[0]
Expand Down Expand Up @@ -413,41 +413,41 @@ def load_database(
with sqlite3.connect(db) as con:
cur = con.cursor()
for vs in valuesets:
# insert the ValueSet Type into the database if it does not exist
cur.execute(
"INSERT INTO value_set_type (id, clinical_service_type) "
"VALUES (:category, :category) "
"ON CONFLICT DO NOTHING",
vs.__dict__,
)
# insert the ValueSet into the database if it does not exist
cur.execute(
"INSERT INTO value_sets (id, version, value_set_name, author, clinical_service_type_id) "
"VALUES (:oid, :version, :title, :author, :category) "
"ON CONFLICT DO NOTHING",
vs.__dict__,
)
counts["valuesets"] += cur.rowcount
# # insert the ValueSet Type into the database if it does not exist
JNygaard-Skylight marked this conversation as resolved.
Show resolved Hide resolved
# cur.execute(
# "INSERT INTO value_set_type (id, clinical_service_type) "
# "VALUES (:category, :category) "
# "ON CONFLICT DO NOTHING",
# vs.__dict__,
# )
# # insert the ValueSet into the database if it does not exist
# cur.execute(
# "INSERT INTO value_sets (id, version, value_set_name, author, clinical_service_type_id) "
# "VALUES (:oid, :version, :title, :author, :category) "
# "ON CONFLICT DO NOTHING",
# vs.__dict__,
# )
# counts["valuesets"] += cur.rowcount
cond = {**{"oid": vs.oid}, **vs.conditions[0].__dict__}
# insert the Condition into the database if it does not exist
cur.execute(
"INSERT INTO conditions (id, value_set_id, system, name) "
"VALUES (:snomed, :oid, 'http://snomed.info/sct', :name) "
"INSERT INTO conditions (id, system, name) "
"VALUES (:snomed, 'http://snomed.info/sct', :name) "
"ON CONFLICT DO NOTHING",
cond,
)
counts["conditions"] += cur.rowcount
for cs in codesets:
oid = cs.valueset.oid
data = {**{"id": f"{oid}_{cs.code}", "oid": oid}, **cs.__dict__}
# insert the CodeSet into the database if it does not exist
cur.execute(
"INSERT INTO clinical_services (id, value_set_id, code, code_system, display, version) "
"VALUES (:id, :oid, :code, :system, :display, :version) "
"ON CONFLICT DO NOTHING",
data,
)
counts["codesets"] += cur.rowcount
# for cs in codesets:
# oid = cs.valueset.oid
# data = {**{"id": f"{oid}_{cs.code}", "oid": oid}, **cs.__dict__}
# # insert the CodeSet into the database if it does not exist
# cur.execute(
# "INSERT INTO clinical_services (id, value_set_id, code, code_system, display, version) "
# "VALUES (:id, :oid, :code, :system, :display, :version) "
# "ON CONFLICT DO NOTHING",
# data,
# )
# counts["codesets"] += cur.rowcount
con.commit()

# return the counts of conditions, valuesets and codesets loaded
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def test_extract_condition_from_rckms_doc():

result = srd.extract_condition_from_rckms_doc("test.1.docx", docx)
assert result == srd.Condition(
name="test condition",
name="Test Condition",
version="1",
snomed="12345",
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,11 @@ def test_stamp_conditions_no_resources_to_stamp(patched_get_services_list):
assert not found_matching_extension


@patch("app.utils._get_condition_name_from_snomed_code")
@patch("app.main.get_concepts_list")
def test_stamp_condition_extensions(patched_get_services_list):
def test_stamp_condition_extensions(
patched_get_services_list, patched_get_condition_name
):
# We'll just try stamping one of each resource type, no need
# to see 47 observations
message = json.load(open(Path(__file__).parent / "assets" / "sample_ecr.json"))
Expand Down Expand Up @@ -138,6 +141,9 @@ def test_stamp_condition_extensions(patched_get_services_list):
("dxtc", "8971234987123", "code-sys-2"),
("lotc", "72391283|8916394-2|24", "code-sys-1"),
]

patched_get_condition_name.return_value = "Test Condition"

input = {"bundle": message}
response = client.post("/stamp-condition-extensions", json=input)
assert response.status_code == 200
Expand Down
14 changes: 10 additions & 4 deletions containers/trigger-code-reference/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import pytest
from app.utils import _find_codes_by_resource_type
from app.utils import _stamp_resource_with_code_extension
from app.utils import add_code_extension_and_human_readable_name
from app.utils import convert_inputs_to_list
from app.utils import get_clean_snomed_code
from app.utils import get_concepts_dict
Expand Down Expand Up @@ -156,7 +156,8 @@ def test_find_codes_by_resource_type():
assert [] == _find_codes_by_resource_type(observation_resource)


def test_stamp_resource_with_code_extension():
@patch("app.utils._get_condition_name_from_snomed_code")
def test_add_code_extension_and_human_readable_name(mock_get_condition_name):
message = json.load(
open(
Path(__file__).parent / "assets" / "sample_ecr_with_diagnostic_report.json"
Expand All @@ -173,7 +174,10 @@ def test_stamp_resource_with_code_extension():
if e.get("resource").get("resourceType") == "Condition"
][0]

stamped_obs = _stamp_resource_with_code_extension(
# Mock the condition name lookup
mock_get_condition_name.return_value = "Cyclosporiasis"

stamped_obs = add_code_extension_and_human_readable_name(
observation_resource, "test_obs_code"
)
found_stamp = False
Expand All @@ -186,7 +190,9 @@ def test_stamp_resource_with_code_extension():
break
assert found_stamp

stamped_condition = _stamp_resource_with_code_extension(
assert stamped_obs["valueCodeableConcept"]["text"] == "Cyclosporiasis"

stamped_condition = add_code_extension_and_human_readable_name(
condition_resource, "test_cond_code"
)
found_stamp = False
Expand Down
Loading