Skip to content

Commit

Permalink
Merge branch 'main' into gordon/refactor-rr-info-parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
gordonfarrell authored Nov 7, 2024
2 parents 682350d + bae4e0c commit 91fca2f
Show file tree
Hide file tree
Showing 885 changed files with 328 additions and 242,365 deletions.
12 changes: 0 additions & 12 deletions .github/workflows/container-fhir-converter.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -94,18 +94,6 @@ jobs:
working-directory: ./containers/${{env.CONTAINER}}/tests/integration
run: |
python -m pytest -m "integration"
unit-test-dotnet-fhir-converter:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup dotnet
uses: actions/setup-dotnet@v3
with:
dotnet-version: "8.0.x"
- name: Run tests
working-directory: ./containers/${{env.CONTAINER}}
run: dotnet test
build-container:
runs-on: ubuntu-latest
steps:
Expand Down
112 changes: 74 additions & 38 deletions containers/ecr-viewer/seed-scripts/create-seed-data.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import argparse
import json
import os
import traceback
Expand All @@ -8,7 +9,22 @@
BASEDIR = os.path.dirname(os.path.abspath(__file__))


def convert_files():
def _get_args():
parser = argparse.ArgumentParser(
prog="Create Seed Data",
description="Convert eICR and RR files to FHIR bundles and insert them into the database.",
epilog="For each directory in baseECR/LA the script will look for a `CDA_eICR.xml` and `CDA_RR.xml` file. If they are found, it will convert them into a FHIR bundle (saved as `bundle.json`) and insert that into the database using the Orchestration service.",
)
parser.add_argument(
"-s",
"--skip_convert",
action="store_true",
help="If this is set, if `bundle.json` already exists, the script will not look for `CDA_eICR.xml` and `CDA_RR.xml` files to convert them again, and use the existing `bundle.json`.",
)
return parser.parse_args()


