Several IPS fixes (#5297)

* Several IPS fixes

* One more fix

* Fix

* Fixes

* med fixes

* Null safety
This commit is contained in:
James Agnew 2023-09-12 11:41:33 -04:00 committed by GitHub
parent da58e9f250
commit 03ebabad5b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 394 additions and 88 deletions

View File

@ -0,0 +1,13 @@
---
type: fix
issue: 5297
title: "Several fixes have been made to the IPS generator:
<ul>
<li>The display names associated with several sections have been corrected to exactly match the LOINC definitions for their codes</li>
<li>Immunizations will now be ordered from most recent to least recent</li>
<li>IPS documents containing Consent resources for Advanced Directives could result in a crash</li>
<li>IPS documents containing Procedure resources for History of Procedures with a performed date could result in a crash</li>
<li>IPS documents containing AllergyIntolerance resources containing an occurrence but not a reaction date could result in a crash</li>
<li>IPS documents containing AllergyIntolerance resources containing an onset value in string format could result in a crash</li>
<li>IPS documents containing MedicationRequest resources with no associated Medication could result in a crash</li>
</ul>"

View File

@ -105,7 +105,7 @@ public class SectionRegistry {
addSection(IpsSectionEnum.ALLERGY_INTOLERANCE)
.withTitle("Allergies and Intolerances")
.withSectionCode("48765-2")
.withSectionDisplay("Allergies and Adverse Reactions")
.withSectionDisplay("Allergies and adverse reactions Document")
.withResourceTypes(ResourceType.AllergyIntolerance.name())
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionAllergies")
@ -117,7 +117,7 @@ public class SectionRegistry {
addSection(IpsSectionEnum.MEDICATION_SUMMARY)
.withTitle("Medication List")
.withSectionCode("10160-0")
.withSectionDisplay("Medication List")
.withSectionDisplay("History of Medication use Narrative")
.withResourceTypes(
ResourceType.MedicationStatement.name(),
ResourceType.MedicationRequest.name(),
@ -133,7 +133,7 @@ public class SectionRegistry {
addSection(IpsSectionEnum.PROBLEM_LIST)
.withTitle("Problem List")
.withSectionCode("11450-4")
.withSectionDisplay("Problem List")
.withSectionDisplay("Problem list - Reported")
.withResourceTypes(ResourceType.Condition.name())
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionProblems")
@ -145,7 +145,7 @@ public class SectionRegistry {
addSection(IpsSectionEnum.IMMUNIZATIONS)
.withTitle("History of Immunizations")
.withSectionCode("11369-6")
.withSectionDisplay("History of Immunizations")
.withSectionDisplay("History of Immunization Narrative")
.withResourceTypes(ResourceType.Immunization.name())
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionImmunizations")
@ -156,7 +156,7 @@ public class SectionRegistry {
addSection(IpsSectionEnum.PROCEDURES)
.withTitle("History of Procedures")
.withSectionCode("47519-4")
.withSectionDisplay("History of Procedures")
.withSectionDisplay("History of Procedures Document")
.withResourceTypes(ResourceType.Procedure.name())
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionProceduresHx")
@ -166,8 +166,8 @@ public class SectionRegistry {
protected void addSectionMedicalDevices() {
addSection(IpsSectionEnum.MEDICAL_DEVICES)
.withTitle("Medical Devices")
.withSectionCode("46240-8")
.withSectionDisplay("Medical Devices")
.withSectionCode("46264-8")
.withSectionDisplay("History of medical device use")
.withResourceTypes(ResourceType.DeviceUseStatement.name())
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionMedicalDevices")
@ -178,7 +178,7 @@ public class SectionRegistry {
addSection(IpsSectionEnum.DIAGNOSTIC_RESULTS)
.withTitle("Diagnostic Results")
.withSectionCode("30954-2")
.withSectionDisplay("Diagnostic Results")
.withSectionDisplay("Relevant diagnostic tests/laboratory data Narrative")
.withResourceTypes(ResourceType.DiagnosticReport.name(), ResourceType.Observation.name())
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionResults")
@ -189,7 +189,7 @@ public class SectionRegistry {
addSection(IpsSectionEnum.VITAL_SIGNS)
.withTitle("Vital Signs")
.withSectionCode("8716-3")
.withSectionDisplay("Vital Signs")
.withSectionDisplay("Vital signs")
.withResourceTypes(ResourceType.Observation.name())
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionVitalSigns")
@ -200,7 +200,7 @@ public class SectionRegistry {
addSection(IpsSectionEnum.PREGNANCY)
.withTitle("Pregnancy Information")
.withSectionCode("10162-6")
.withSectionDisplay("Pregnancy Information")
.withSectionDisplay("History of pregnancies Narrative")
.withResourceTypes(ResourceType.Observation.name())
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionPregnancyHx")
@ -211,7 +211,7 @@ public class SectionRegistry {
addSection(IpsSectionEnum.SOCIAL_HISTORY)
.withTitle("Social History")
.withSectionCode("29762-2")
.withSectionDisplay("Social History")
.withSectionDisplay("Social history Narrative")
.withResourceTypes(ResourceType.Observation.name())
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionSocialHistory")
@ -222,7 +222,7 @@ public class SectionRegistry {
addSection(IpsSectionEnum.ILLNESS_HISTORY)
.withTitle("History of Past Illness")
.withSectionCode("11348-0")
.withSectionDisplay("History of Past Illness")
.withSectionDisplay("History of Past illness Narrative")
.withResourceTypes(ResourceType.Condition.name())
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionPastIllnessHx")
@ -233,7 +233,7 @@ public class SectionRegistry {
addSection(IpsSectionEnum.FUNCTIONAL_STATUS)
.withTitle("Functional Status")
.withSectionCode("47420-5")
.withSectionDisplay("Functional Status")
.withSectionDisplay("Functional status assessment note")
.withResourceTypes(ResourceType.ClinicalImpression.name())
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionFunctionalStatus")
@ -244,7 +244,7 @@ public class SectionRegistry {
addSection(IpsSectionEnum.PLAN_OF_CARE)
.withTitle("Plan of Care")
.withSectionCode("18776-5")
.withSectionDisplay("Plan of Care")
.withSectionDisplay("Plan of care note")
.withResourceTypes(ResourceType.CarePlan.name())
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionPlanOfCare")
@ -254,8 +254,8 @@ public class SectionRegistry {
protected void addSectionAdvanceDirectives() {
addSection(IpsSectionEnum.ADVANCE_DIRECTIVES)
.withTitle("Advance Directives")
.withSectionCode("42349-0")
.withSectionDisplay("Advance Directives")
.withSectionCode("42348-3")
.withSectionDisplay("Advance directives")
.withResourceTypes(ResourceType.Consent.name())
.withProfile(
"https://hl7.org/fhir/uv/ips/StructureDefinition-Composition-uv-ips-definitions.html#Composition.section:sectionAdvanceDirectives")

View File

@ -24,6 +24,8 @@ import ca.uhn.fhir.jpa.ips.api.IpsContext;
import ca.uhn.fhir.jpa.ips.api.SectionRegistry;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.rest.api.SortOrderEnum;
import ca.uhn.fhir.rest.api.SortSpec;
import ca.uhn.fhir.rest.param.TokenOrListParam;
import ca.uhn.fhir.rest.param.TokenParam;
import com.google.common.collect.Lists;
@ -109,12 +111,14 @@ public class DefaultIpsGenerationStrategy implements IIpsGenerationStrategy {
switch (theIpsSectionContext.getSection()) {
case ALLERGY_INTOLERANCE:
case PROBLEM_LIST:
case IMMUNIZATIONS:
case PROCEDURES:
case MEDICAL_DEVICES:
case ILLNESS_HISTORY:
case FUNCTIONAL_STATUS:
return;
case IMMUNIZATIONS:
theSearchParameterMap.setSort(new SortSpec(Immunization.SP_DATE).setOrder(SortOrderEnum.DESC));
return;
case VITAL_SIGNS:
if (theIpsSectionContext.getResourceType().equals(ResourceType.Observation.name())) {
theSearchParameterMap.add(

View File

@ -22,7 +22,7 @@ Date: Consent.dateTime
<th:block th:with="extension=${entry.getResource().getExtensionByUrl('http://hl7.org/fhir/StructureDefinition/narrativeLink').getValue().getValue()}">
<tr th:id="${#strings.arraySplit(extension, '#')[1]}">
<td th:insert="IpsUtilityFragments :: codeableConcept (cc=*{getScope()},attr='display')">Scope</td>
<td th:text="*{getStatus().getCode()}">Status</td>
<td th:text="*{getStatus().getDisplay()}">Status</td>
<td th:insert="IpsUtilityFragments :: concatCodeableConcept (list=*{getProvision().getAction()})">Action Controlled</td>
<td th:text="*{getDateTimeElement().getValue()}">Date</td>
</tr>

View File

@ -26,12 +26,22 @@ Comments: AllergyIntolerance.note[x].text (separated by <br />)
<th:block th:with="extension=${entry.getResource().getExtensionByUrl('http://hl7.org/fhir/StructureDefinition/narrativeLink').getValue().getValue()}">
<tr th:id="${#strings.arraySplit(extension, '#')[1]}">
<td th:insert="IpsUtilityFragments :: codeableConcept (cc=*{getCode()},attr='display')">Allergen</td>
<td th:insert="IpsUtilityFragments :: codeableConcept (cc=*{getClinicalStatus()},attr='code')">Status</td>
<td th:insert="~{IpsUtilityFragments :: codeableConcept (cc=*{getClinicalStatus()},attr='code')}">Status</td>
<td th:insert="IpsUtilityFragments :: concat (list=*{getCategory()},attr='value')">Category</td>
<td th:insert="IpsUtilityFragments :: concatReactionManifestation (list=*{getReaction()})">Reaction</td>
<td th:insert="IpsUtilityFragments :: concat (list=*{getReaction()},attr='severity')">Severity</td>
<td th:insert="IpsUtilityFragments :: concat (list=*{getNote()},attr='text')">Comments</td>
<td th:text="*{getOnsetDateTimeType().getValue()}">Onset</td>
<th:block th:if="*{hasOnsetDateTimeType()}">
<td th:text="*{getOnsetDateTimeType().getValue()}">Onset</td>
</th:block>
<th:block th:if="*{hasOnsetStringType()}">
<td th:text="*{getOnsetStringType().getValue()}">Onset</td>
</th:block>
<th:block th:if="*{!hasOnsetDateTimeType() && !hasOnsetStringType()}">
<td></td>
</th:block>
</tr>
</th:block>
</th:block>

View File

@ -21,7 +21,7 @@ Date: Procedure.performedDateTime || Procedure.performedPeriod.start && “-“
<tr th:id="${#strings.arraySplit(extension, '#')[1]}">
<td th:insert="IpsUtilityFragments :: codeableConcept (cc=*{getCode()},attr='display')">Procedure</td>
<td th:insert="IpsUtilityFragments :: concat (list=*{getNote()},attr='text')">Comments</td>
<td th:insert=":: renderPerformed (performed=*{getPerformed()})">Date</td>
<td th:insert="IpsUtilityFragments :: renderPerformed (performed=*{getPerformed()})">Date</td>
</tr>
</th:block>
</th:block>

View File

@ -22,7 +22,7 @@ Onset Date: Condition.onsetDateTime || Condition.onsetPeriod.start && “-“ &&
<th:block th:with="extension=${entry.getResource().getExtensionByUrl('http://hl7.org/fhir/StructureDefinition/narrativeLink').getValue().getValue()}">
<tr th:id="${#strings.arraySplit(extension, '#')[1]}">
<td th:insert="IpsUtilityFragments :: codeableConcept (cc=*{getCode()},attr='display')">Medical Problem</td>
<td th:insert="IpsUtilityFragments :: codeableConcept (cc=*{getClinicalStatus()},attr='code')">Status</td>
<td th:insert="~{IpsUtilityFragments :: codeableConcept (cc=*{getClinicalStatus()},attr='code')}">Status</td>
<td th:insert="IpsUtilityFragments :: concat (list=*{getNote()},attr='text')">Comments</td>
<td th:insert="IpsUtilityFragments :: renderOnset (onset=*{getOnset()})">Onset Date</td>
</tr>

View File

@ -22,7 +22,7 @@ Onset Date: Condition.onsetDateTime || Condition.onsetPeriod.start && “-“ &&
<th:block th:with="extension=${entry.getResource().getExtensionByUrl('http://hl7.org/fhir/StructureDefinition/narrativeLink').getValue().getValue()}">
<tr th:id="${#strings.arraySplit(extension, '#')[1]}">
<td th:insert="IpsUtilityFragments :: codeableConcept (cc=*{getCode()},attr='display')">Medical Problems</td>
<td th:insert="IpsUtilityFragments :: codeableConcept (cc=*{getClinicalStatus()},attr='code')">Status</td>
<td th:insert="~{IpsUtilityFragments :: codeableConcept (cc=*{getClinicalStatus()},attr='code')}">Status</td>
<td th:insert="IpsUtilityFragments :: concat (list=*{getNote()},attr='text')">Comments</td>
<td th:insert="IpsUtilityFragments :: renderOnset (onset=*{getOnset()})">Onset Date</td>
</tr>

View File

@ -23,7 +23,7 @@
</th:block>
<th:block th:fragment="renderMedication (medicationType)">
<th:block th:object="${medicationType}">
<th:block th:if="${medicationType} != null" th:object="${medicationType}">
<th:block th:switch="*{getClass().getSimpleName()}">
<th:block th:case="'CodeableConcept'">
<th:block th:replace=":: codeableConcept (cc=${medicationType}, attr='display')">Medication</th:block>
@ -44,13 +44,15 @@
</th:block>
<th:block th:if="${medication}" th:fragment="renderMedicationCode (medication)">
<th:block th:replace=":: codeableConcept (cc=${medication.getCode()},attr='display')">Medication</th:block>
<th:block th:if="${medication} != null">
<th:block th:replace=":: codeableConcept (cc=${medication.getCode()},attr='display')">Medication</th:block>
</th:block>
</th:block>
<!--/* Dose Number */-->
<th:block th:if="${doseNumber}" th:fragment="renderDoseNumber (doseNumber)">
<th:block th:object="${doseNumber}">
<th:block th:if="${doseNumber} != null" th:object="${doseNumber}">
<th:block th:switch="*{getClass().getSimpleName()}">
<th:block th:case="'PositiveIntType'" th:text="*{getValue()}">Dose Number</th:block>
<th:block th:case="'StringType'" th:text="*{getValue()}">Dose Number</th:block>
@ -61,7 +63,7 @@
<!--/* Value */-->
<th:block th:if="${value}" th:fragment="renderValue (value)">
<th:block th:object="${value}">
<th:block th:if="${value} != null" th:object="${value}">
<th:block th:switch="*{getClass().getSimpleName()}">
<th:block th:case="'Quantity'" th:text="*{getValue()}">Result</th:block>
<th:block th:case="'DateTimeType'" th:text="*{getValue()}">Result</th:block>
@ -74,7 +76,7 @@
</th:block>
<th:block th:if="${value}" th:fragment="renderValueUnit (value)">
<th:block th:object="${value}">
<th:block th:if="${value} != null" th:object="${value}">
<th:block th:switch="*{getClass().getSimpleName()}">
<th:block th:case="'Quantity'" th:text="*{getUnit()}">Unit</th:block>
</th:block>
@ -84,7 +86,7 @@
<!--/* Dates */-->
<th:block th:if="${effective}" th:fragment="renderEffective (effective)">
<th:block th:object="${effective}">
<th:block th:if="${effective} != null" th:object="${effective}">
<th:block th:switch="*{getClass().getSimpleName()}">
<th:block th:case="'DateTimeType'" th:text="*{getValue()}">Date</th:block>
<th:block th:case="'Period'" th:text="*{getStartElement().getValue()}">Date</th:block>
@ -93,7 +95,7 @@
</th:block>
<th:block th:if="${onset}" th:fragment="renderOnset (onset)">
<th:block th:object="${onset}">
<th:block th:if="${onset} != null" th:object="${onset}">
<th:block th:switch="*{getClass().getSimpleName()}">
<th:block th:case="'DateTimeType'" th:text="*{getValue()}">Date</th:block>
<th:block th:case="'Period'"
@ -110,7 +112,7 @@
</th:block>
<th:block th:if="${performed}" th:fragment="renderPerformed (performed)">
<th:block th:object="${performed}">
<th:block th:if="${performed} != null" th:object="${performed}">
<th:block th:switch="*{getClass().getSimpleName()}">
<th:block th:case="'DateTimeType'" th:text="*{getValue()}">Date</th:block>
<th:block th:case="'Period'"
@ -127,7 +129,7 @@
</th:block>
<th:block th:if="${occurrence}" th:fragment="renderOccurrence (occurrence)">
<th:block th:object="${occurrence}">
<th:block th:if="${occurrence} != null" th:object="${occurrence}">
<th:block th:switch="*{getClass().getSimpleName()}">
<th:block th:case="'DateTimeType'" th:text="*{getValue()}">Date</th:block>
<th:block th:case="'StringType'" th:text="*{getValue()}">Date</th:block>
@ -136,7 +138,7 @@
</th:block>
<th:block th:if="${recorded}" th:fragment="renderRecorded (recorded)">
<th:block th:object="${recorded}">
<th:block th:if="${recorded} != null" th:object="${recorded}">
<th:block th:switch="*{getClass().getSimpleName()}">
<th:block th:case="'DateTimeType'" th:text="*{getValue()}">Date Recorded</th:block>
</th:block>
@ -146,19 +148,21 @@
<!--/* CodeableConcept */-->
<th:block th:if="${cc}" th:fragment="codeableConcept (cc, attr)">
<th:block th:if="${!cc.getTextElement().empty}" th:text="${cc.getText()}"/>
<th:block th:if="${cc.getTextElement().empty}" th:switch="${attr} ?: 'display'">
<th:block th:case="'display'">
<th:block th:replace=":: concat (list=${cc.getCoding()},attr='display')"/>
</th:block>
<th:block th:case="'code'">
<th:block th:replace=":: concat (list=${cc.getCoding()},attr='code')"/>
<th:block th:if="${cc} != null">
<th:block th:if="${!cc.getTextElement().empty}" th:text="${cc.getText()}"/>
<th:block th:if="${cc.getTextElement().empty}" th:switch="${attr} ?: 'display'">
<th:block th:case="'display'">
<th:block th:replace=":: concat (list=${cc.getCoding()},attr='display')"/>
</th:block>
<th:block th:case="'code'">
<th:block th:replace=":: concat (list=${cc.getCoding()},attr='code')"/>
</th:block>
</th:block>
</th:block>
</th:block>
<th:block th:if="${list}" th:fragment="firstFromCodeableConceptList (list)">
<th:block th:if="${!list.empty}" with="${attr} ?: 'display'">
<th:block th:if="${list} != null AND ${!list.empty}" with="${attr} ?: 'display'">
<th:block th:replace=":: codeableConcept (cc=${list.get(0)},attr=${attr})">Interpretation</th:block>
</th:block>
</th:block>
@ -181,7 +185,9 @@
<th:block th:replace=":: concatItem (listItem=${item}, iter=${iter}, separator='')"/>
</th:block>
<th:block th:case="'severity'">
<th:block th:replace=":: concatItem (listItem=${item.getSeverity().toCode()}, iter=${iter}, separator='')"/>
<th:block th:if="${item.getSeverity() != null}">
<th:block th:replace=":: concatItem (listItem=${item.getSeverity().toCode()}, iter=${iter}, separator='')"/>
</th:block>
</th:block>
</th:block>
</th:block>
@ -206,21 +212,21 @@
<th:block th:if="${!item.hasDescription()}">
<th:block th:replace=":: concatCodeableConcept (list=${item.getManifestation()})">Reaction</th:block>
</th:block>
<th:block th:if="${!iter.last}" th:text=", "/>
<th:block th:if="${!iter.last}" th:text="', '"/>
</th:block>
</th:block>
<th:block th:if="${list}" th:fragment="concatCodeableConcept (list)">
<th:block th:each="item,iter : ${list}" th:if="${!list.empty}" with="attr=${attr} ?: 'display'">
<th:block th:replace=":: codeableConcept (cc=${item},attr=${attr})"/>
<th:block th:if="${!iter.last}" th:text=", "/>
<th:block th:if="${!iter.last}" th:text="', '"/>
</th:block>
</th:block>
<th:block th:if="${list}" th:fragment="concatDosageRoute (list)">
<th:block th:each="item,iter : ${list}" th:if="${!list.empty}" with="attr=${attr} ?: 'display'">
<th:block th:replace=":: codeableConcept (cc=${item.getRoute()},attr=${attr})"/>
<th:block th:if="${!iter.last}" th:text=", "/>
<th:block th:if="${!iter.last}" th:text="', '"/>
</th:block>
</th:block>
@ -238,6 +244,6 @@
th:text="${#strings.concatReplaceNulls('', item.getLow().getValue(), '-', item.getHigh().getValue() )}">
Reference Range
</th:block>
<th:block th:if="${!iter.last}" th:text=", "/>
<th:block th:if="${!iter.last}" th:text="', '"/>
</th:block>
</th:block>

View File

@ -20,11 +20,15 @@ import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.CodeSystem;
import org.hl7.fhir.r4.model.CodeableConcept;
import org.hl7.fhir.r4.model.Composition;
import org.hl7.fhir.r4.model.Condition;
import org.hl7.fhir.r4.model.DateTimeType;
import org.hl7.fhir.r4.model.Immunization;
import org.hl7.fhir.r4.model.MedicationStatement;
import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.Resource;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
@ -37,10 +41,12 @@ import org.springframework.test.context.ContextConfiguration;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.List;
import java.util.stream.Collectors;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.matchesPattern;
import static org.hamcrest.Matchers.stringContainsInOrder;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ -102,6 +108,58 @@ public class IpsGenerationR4Test extends BaseResourceProviderR4Test {
assertThat(sectionTitles.toString(), sectionTitles, contains("Allergies and Intolerances", "Medication List", "Problem List", "History of Immunizations", "Diagnostic Results"));
}
@Test
public void testGenerateLargePatientSummary2() {
myStorageSettings.setResourceClientIdStrategy(JpaStorageSettings.ClientIdStrategyEnum.ANY);
Bundle sourceData = ClasspathUtil.loadCompressedResource(myFhirContext, Bundle.class, "/large-patient-everything-2.json.gz");
sourceData.setType(Bundle.BundleType.TRANSACTION);
for (Bundle.BundleEntryComponent nextEntry : sourceData.getEntry()) {
nextEntry.getRequest().setMethod(Bundle.HTTPVerb.PUT);
nextEntry.getRequest().setUrl(nextEntry.getResource().getIdElement().toUnqualifiedVersionless().getValue());
}
Bundle outcome = mySystemDao.transaction(mySrd, sourceData);
ourLog.info("Created {} resources", outcome.getEntry().size());
Bundle output = myClient
.operation()
.onInstance("Patient/11439250")
.named(JpaConstants.OPERATION_SUMMARY)
.withNoParameters(Parameters.class)
.returnResourceType(Bundle.class)
.execute();
ourLog.info("Output: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(output));
// Verify
assertEquals(74, output.getEntry().size());
}
@Test
public void testGenerateLargePatientSummary3() {
myStorageSettings.setResourceClientIdStrategy(JpaStorageSettings.ClientIdStrategyEnum.ANY);
Bundle sourceData = ClasspathUtil.loadCompressedResource(myFhirContext, Bundle.class, "/large-patient-everything-3.json.gz");
sourceData.setType(Bundle.BundleType.TRANSACTION);
for (Bundle.BundleEntryComponent nextEntry : sourceData.getEntry()) {
nextEntry.getRequest().setMethod(Bundle.HTTPVerb.PUT);
nextEntry.getRequest().setUrl(nextEntry.getResource().getIdElement().toUnqualifiedVersionless().getValue());
}
Bundle outcome = mySystemDao.transaction(mySrd, sourceData);
ourLog.info("Created {} resources", outcome.getEntry().size());
Bundle output = myClient
.operation()
.onInstance("Patient/nl-core-Patient-01")
.named(JpaConstants.OPERATION_SUMMARY)
.withNoParameters(Parameters.class)
.returnResourceType(Bundle.class)
.execute();
ourLog.info("Output: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(output));
// Verify
assertEquals(80, output.getEntry().size());
}
@Test
public void testGenerateTinyPatientSummary() {
myStorageSettings.setResourceClientIdStrategy(JpaStorageSettings.ClientIdStrategyEnum.ANY);
@ -136,6 +194,74 @@ public class IpsGenerationR4Test extends BaseResourceProviderR4Test {
assertThat(sectionTitles.toString(), sectionTitles, contains("Allergies and Intolerances", "Medication List", "Problem List"));
}
/**
* Default strategy should order immunizations alphabetically
*/
@Test
public void testImmunizationOrder() {
// Setup
createPatient(withId("PT1"), withFamily("Simpson"), withGiven("Homer"));
// Create some immunizations out of order
Immunization i;
i = new Immunization();
i.setPatient(new Reference("Patient/PT1"));
i.setOccurrence(new DateTimeType("2010-01-01T00:00:00Z"));
i.setVaccineCode(new CodeableConcept().setText("Vax 2010"));
myImmunizationDao.create(i, mySrd);
i = new Immunization();
i.setPatient(new Reference("Patient/PT1"));
i.setOccurrence(new DateTimeType("2005-01-01T00:00:00Z"));
i.setVaccineCode(new CodeableConcept().setText("Vax 2005"));
myImmunizationDao.create(i, mySrd);
i = new Immunization();
i.setPatient(new Reference("Patient/PT1"));
i.setOccurrence(new DateTimeType("2015-01-01T00:00:00Z"));
i.setVaccineCode(new CodeableConcept().setText("Vax 2015"));
myImmunizationDao.create(i, mySrd);
// Test
Bundle output = myClient
.operation()
.onInstance("Patient/PT1")
.named(JpaConstants.OPERATION_SUMMARY)
.withNoParameters(Parameters.class)
.returnResourceType(Bundle.class)
.execute();
ourLog.info("Output: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(output));
Composition composition = findCompositionSectionByDisplay(output, "History of Immunization Narrative");
// Should be newest first
assertThat(composition.getText().getDivAsString(), stringContainsInOrder(
"Vax 2015", "Vax 2010", "Vax 2005"
));
List<String> resourceDates = output
.getEntry()
.stream()
.filter(t -> t.getResource() instanceof Immunization)
.map(t -> (Immunization) t.getResource())
.map(t -> t.getOccurrenceDateTimeType().getValueAsString().substring(0, 4))
.collect(Collectors.toList());
assertThat(resourceDates, contains("2015", "2010", "2005"));
}
@Nonnull
private static Composition findCompositionSectionByDisplay(Bundle output, String theDisplay) {
Composition composition = (Composition) output.getEntry().get(0).getResource();
Composition.SectionComponent section = composition
.getSection()
.stream()
.filter(t -> t.getCode().getCoding().get(0).getDisplay().equals(theDisplay))
.findFirst()
.orElseThrow();
return composition;
}
@Nonnull
private static List<String> extractSectionTitles(Bundle outcome) {
Composition composition = (Composition) outcome.getEntry().get(0).getResource();

View File

@ -15,10 +15,40 @@ import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.server.SimpleBundleProvider;
import ca.uhn.fhir.test.utilities.HtmlUtil;
import ca.uhn.fhir.util.ClasspathUtil;
import com.gargoylesoftware.htmlunit.html.*;
import com.gargoylesoftware.htmlunit.html.DomElement;
import com.gargoylesoftware.htmlunit.html.DomNodeList;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import com.gargoylesoftware.htmlunit.html.HtmlTable;
import com.gargoylesoftware.htmlunit.html.HtmlTableRow;
import com.google.common.collect.Lists;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.*;
import org.hl7.fhir.r4.model.AllergyIntolerance;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.CarePlan;
import org.hl7.fhir.r4.model.ClinicalImpression;
import org.hl7.fhir.r4.model.Composition;
import org.hl7.fhir.r4.model.Condition;
import org.hl7.fhir.r4.model.Consent;
import org.hl7.fhir.r4.model.DateTimeType;
import org.hl7.fhir.r4.model.Device;
import org.hl7.fhir.r4.model.DeviceUseStatement;
import org.hl7.fhir.r4.model.DiagnosticReport;
import org.hl7.fhir.r4.model.Encounter;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Immunization;
import org.hl7.fhir.r4.model.Medication;
import org.hl7.fhir.r4.model.MedicationAdministration;
import org.hl7.fhir.r4.model.MedicationDispense;
import org.hl7.fhir.r4.model.MedicationRequest;
import org.hl7.fhir.r4.model.MedicationStatement;
import org.hl7.fhir.r4.model.Observation;
import org.hl7.fhir.r4.model.Organization;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.PositiveIntType;
import org.hl7.fhir.r4.model.Procedure;
import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.Resource;
import org.hl7.fhir.r4.model.StringType;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@ -29,17 +59,22 @@ import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import java.io.IOException;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
import static ca.uhn.fhir.jpa.ips.generator.IpsGenerationR4Test.findEntryResource;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.startsWith;
import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* This test verifies various IPS generation logic without using a full
@ -76,37 +111,6 @@ public class IpsGeneratorSvcImplTest {
private IIpsGeneratorSvc mySvc;
private DefaultIpsGenerationStrategy myStrategy;
@Nonnull
private static List<String> toEntryResourceTypeStrings(Bundle outcome) {
return outcome
.getEntry()
.stream()
.map(t -> t.getResource().getResourceType().name())
.collect(Collectors.toList());
}
@Nonnull
private static Medication createSecondaryMedication(String medicationId) {
Medication medication = new Medication();
medication.setId(new IdType(medicationId));
medication.getCode().addCoding().setDisplay("Tylenol");
ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(medication, BundleEntrySearchModeEnum.INCLUDE);
return medication;
}
@Nonnull
private static MedicationStatement createPrimaryMedicationStatement(String medicationId, String medicationStatementId) {
MedicationStatement medicationStatement = new MedicationStatement();
medicationStatement.setId(medicationStatementId);
medicationStatement.setMedication(new Reference(medicationId));
medicationStatement.setStatus(MedicationStatement.MedicationStatementStatus.ACTIVE);
medicationStatement.getDosageFirstRep().getRoute().addCoding().setDisplay("Oral");
medicationStatement.getDosageFirstRep().setText("DAW");
medicationStatement.setEffective(new DateTimeType("2023-01-01T11:22:33Z"));
ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(medicationStatement, BundleEntrySearchModeEnum.MATCH);
return medicationStatement;
}
@BeforeEach
public void beforeEach() {
myDaoRegistry.setResourceDaos(Collections.emptyList());
@ -153,6 +157,83 @@ public class IpsGeneratorSvcImplTest {
}
@Test
public void testAllergyIntolerance_OnsetTypes() throws IOException {
// Setup Patient
registerPatientDaoWithRead();
AllergyIntolerance allergy1 = new AllergyIntolerance();
ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(allergy1, BundleEntrySearchModeEnum.MATCH);
allergy1.setId("AllergyIntolerance/1");
allergy1.getCode().addCoding().setCode("123").setDisplay("Some Code");
allergy1.addReaction().addNote().setTime(new Date());
allergy1.setOnset(new DateTimeType("2020-02-03T11:22:33Z"));
AllergyIntolerance allergy2 = new AllergyIntolerance();
ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(allergy2, BundleEntrySearchModeEnum.MATCH);
allergy2.setId("AllergyIntolerance/2");
allergy2.getCode().addCoding().setCode("123").setDisplay("Some Code");
allergy2.addReaction().addNote().setTime(new Date());
allergy2.setOnset(new StringType("Some Onset"));
AllergyIntolerance allergy3 = new AllergyIntolerance();
ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(allergy3, BundleEntrySearchModeEnum.MATCH);
allergy3.setId("AllergyIntolerance/3");
allergy3.getCode().addCoding().setCode("123").setDisplay("Some Code");
allergy3.addReaction().addNote().setTime(new Date());
allergy3.setOnset(null);
IFhirResourceDao<AllergyIntolerance> allergyDao = registerResourceDaoWithNoData(AllergyIntolerance.class);
when(allergyDao.search(any(), any())).thenReturn(new SimpleBundleProvider(Lists.newArrayList(allergy1, allergy2, allergy3)));
registerRemainingResourceDaos();
// Test
Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID));
// Verify
Composition compositions = (Composition) outcome.getEntry().get(0).getResource();
Composition.SectionComponent section = findSection(compositions, IpsSectionEnum.ALLERGY_INTOLERANCE);
HtmlPage narrativeHtml = HtmlUtil.parseAsHtml(section.getText().getDivAsString());
ourLog.info("Narrative:\n{}", narrativeHtml.asXml());
DomNodeList<DomElement> tables = narrativeHtml.getElementsByTagName("table");
assertEquals(1, tables.size());
HtmlTable table = (HtmlTable) tables.get(0);
int onsetIndex = 6;
assertEquals("Onset", table.getHeader().getRows().get(0).getCell(onsetIndex).asNormalizedText());
assertEquals(new DateTimeType("2020-02-03T11:22:33Z").getValue().toString(), table.getBodies().get(0).getRows().get(0).getCell(onsetIndex).asNormalizedText());
assertEquals("Some Onset", table.getBodies().get(0).getRows().get(1).getCell(onsetIndex).asNormalizedText());
assertEquals("", table.getBodies().get(0).getRows().get(2).getCell(onsetIndex).asNormalizedText());
}
@Test
public void testAllergyIntolerance_MissingElements() throws IOException {
// Setup Patient
registerPatientDaoWithRead();
AllergyIntolerance allergy = new AllergyIntolerance();
ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(allergy, BundleEntrySearchModeEnum.MATCH);
allergy.setId("AllergyIntolerance/1");
allergy.getCode().addCoding().setCode("123").setDisplay("Some Code");
allergy.addReaction().addNote().setTime(new Date());
IFhirResourceDao<AllergyIntolerance> allergyDao = registerResourceDaoWithNoData(AllergyIntolerance.class);
when(allergyDao.search(any(), any())).thenReturn(new SimpleBundleProvider(Lists.newArrayList(allergy)));
registerRemainingResourceDaos();
// Test
Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID));
// Verify
Composition compositions = (Composition) outcome.getEntry().get(0).getResource();
Composition.SectionComponent section = findSection(compositions, IpsSectionEnum.ALLERGY_INTOLERANCE);
HtmlPage narrativeHtml = HtmlUtil.parseAsHtml(section.getText().getDivAsString());
ourLog.info("Narrative:\n{}", narrativeHtml.asXml());
DomNodeList<DomElement> tables = narrativeHtml.getElementsByTagName("table");
assertEquals(1, tables.size());
}
@Test
public void testMedicationSummary_MedicationStatementWithMedicationReference() throws IOException {
// Setup Patient
@ -196,6 +277,41 @@ public class IpsGeneratorSvcImplTest {
assertThat(row.getCell(4).asNormalizedText(), containsString("2023"));
}
@Test
public void testMedicationSummary_MedicationRequestWithNoMedication() throws IOException {
// Setup Patient
registerPatientDaoWithRead();
// Setup Medication + MedicationStatement
MedicationRequest medicationRequest = new MedicationRequest();
ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(medicationRequest, BundleEntrySearchModeEnum.MATCH);
medicationRequest.setId(MEDICATION_STATEMENT_ID);
medicationRequest.setStatus(MedicationRequest.MedicationRequestStatus.ACTIVE);
IFhirResourceDao<MedicationRequest> medicationRequestDao = registerResourceDaoWithNoData(MedicationRequest.class);
when(medicationRequestDao.search(any(), any())).thenReturn(new SimpleBundleProvider(Lists.newArrayList(medicationRequest)));
registerRemainingResourceDaos();
// Test
Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID));
// Verify
Composition compositions = (Composition) outcome.getEntry().get(0).getResource();
Composition.SectionComponent section = findSection(compositions, IpsSectionEnum.MEDICATION_SUMMARY);
HtmlPage narrativeHtml = HtmlUtil.parseAsHtml(section.getText().getDivAsString());
ourLog.info("Narrative:\n{}", narrativeHtml.asXml());
DomNodeList<DomElement> tables = narrativeHtml.getElementsByTagName("table");
assertEquals(2, tables.size());
HtmlTable table = (HtmlTable) tables.get(0);
HtmlTableRow row = table.getBodies().get(0).getRows().get(0);
assertEquals("", row.getCell(0).asNormalizedText());
assertEquals("Active", row.getCell(1).asNormalizedText());
assertEquals("", row.getCell(2).asNormalizedText());
assertEquals("", row.getCell(3).asNormalizedText());
}
@Nonnull
private Composition.SectionComponent findSection(Composition compositions, IpsSectionEnum sectionEnum) {
Composition.SectionComponent section = compositions
@ -543,4 +659,35 @@ public class IpsGeneratorSvcImplTest {
}
@Nonnull
private static List<String> toEntryResourceTypeStrings(Bundle outcome) {
return outcome
.getEntry()
.stream()
.map(t -> t.getResource().getResourceType().name())
.collect(Collectors.toList());
}
@Nonnull
private static Medication createSecondaryMedication(String medicationId) {
Medication medication = new Medication();
medication.setId(new IdType(medicationId));
medication.getCode().addCoding().setDisplay("Tylenol");
ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(medication, BundleEntrySearchModeEnum.INCLUDE);
return medication;
}
@Nonnull
private static MedicationStatement createPrimaryMedicationStatement(String medicationId, String medicationStatementId) {
MedicationStatement medicationStatement = new MedicationStatement();
medicationStatement.setId(medicationStatementId);
medicationStatement.setMedication(new Reference(medicationId));
medicationStatement.setStatus(MedicationStatement.MedicationStatementStatus.ACTIVE);
medicationStatement.getDosageFirstRep().getRoute().addCoding().setDisplay("Oral");
medicationStatement.getDosageFirstRep().setText("DAW");
medicationStatement.setEffective(new DateTimeType("2023-01-01T11:22:33Z"));
ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(medicationStatement, BundleEntrySearchModeEnum.MATCH);
return medicationStatement;
}
}

View File

@ -61,7 +61,7 @@
</div>
</div>
<div th:replace="tmpl-footer :: footer" ></div>
<div th:replace="~{tmpl-footer :: footer}" ></div>
</form>
</body>
</html>

View File

@ -182,6 +182,6 @@
</form>
<div th:replace="tmpl-footer :: footer"></div>
<div th:replace="~{tmpl-footer :: footer}"></div>
</body>
</html>

View File

@ -28,6 +28,6 @@
</div>
</div>
<div th:replace="tmpl-footer :: footer" ></div>
<div th:replace="~{tmpl-footer :: footer}" ></div>
</body>
</html>

View File

@ -129,6 +129,6 @@
</form>
<div th:replace="tmpl-footer :: footer" ></div>
<div th:replace="~{tmpl-footer :: footer}" ></div>
</body>
</html>

View File

@ -246,6 +246,6 @@
</form>
<div th:replace="tmpl-footer :: footer" ></div>
<div th:replace="~{tmpl-footer :: footer}" ></div>
</body>
</html>

View File

@ -612,7 +612,7 @@
</div>
</form>
<div th:replace="tmpl-footer :: footer" ></div>
<div th:replace="~{tmpl-footer :: footer}" ></div>
<!--
<script type="text/javascript">

View File

@ -210,6 +210,6 @@
</div>
</form>
<div th:replace="tmpl-footer :: footer" ></div>
<div th:replace="~{tmpl-footer :: footer}" ></div>
</body>
</html>