def _process_files(args):
"""
Convert eICR and RR into FHIR bundles using the FHIR converter.
Expand All @@ -28,7 +44,24 @@ def convert_files():

# Check if it's a directory
if os.path.isdir(folder_path):
try:
# If `bundle.json` exists and ski_convert is set just upload the bundle
if (
os.path.exists(os.path.join(folder_path, "bundle.json"))
and args.skip_convert
):
# Just upload the bundle
with open(
os.path.join(folder_path, "bundle.json")
) as fhir_file:
payload = {
"message_type": "fhir",
"data_type": "fhir",
"config_file_name": "save-bundle-to-ecr-viewer.json",
"message": json.load(fhir_file),
}
_process_eicrs(subfolder, folder, folder_path, payload)
# If we are not just inserting the bundle, check for the necessary files
elif os.path.exists(os.path.join(folder_path, "CDA_eICR.xml")):
# Open the necessary files in the folder
with (
open(
Expand All @@ -41,49 +74,52 @@ def convert_files():
payload = {
"message_type": "ecr",
"data_type": "ecr",
"config_file_name": "seed-ecr-viewer-config.json",
"config_file_name": "save-eicr-to-ecr-viewer-config.json",
"message": eicr_file.read(),
"rr_data": rr_file.read(),
}

print(f"{URL}/process-message for {subfolder}/{folder}")

response = requests.post(
f"{URL}/process-message", json=payload
)
if response.status_code == 200:
responses_json = response.json()["processed_values"][
"responses"
]
for response in responses_json:
if "stamped_ecr" in response:
with open(
os.path.join(folder_path, "bundle.json"),
"w",
) as fhir_file:
json.dump(
response["stamped_ecr"][
"extended_bundle"
],
fhir_file,
indent=4,
)
print(
f"Converted {folder} in {subfolder} successfully."
)
# Handle the case where the response fails
else:
print(f"Failed to convert {folder} in {subfolder}.")
# Handle file not found or other potential errors
except FileNotFoundError as e:
print(f"Required file not found in {folder_path}: {e}")
except Exception as e:
_process_eicrs(subfolder, folder, folder_path, payload)
# If neither `bundle.json` nor `CDA_eICR.xml` exists, skip processing
else:
print(
f"An error occurred processing {folder} in {subfolder}: {e}\n\n{traceback.format_exc()}"
f"Neither `bundle.json` nor `CDA_eICR.xml` found in {folder_path}. Skipping."
)
continue

# If the subfolder is not a directory, print a message
else:
print(f"{subfolder_path} is not a valid directory.")


convert_files()
def _process_eicrs(subfolder, folder, folder_path, payload):
try:
print(f"{URL}/process-message for {subfolder}/{folder}")
response = requests.post(f"{URL}/process-message", json=payload)
if response.status_code == 200:
responses_json = response.json()["processed_values"]["responses"]
for response in responses_json:
if "stamped_ecr" in response:
with open(
os.path.join(folder_path, "bundle.json"), "w"
) as fhir_file:
json.dump(
response["stamped_ecr"]["extended_bundle"],
fhir_file,
indent=4,
)
print(f"Converted {folder} in {subfolder} successfully.")
# Handle the case where the response fails
else:
print(f"Failed to convert {folder} in {subfolder}.")
# Handle file not found or other potential errors
except FileNotFoundError as e:
print(f"Required file not found in {folder_path}: {e}")
except Exception as e:
print(
f"An error occurred processing {folder} in {subfolder}: {e}\n\n{traceback.format_exc()}"
)


if __name__ == "__main__":
args = _get_args()
_process_files(args)
2 changes: 1 addition & 1 deletion containers/ecr-viewer/src/app/api/fhirPath.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ resolve: "Bundle.entry.resource.where(resourceType = %resourceType).where(id = %

# Clinical Info
activeProblems: "Bundle.entry.resource.where(resourceType='Condition').where(category.coding.code='problem-item-list')"
activeProblemsDisplay: "Condition.code.coding[0].display"
activeProblemsDisplay: "Condition.code.coding.display.first()"
activeProblemsOnsetDate: "Condition.onsetDateTime"
activeProblemsOnsetAge: "Condition.onsetAge.value"
activeProblemsComments: "Condition.note[0].text"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,6 @@ const EcrPaginationWrapper = ({
}}
>
<React.Fragment>
<option value="6">6</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="75">75</option>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,15 +129,9 @@ export const evaluatePatientContactInfo = (
fhirBundle: Bundle,
mappings: PathMappings,
) => {
const phoneNumbers = evaluate(fhirBundle, mappings.patientPhoneNumbers)
.map(
(phoneNumber) =>
`${
phoneNumber?.use?.charAt(0).toUpperCase() +
phoneNumber?.use?.substring(1)
} ${phoneNumber.value}`,
)
.join("\n");
const phoneNumbers = formatContactPoint(
evaluate(fhirBundle, mappings.patientPhoneNumbers),
).join("\n");
const emails = evaluate(fhirBundle, mappings.patientEmails)
.map((email) => `${email.value}`)
.join("\n");
Expand Down
63 changes: 25 additions & 38 deletions containers/ecr-viewer/src/app/services/formatService.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from "react";
import { ToolTipElement } from "@/app/view-data/components/ToolTipElement";
import { ContactPoint } from "fhir/r4";
import sanitizeHtml from "sanitize-html";
import { sanitizeAndMap } from "../view-data/utils/utils";

interface Metadata {
[key: string]: string;
Expand Down Expand Up @@ -211,19 +211,24 @@ export const formatDate = (dateString?: string): string | undefined => {
}
};

const VALID_PHONE_NUMBER_REGEX = /^\d{3}-\d{3}-\d{4}$/;
/**
* Formats a phone number into a standard format of XXX-XXX-XXXX.
* @param phoneNumber - The phone number to format.
* @returns The formatted phone number or undefined if the input is invalid.
* @returns The formatted phone number or "Invalid Number" if the input is invalid or undefined if the input is empty.
*/
export const formatPhoneNumber = (phoneNumber: string) => {
try {
return phoneNumber
.replace("+1", "")
.replace(/\D/g, "")
.replace(/(\d{3})(\d{3})(\d{4})/, "$1-$2-$3");
} catch {
return undefined;
export const formatPhoneNumber = (phoneNumber: string): string | undefined => {
if (!phoneNumber || phoneNumber.trim() === "") return undefined;

const formatted = phoneNumber
.replace("+1", "")
.replace(/\D/g, "")
.replace(/(\d{3})(\d{3})(\d{4})/, "$1-$2-$3");

if (VALID_PHONE_NUMBER_REGEX.test(formatted)) {
return formatted;
} else {
return "Invalid Number";
}
};

Expand Down Expand Up @@ -481,7 +486,7 @@ function processTable(table: Element): TableRow[] {
}

function getElementContent(el: Node): string {
return sanitizeHtml(el.textContent?.trim() ?? "");
return sanitizeAndMap(el.textContent?.trim() ?? "");
}

/**
Expand All @@ -508,31 +513,6 @@ export function extractNumbersAndPeriods(inputValues: string[]): string[] {
});
}

/**
* Truncates up to the character limit. If it stops in the middle of the word, it removes the whole word.
* @param input_str - The string to truncate
* @param character_limit - The number of characters to truncate defaults to 30
* @returns - The string that was
*/
export const truncateLabNameWholeWord = (
input_str: string,
character_limit: number = 30,
) => {
if (input_str.length <= character_limit) {
return input_str;
}

const trimStr = input_str.substring(0, 30);
const lastSpaceIndex = trimStr.lastIndexOf(" ");

if (lastSpaceIndex === -1) {
return input_str.length <= character_limit ? input_str : "";
}

// Truncate to the last full word within the limit
return input_str.substring(0, lastSpaceIndex);
};

/**
* Converts a string to sentence case, making the first character uppercase and the rest lowercase.
* @param str - The string to convert to sentence case.
Expand Down Expand Up @@ -596,11 +576,18 @@ export const formatContactPoint = (
const phoneNumberUse = toSentenceCase(contactPoint.use ?? "");
contactArr.push(
[phoneNumberUse, formatPhoneNumber(contactPoint.value ?? "")]
.join(" ")
.trim(),
.filter((c) => c)
.join(": "),
);
} else if (contactPoint.system === "email" && contactPoint.value) {
contactArr.push(contactPoint.value);
} else if (contactPoint.system === "fax" && contactPoint.value) {
const faxNumberUse = toSentenceCase(contactPoint.use ?? "");
contactArr.push(
[faxNumberUse, "Fax:", formatPhoneNumber(contactPoint.value ?? "")]
.join(" ")
.trim(),
);
}
}
return contactArr;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,4 +143,19 @@ describe("Pagination for EcrPaginationWrapper", () => {

expect(screen.getByText("Showing 0-0 of 0 eCRs")).toBeInTheDocument();
});

it("the dropdown should only have 25, 50, 75, and 100 as options", async () => {
render(
<EcrPaginationWrapper totalCount={0}>
<br />
</EcrPaginationWrapper>,
);

const select = screen.getByTestId("Select");
expect(select.children).toHaveLength(4);
expect(select.children[0]).toHaveTextContent("25");
expect(select.children[1]).toHaveTextContent("50");
expect(select.children[2]).toHaveTextContent("75");
expect(select.children[3]).toHaveTextContent("100");
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -149,11 +149,6 @@ exports[`EcrPaginationWrapper should match snapshot 1`] = `
id="input-select"
name="input-select"
>
<option
value="6"
>
6
</option>
<option
value="25"
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,9 @@ exports[`LabInfo should match snapshot test 1`] = `
<div>
<h4
class="usa-summary-box__heading padding-y-105"
id="lab-results-from-providence-st-joseph-medical"
id="lab-results-from-providence-st-joseph-medical-center-laboratory-clia-05d0672675"
>
Lab Results from
PROVIDENCE ST. JOSEPH MEDICAL
Lab Results from PROVIDENCE ST. JOSEPH MEDICAL CENTER LABORATORY (CLIA 05D0672675)
</h4>
<div
class="usa-summary-box__text"
Expand Down Expand Up @@ -782,8 +781,7 @@ Burbank, CA
class="usa-summary-box__heading padding-y-105"
id="lab-results-from-vumc-cerner-lab"
>
Lab Results from
VUMC CERNER LAB
Lab Results from VUMC CERNER LAB
</h4>
<div
class="usa-summary-box__text"
Expand Down
Loading

0 comments on commit 91fca2f

Please sign in to comment.