From 3452bb89a1c7e2ed5026055b8acc961cb94f7f27 Mon Sep 17 00:00:00 2001 From: Fikri Milano Date: Fri, 13 Oct 2023 17:51:35 +0700 Subject: [PATCH] LaunchContext for initialExpression (#2025) * Use launchContext for initialExpression * Test ResourceMapper * Fix test * Fix launchContexts for demo app when editing patient * spotlessApply * WIP * Revert "WIP" This reverts commit d2d53a2d5c7a92a503468a5b26f66b9691aa348e. * spotlessApply * Fix test * Refactor validateLaunchContextExtension * Remove QuestionnaireLaunchContextSet enum class * Rename vars and functions * Add code comment for MoreResourceTypes.kt * Unit testing * Fix post-merge-conflict * spotlessApply * Address review * spotlessApply * Revert un-intended changes * Fix failing checks * Remove check of must contain 2 sub-extensions * Update Kdoc * Revert * validate launch context when using populate public API --- .../fhir/catalog/DemoQuestionnaireFragment.kt | 4 +- .../fhir/datacapture/QuestionnaireFragment.kt | 13 +- .../datacapture/QuestionnaireViewModel.kt | 12 +- .../extensions/MoreQuestionnaires.kt | 88 ++-- .../datacapture/mapping/ResourceMapper.kt | 47 +-- .../datacapture/QuestionnaireViewModelTest.kt | 6 +- .../extensions/MoreQuestionnairesTest.kt | 76 ++-- .../datacapture/mapping/ResourceMapperTest.kt | 380 ++++++++++++++---- .../new-patient-registration-paginated.json | 33 +- .../android/fhir/demo/EditPatientViewModel.kt | 8 +- 10 files changed, 433 insertions(+), 234 deletions(-) diff --git a/catalog/src/main/java/com/google/android/fhir/catalog/DemoQuestionnaireFragment.kt b/catalog/src/main/java/com/google/android/fhir/catalog/DemoQuestionnaireFragment.kt index cbc0e6f49b..b9f8309e27 100644 --- a/catalog/src/main/java/com/google/android/fhir/catalog/DemoQuestionnaireFragment.kt +++ b/catalog/src/main/java/com/google/android/fhir/catalog/DemoQuestionnaireFragment.kt @@ -168,11 +168,11 @@ class DemoQuestionnaireFragment : Fragment() { R.id.container, QuestionnaireFragment.builder() .setQuestionnaire(questionnaireJsonString) - .setQuestionnaireLaunchContexts( + .setQuestionnaireLaunchContextMap( FhirContext.forR4Cached() .newJsonParser() .encodeResourceToString(Patient().apply { id = "P1" }) - .let { listOf(it) }, + .let { mapOf("patient" to it) }, ) .build(), QUESTIONNAIRE_FRAGMENT_TAG, diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireFragment.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireFragment.kt index 19cfaf8609..4bead5d1bd 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireFragment.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireFragment.kt @@ -352,10 +352,10 @@ class QuestionnaireFragment : Fragment() { * user, etc. is "in context" at the time the questionnaire response is being completed: * https://build.fhir.org/ig/HL7/sdc/StructureDefinition-sdc-questionnaire-launchContext.html * - * @param launchContexts list of serialized resources + * @param launchContextMap map of launchContext name and serialized resources */ - fun setQuestionnaireLaunchContexts(launchContexts: List) = apply { - args.add(EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_JSON_STRINGS to launchContexts) + fun setQuestionnaireLaunchContextMap(launchContextMap: Map) = apply { + args.add(EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_MAP to launchContextMap) } /** @@ -454,9 +454,10 @@ class QuestionnaireFragment : Fragment() { */ internal const val EXTRA_QUESTIONNAIRE_RESPONSE_JSON_STRING = "questionnaire-response" - /** A list of JSON encoded strings extra for each questionnaire context. */ - internal const val EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_JSON_STRINGS = - "questionnaire-launch-contexts" + /** + * A map of launchContext name and JSON encoded strings extra for each questionnaire context. + */ + internal const val EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_MAP = "questionnaire-launch-contexts" /** * A [URI][android.net.Uri] extra for streaming a JSON encoded questionnaire response. diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt index e868b66f42..8c5d551cca 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt @@ -33,6 +33,7 @@ import com.google.android.fhir.datacapture.extensions.allItems import com.google.android.fhir.datacapture.extensions.cqfExpression import com.google.android.fhir.datacapture.extensions.createQuestionnaireResponseItem import com.google.android.fhir.datacapture.extensions.entryMode +import com.google.android.fhir.datacapture.extensions.filterByCodeInNameExtension import com.google.android.fhir.datacapture.extensions.flattened import com.google.android.fhir.datacapture.extensions.hasDifferentAnswerSet import com.google.android.fhir.datacapture.extensions.isDisplayItem @@ -166,15 +167,16 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat init { questionnaireLaunchContextMap = - if (state.contains(QuestionnaireFragment.EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_JSON_STRINGS)) { + if (state.contains(QuestionnaireFragment.EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_MAP)) { - val launchContextJsonStrings: List = - state[QuestionnaireFragment.EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_JSON_STRINGS]!! + val launchContextMapString: Map = + state[QuestionnaireFragment.EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_MAP]!! - val launchContexts = launchContextJsonStrings.map { parser.parseResource(it) as Resource } + val launchContextMapResource = + launchContextMapString.mapValues { parser.parseResource(it.value) as Resource } questionnaire.questionnaireLaunchContexts?.let { launchContextExtensions -> validateLaunchContextExtensions(launchContextExtensions) - launchContexts.associateBy { it.resourceType.name.lowercase() } + filterByCodeInNameExtension(launchContextMapResource, launchContextExtensions) } } else { null diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaires.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaires.kt index 7257c4af90..82ecb6a2e7 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaires.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaires.kt @@ -16,12 +16,15 @@ package com.google.android.fhir.datacapture.extensions +import org.hl7.fhir.exceptions.FHIRException import org.hl7.fhir.r4.model.CanonicalType import org.hl7.fhir.r4.model.CodeType import org.hl7.fhir.r4.model.Coding import org.hl7.fhir.r4.model.Expression import org.hl7.fhir.r4.model.Extension import org.hl7.fhir.r4.model.Questionnaire +import org.hl7.fhir.r4.model.Resource +import org.hl7.fhir.r4.model.ResourceType /** * The StructureMap url in the @@ -64,73 +67,54 @@ internal fun Questionnaire.findVariableExpression(variableName: String): Express */ internal fun validateLaunchContextExtensions(launchContextExtensions: List) = launchContextExtensions.forEach { launchExtension -> - validateLaunchContextExtension( - Extension().apply { - addExtension(launchExtension.extension.firstOrNull { it.url == "name" }) - addExtension(launchExtension.extension.firstOrNull { it.url == "type" }) - }, - ) + validateLaunchContextExtension(launchExtension) } /** - * Checks that the extension:name extension exists and its value contains a valid code from - * [QuestionnaireLaunchContextSet] + * Verifies the existence of extension:name and extension:type with valid name system and type + * values. */ private fun validateLaunchContextExtension(launchExtension: Extension) { - check(launchExtension.extension.size == 2) { - "The extension:name or extension:type extension is missing in $EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT" - } + val nameCoding = + launchExtension.getExtensionByUrl("name")?.value as? Coding + ?: error( + "The extension:name is missing or is not of type Coding in $EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT", + ) - val isValidExtension = - QuestionnaireLaunchContextSet.values().any { - launchExtension.equalsDeep( - Extension().apply { - addExtension( - Extension().apply { - url = "name" - setValue( - Coding().apply { - code = it.code - display = it.display - system = it.system - }, - ) - }, - ) - addExtension( - Extension().apply { - url = "type" - setValue(CodeType().setValue(it.resourceType)) - }, - ) - }, + val typeCodeType = + launchExtension.getExtensionByUrl("type")?.value as? CodeType + ?: error( + "The extension:type is missing or is not of type CodeType in $EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT", ) + + val isValidResourceType = + try { + ResourceType.fromCode(typeCodeType.value) != null + } catch (exception: FHIRException) { + false } - if (!isValidExtension) { + + if (nameCoding.system != EXTENSION_LAUNCH_CONTEXT || !isValidResourceType) { error( - "The extension:name extension and/or extension:type extension do not follow the format " + - "specified in $EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT", + "The extension:name and/or extension:type do not follow the format specified in $EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT", ) } } /** - * The set of supported launch contexts, as per: http://hl7.org/fhir/uv/sdc/ValueSet/launchContext + * Filters the provided launch contexts by matching the keys with the `code` values found in the + * "name" extensions. */ -private enum class QuestionnaireLaunchContextSet( - val code: String, - val display: String, - val system: String, - val resourceType: String, -) { - PATIENT("patient", "Patient", EXTENSION_LAUNCH_CONTEXT, "Patient"), - ENCOUNTER("encounter", "Encounter", EXTENSION_LAUNCH_CONTEXT, "Encounter"), - LOCATION("location", "Location", EXTENSION_LAUNCH_CONTEXT, "Location"), - USER_AS_PATIENT("user", "User", EXTENSION_LAUNCH_CONTEXT, "Patient"), - USER_AS_PRACTITIONER("user", "User", EXTENSION_LAUNCH_CONTEXT, "Practitioner"), - USER_AS_PRACTITIONER_ROLE("user", "User", EXTENSION_LAUNCH_CONTEXT, "PractitionerRole"), - USER_AS_RELATED_PERSON("user", "User", EXTENSION_LAUNCH_CONTEXT, "RelatedPerson"), - STUDY("study", "ResearchStudy", EXTENSION_LAUNCH_CONTEXT, "ResearchStudy"), +internal fun filterByCodeInNameExtension( + launchContexts: Map, + launchContextExtensions: List, +): Map { + val nameCodes = + launchContextExtensions + .mapNotNull { extension -> (extension.getExtensionByUrl("name").value as? Coding)?.code } + .toSet() + + return launchContexts.filterKeys { nameCodes.contains(it) } } /** diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/mapping/ResourceMapper.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/mapping/ResourceMapper.kt index aaf60345d7..eeb37f4ca3 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/mapping/ResourceMapper.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/mapping/ResourceMapper.kt @@ -18,13 +18,16 @@ package com.google.android.fhir.datacapture.mapping import com.google.android.fhir.datacapture.DataCapture import com.google.android.fhir.datacapture.extensions.createQuestionnaireResponseItem +import com.google.android.fhir.datacapture.extensions.filterByCodeInNameExtension import com.google.android.fhir.datacapture.extensions.initialExpression import com.google.android.fhir.datacapture.extensions.logicalId +import com.google.android.fhir.datacapture.extensions.questionnaireLaunchContexts import com.google.android.fhir.datacapture.extensions.targetStructureMap import com.google.android.fhir.datacapture.extensions.toCodeType import com.google.android.fhir.datacapture.extensions.toCoding import com.google.android.fhir.datacapture.extensions.toIdType import com.google.android.fhir.datacapture.extensions.toUriType +import com.google.android.fhir.datacapture.extensions.validateLaunchContextExtensions import com.google.android.fhir.datacapture.fhirpath.fhirPathEngine import java.lang.reflect.Field import java.lang.reflect.Method @@ -214,13 +217,19 @@ object ResourceMapper { * Performs * [Expression-based population](http://build.fhir.org/ig/HL7/sdc/populate.html#expression-based-population) * and returns a [QuestionnaireResponse] for the [questionnaire] that is populated from the - * [resources]. + * [launchContexts]. */ suspend fun populate( questionnaire: Questionnaire, - vararg resources: Resource, + launchContexts: Map, ): QuestionnaireResponse { - populateInitialValues(questionnaire.item, *resources) + validateLaunchContextExtensions(questionnaire.questionnaireLaunchContexts ?: listOf()) + val filteredLaunchContexts = + filterByCodeInNameExtension( + launchContexts, + questionnaire.questionnaireLaunchContexts ?: listOf(), + ) + populateInitialValues(questionnaire.item, filteredLaunchContexts) return QuestionnaireResponse().apply { item = questionnaire.item.map { it.createQuestionnaireResponseItem() } } @@ -228,14 +237,14 @@ object ResourceMapper { private suspend fun populateInitialValues( questionnaireItems: List, - vararg resources: Resource, + launchContexts: Map, ) { - questionnaireItems.forEach { populateInitialValue(it, *resources) } + questionnaireItems.forEach { populateInitialValue(it, launchContexts) } } private suspend fun populateInitialValue( questionnaireItem: Questionnaire.QuestionnaireItemComponent, - vararg resources: Resource, + launchContexts: Map, ) { check(questionnaireItem.initial.isEmpty() || questionnaireItem.initialExpression == null) { "QuestionnaireItem item is not allowed to have both initial.value and initial expression. See rule at http://build.fhir.org/ig/HL7/sdc/expressions.html#initialExpression." @@ -243,12 +252,7 @@ object ResourceMapper { questionnaireItem.initialExpression ?.let { - fhirPathEngine - .evaluate( - selectPopulationContext(resources.asList(), it), - it.expression.removePrefix("%"), - ) - .singleOrNull() + fhirPathEngine.evaluate(launchContexts, null, null, null, it.expression).firstOrNull() } ?.let { // Set initial value for the questionnaire item. Questionnaire items should not have both @@ -258,24 +262,7 @@ object ResourceMapper { mutableListOf(Questionnaire.QuestionnaireItemInitialComponent().setValue(value)) } - populateInitialValues(questionnaireItem.item, *resources) - } - - /** - * Returns the population context for the questionnaire/group. - * - * The resource of the same type as the expected type of the initial expression will be selected - * first. Otherwise, the first resource in the list will be selected. - * - * TODO: rewrite this using the launch context and population context. - */ - private fun selectPopulationContext( - resources: List, - initialExpression: Expression, - ): Resource? { - val resourceType = initialExpression.expression.substringBefore(".").removePrefix("%") - return resources.singleOrNull { it.resourceType.name.lowercase() == resourceType.lowercase() } - ?: resources.firstOrNull() + populateInitialValues(questionnaireItem.item, launchContexts) } /** diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewModelTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewModelTest.kt index a9b2736ca1..234441c622 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewModelTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewModelTest.kt @@ -25,7 +25,7 @@ import ca.uhn.fhir.context.FhirContext import ca.uhn.fhir.parser.IParser import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_ENABLE_REVIEW_PAGE import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_QUESTIONNAIRE_JSON_STRING -import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_JSON_STRINGS +import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_MAP import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_QUESTIONNAIRE_RESPONSE_JSON_STRING import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_READ_ONLY import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_SHOW_CANCEL_BUTTON @@ -4410,8 +4410,8 @@ class QuestionnaireViewModelTest { } state.set(EXTRA_QUESTIONNAIRE_JSON_STRING, printer.encodeResourceToString(questionnaire)) state.set( - EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_JSON_STRINGS, - listOf(printer.encodeResourceToString(patient)), + EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_MAP, + mapOf("patient" to printer.encodeResourceToString(patient)), ) val viewModel = QuestionnaireViewModel(context, state) diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnairesTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnairesTest.kt index 13d5b21b21..d94fbca369 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnairesTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnairesTest.kt @@ -111,19 +111,15 @@ class MoreQuestionnairesTest { } @Test - fun `should throw exception if resource type in context is not part of launchContext value set`() { + fun `validateLaunchContextExtensions should throw exception if type in type extension is not a valid resource type`() { val launchContextExtension = Extension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-launchContext") .apply { addExtension( "name", - Coding( - "http://hl7.org/fhir/uv/sdc/CodeSystem/launchContext", - "observation", - "Observation", - ), + Coding("http://hl7.org/fhir/uv/sdc/CodeSystem/launchContext", "me", "Me"), ) - addExtension("type", CodeType("Observation")) + addExtension("type", CodeType("Avocado")) } val errorMessage = @@ -134,22 +130,38 @@ class MoreQuestionnairesTest { assertThat(errorMessage) .isEqualTo( - "The extension:name extension and/or extension:type extension do not follow " + - "the format specified in http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-launchContext", + "The extension:name and/or extension:type do not follow the format specified in $EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT", ) } @Test - fun `should throw exception if resource type in type extension is different to what is in name extension`() { + fun `validateLaunchContextExtensions should throw exception if system in name extension is not valid`() { val launchContextExtension = Extension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-launchContext") .apply { - addExtension( - "name", - Coding("http://hl7.org/fhir/uv/sdc/CodeSystem/launchContext", "encounter", "Encounter"), - ) + addExtension("name", Coding("http://wrong-system", "grandma", "Grandma")) addExtension("type", CodeType("Patient")) } + val errorMessage = + assertFailsWith { + validateLaunchContextExtensions(listOf(launchContextExtension)) + } + .localizedMessage + + assertThat(errorMessage) + .isEqualTo( + "The extension:name and/or extension:type do not follow the format specified in $EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT", + ) + } + + @Test + fun `validateLaunchContextExtensions should throw exception if the type extension has wrong url`() { + val launchContextExtension = + Extension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-launchContext") + .apply { + addExtension("name", Coding("http://wrong-system", "grandma", "Grandma")) + addExtension("waitwhat", StringType("Patient")) + } val errorMessage = assertFailsWith { @@ -159,21 +171,17 @@ class MoreQuestionnairesTest { assertThat(errorMessage) .isEqualTo( - "The extension:name extension and/or extension:type extension do not follow " + - "the format specified in http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-launchContext", + "The extension:type is missing or is not of type CodeType in $EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT", ) } @Test - fun `should throw exception if type extension is not a subset of User value set in name extension`() { + fun `validateLaunchContextExtensions should throw exception if the type extension value is not CodeType`() { val launchContextExtension = Extension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-launchContext") .apply { - addExtension( - "name", - Coding("http://hl7.org/fhir/uv/sdc/CodeSystem/launchContext", "user", "User"), - ) - addExtension("type", CodeType("Observation")) + addExtension("name", Coding("http://wrong-system", "grandma", "Grandma")) + addExtension("type", StringType("Patient")) } val errorMessage = @@ -184,20 +192,20 @@ class MoreQuestionnairesTest { assertThat(errorMessage) .isEqualTo( - "The extension:name extension and/or extension:type extension do not follow " + - "the format specified in http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-launchContext", + "The extension:type is missing or is not of type CodeType in $EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT", ) } @Test - fun `should throw exception if the type extension is not present`() { + fun `validateLaunchContextExtensions should throw exception if the name extension has wrong url`() { val launchContextExtension = Extension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-launchContext") .apply { addExtension( - "name", - Coding("http://hl7.org/fhir/uv/sdc/CodeSystem/launchContext", "user", "User"), + "waitwhat", + Coding("http://hl7.org/fhir/uv/sdc/CodeSystem/launchContext", "grandma", "Grandma"), ) + addExtension("type", CodeType("Patient")) } val errorMessage = @@ -208,20 +216,17 @@ class MoreQuestionnairesTest { assertThat(errorMessage) .isEqualTo( - "The extension:name or extension:type extension is missing in " + - EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT, + "The extension:name is missing or is not of type Coding in $EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT", ) } @Test - fun `should throw exception if the name extension is not present`() { + fun `validateLaunchContextExtensions should throw exception if the name extension value is not Coding`() { val launchContextExtension = Extension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-launchContext") .apply { - addExtension( - "name", - Coding("http://hl7.org/fhir/uv/sdc/CodeSystem/launchContext", "user", "User"), - ) + addExtension("name", StringType("waitwhat")) + addExtension("type", CodeType("Patient")) } val errorMessage = @@ -232,8 +237,7 @@ class MoreQuestionnairesTest { assertThat(errorMessage) .isEqualTo( - "The extension:name or extension:type extension is missing in " + - EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT, + "The extension:name is missing or is not of type Coding in $EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT", ) } } diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/mapping/ResourceMapperTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/mapping/ResourceMapperTest.kt index fefc484dde..4c88f02408 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/mapping/ResourceMapperTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/mapping/ResourceMapperTest.kt @@ -22,6 +22,8 @@ import androidx.test.core.app.ApplicationProvider import ca.uhn.fhir.context.FhirContext import ca.uhn.fhir.context.FhirVersionEnum import ca.uhn.fhir.parser.IParser +import com.google.android.fhir.datacapture.extensions.EXTENSION_LAUNCH_CONTEXT +import com.google.android.fhir.datacapture.extensions.EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT import com.google.android.fhir.datacapture.extensions.ITEM_INITIAL_EXPRESSION_URL import com.google.android.fhir.datacapture.views.factories.localDate import com.google.common.truth.Truth.assertThat @@ -33,24 +35,23 @@ import kotlin.test.assertFailsWith import kotlinx.coroutines.runBlocking import org.hl7.fhir.exceptions.FHIRException import org.hl7.fhir.r4.model.Address -import org.hl7.fhir.r4.model.Annotation import org.hl7.fhir.r4.model.Base import org.hl7.fhir.r4.model.BooleanType +import org.hl7.fhir.r4.model.CodeType import org.hl7.fhir.r4.model.Coding import org.hl7.fhir.r4.model.ContactPoint import org.hl7.fhir.r4.model.DateType +import org.hl7.fhir.r4.model.Encounter import org.hl7.fhir.r4.model.Enumerations import org.hl7.fhir.r4.model.Expression import org.hl7.fhir.r4.model.Extension import org.hl7.fhir.r4.model.HumanName import org.hl7.fhir.r4.model.Immunization -import org.hl7.fhir.r4.model.MarkdownType import org.hl7.fhir.r4.model.Observation import org.hl7.fhir.r4.model.Patient import org.hl7.fhir.r4.model.Questionnaire import org.hl7.fhir.r4.model.QuestionnaireResponse import org.hl7.fhir.r4.model.Reference -import org.hl7.fhir.r4.model.RelatedPerson import org.hl7.fhir.r4.model.ResourceFactory import org.hl7.fhir.r4.model.StringType import org.hl7.fhir.r4.model.codesystems.AdministrativeGender @@ -811,6 +812,16 @@ class ResourceMapperTest { runBlocking { val questionnaire = Questionnaire() + .apply { + addExtension().apply { + url = EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT + extension = + listOf( + Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "mother", "Mother")), + Extension("type", CodeType("Patient")), + ) + } + } .addItem( Questionnaire.QuestionnaireItemComponent().apply { linkId = "patient-dob" @@ -830,7 +841,7 @@ class ResourceMapperTest { val patientId = UUID.randomUUID().toString() val patient = Patient().apply { id = "Patient/$patientId/_history/2" } - val questionnaireResponse = ResourceMapper.populate(questionnaire, patient) + val questionnaireResponse = ResourceMapper.populate(questionnaire, mapOf("mother" to patient)) assertThat((questionnaireResponse.item[0].answer[0].value as DateType).localDate) .isEqualTo((DateType(Date())).localDate) @@ -1187,6 +1198,25 @@ class ResourceMapperTest { { "resourceType": "Questionnaire", "id": "client-registration-sample", + "extension": [ + { + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-launchContext", + "extension": [ + { + "url": "name", + "valueCoding": { + "system": "http://hl7.org/fhir/uv/sdc/CodeSystem/launchContext", + "code": "father", + "display": "Patient" + } + }, + { + "url": "type", + "valueCode": "Patient" + } + ] + } + ], "status": "active", "date": "2020-11-18T07:24:47.111Z", "subjectType": [ @@ -1208,7 +1238,7 @@ class ResourceMapperTest { "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression", "valueExpression": { "language": "text/fhirpath", - "expression": "Patient.name.given", + "expression": "%father.name.given", "name": "patientName" } } @@ -1224,7 +1254,7 @@ class ResourceMapperTest { "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression", "valueExpression": { "language": "text/fhirpath", - "expression": "Patient.name.family", + "expression": "%father.name.family", "name": "patientFamily" } } @@ -1242,7 +1272,7 @@ class ResourceMapperTest { "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression", "valueExpression": { "language": "text/fhirpath", - "expression": "Patient.birthDate", + "expression": "%father.birthDate", "name": "patientBirthDate" } } @@ -1259,7 +1289,7 @@ class ResourceMapperTest { "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression", "valueExpression": { "language": "text/fhirpath", - "expression": "Patient.gender.value", + "expression": "%father.gender.value", "name": "patientGender" } } @@ -1297,7 +1327,7 @@ class ResourceMapperTest { "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression", "valueExpression": { "language": "text/fhirpath", - "expression": "Patient.telecom.value", + "expression": "%father.telecom.value", "name": "patientTelecom" } } @@ -1320,7 +1350,7 @@ class ResourceMapperTest { "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression", "valueExpression": { "language": "text/fhirpath", - "expression": "Patient.address.city", + "expression": "%father.address.city", "name": "patientCity" } } @@ -1336,7 +1366,7 @@ class ResourceMapperTest { "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression", "valueExpression": { "language": "text/fhirpath", - "expression": "Patient.address.country", + "expression": "%father.address.country", "name": "patientCity" } } @@ -1354,7 +1384,7 @@ class ResourceMapperTest { "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression", "valueExpression": { "language": "text/fhirpath", - "expression": "Patient.active", + "expression": "%father.active", "name": "patientActive" } } @@ -1376,7 +1406,7 @@ class ResourceMapperTest { as Questionnaire val patient = createPatientResource() - val response = ResourceMapper.populate(uriTestQuestionnaire, patient) + val response = ResourceMapper.populate(uriTestQuestionnaire, mapOf("father" to patient)) val responseItem = response.item[0] assertThat(((responseItem.item[0].item[0].answer[0]).value as StringType).valueAsString) @@ -1400,29 +1430,44 @@ class ResourceMapperTest { @Test fun `populate() should fill QuestionnaireResponse with values when given multiple Resources`() = runBlocking { - val relatedPerson = - RelatedPerson().apply { - name = - listOf( - HumanName().apply { - given = listOf(StringType("John")) - family = "Doe" - }, - ) - birthDate = "1990-05-20".toDateFromFormatYyyyMmDd() - } - - val observation = - Observation().apply { - value = StringType("Allergic to dairy products and proteins") - note = listOf(Annotation(MarkdownType("Patient Registration Comments"))) - } - val questionnaire = Questionnaire() + .apply { + addExtension().apply { + url = EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT + extension = + listOf( + Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "father", "Father")), + Extension("type", CodeType("Patient")), + ) + } + addExtension().apply { + url = EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT + extension = + listOf( + Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "mother", "Mother")), + Extension("type", CodeType("Patient")), + ) + } + addExtension().apply { + url = EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT + extension = + listOf( + Extension( + "name", + Coding( + EXTENSION_LAUNCH_CONTEXT, + "registration-encounter", + "Registration Encounter", + ), + ), + Extension("type", CodeType("Encounter")), + ) + } + } .addItem( Questionnaire.QuestionnaireItemComponent().apply { - linkId = "first-name" + linkId = "first-name-father" type = Questionnaire.QuestionnaireItemType.TEXT extension = listOf( @@ -1430,7 +1475,7 @@ class ResourceMapperTest { ITEM_INITIAL_EXPRESSION_URL, Expression().apply { language = "text/fhirpath" - expression = "Patient.name.given" + expression = "%father.name.given" }, ), ) @@ -1438,38 +1483,15 @@ class ResourceMapperTest { ) .addItem( Questionnaire.QuestionnaireItemComponent().apply { - linkId = "related-person-details" - type = Questionnaire.QuestionnaireItemType.GROUP - item = - listOf( - Questionnaire.QuestionnaireItemComponent().apply { - linkId = "rp-family-name" - type = Questionnaire.QuestionnaireItemType.TEXT - extension = - listOf( - Extension( - ITEM_INITIAL_EXPRESSION_URL, - Expression().apply { - language = "text/fhirpath" - expression = "RelatedPerson.name.family" - }, - ), - ) - }, - ) - }, - ) - .addItem( - Questionnaire.QuestionnaireItemComponent().apply { - linkId = "related-person-dob" - type = Questionnaire.QuestionnaireItemType.DATE + linkId = "first-name-mother" + type = Questionnaire.QuestionnaireItemType.TEXT extension = listOf( Extension( ITEM_INITIAL_EXPRESSION_URL, Expression().apply { language = "text/fhirpath" - expression = "RelatedPerson.birthDate" + expression = "%mother.name.given" }, ), ) @@ -1477,7 +1499,7 @@ class ResourceMapperTest { ) .addItem( Questionnaire.QuestionnaireItemComponent().apply { - linkId = "comments" + linkId = "encounter-reason" type = Questionnaire.QuestionnaireItemType.TEXT extension = listOf( @@ -1485,33 +1507,140 @@ class ResourceMapperTest { ITEM_INITIAL_EXPRESSION_URL, Expression().apply { language = "text/fhirpath" - expression = "Observation.value" + expression = "%registration-encounter.reasonCode[0].text" }, ), ) }, ) - val patient = createPatientResource() + val patientFather = + Patient().apply { + active = true + gender = Enumerations.AdministrativeGender.MALE + name = listOf(HumanName().apply { given = mutableListOf(StringType("Salman")) }) + } + + val patientMother = + Patient().apply { + active = true + gender = Enumerations.AdministrativeGender.FEMALE + name = listOf(HumanName().apply { given = mutableListOf(StringType("Fatima")) }) + } + + val encounter = + Encounter().apply { + addReasonCode().apply { addCoding().apply { text = "Registration Task" } } + } + val questionnaireResponse = - ResourceMapper.populate(questionnaire, patient, relatedPerson, observation) + ResourceMapper.populate( + questionnaire, + mapOf( + "father" to patientFather, + "mother" to patientMother, + "registration-encounter" to encounter, + ), + ) assertThat((questionnaireResponse.item[0].answer[0].value as StringType).valueAsString) .isEqualTo("Salman") - assertThat( - ((questionnaireResponse.item[1].item[0].answer[0]).value as StringType).valueAsString, + assertThat(((questionnaireResponse.item[1].answer[0]).value as StringType).valueAsString) + .isEqualTo("Fatima") + assertThat(((questionnaireResponse.item[2].answer[0]).value as StringType).valueAsString) + .isEqualTo("Registration Task") + } + + @Test + fun `populate() should not fill QuestionnaireResponse with values if the intended launch context extension is not declared`(): + Unit = runBlocking { + val questionnaire = + Questionnaire() + .apply { + addExtension().apply { + url = EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT + extension = + listOf( + Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "father", "Father")), + Extension("type", CodeType("Patient")), + ) + } + } + .addItem( + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "first-name-father" + type = Questionnaire.QuestionnaireItemType.TEXT + extension = + listOf( + Extension( + ITEM_INITIAL_EXPRESSION_URL, + Expression().apply { + language = "text/fhirpath" + expression = "%father.name.given" + }, + ), + ) + }, + ) + .addItem( + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "first-name-mother" + type = Questionnaire.QuestionnaireItemType.TEXT + extension = + listOf( + Extension( + ITEM_INITIAL_EXPRESSION_URL, + Expression().apply { + language = "text/fhirpath" + expression = "%mother.name.given" + }, + ), + ) + }, ) - .isEqualTo("Doe") - assertThat(((questionnaireResponse.item[2].answer[0]).value as DateType).valueAsString) - .isEqualTo("1990-05-20") - assertThat(((questionnaireResponse.item[3].answer[0]).value as StringType).valueAsString) - .isEqualTo("Allergic to dairy products and proteins") + + val patientFather = + Patient().apply { + active = true + gender = Enumerations.AdministrativeGender.MALE + name = listOf(HumanName().apply { given = mutableListOf(StringType("Salman")) }) + } + + val patientMother = + Patient().apply { + active = true + gender = Enumerations.AdministrativeGender.FEMALE + name = listOf(HumanName().apply { given = mutableListOf(StringType("Fatima")) }) + } + + val questionnaireResponse = + ResourceMapper.populate( + questionnaire, + mapOf("father" to patientFather, "mother" to patientMother), + ) + + assertThat((questionnaireResponse.item[0].answer[0].value as StringType).valueAsString) + .isEqualTo("Salman") + assertFailsWith { + assertThat(((questionnaireResponse.item[1].answer[0]).value as StringType).valueAsString) + .isEqualTo("Fatima") } + } @Test fun `populate() should correctly populate IdType value in QuestionnaireResponse`() = runBlocking { val questionnaire = Questionnaire() + .apply { + addExtension().apply { + url = EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT + extension = + listOf( + Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "father", "Father")), + Extension("type", CodeType("Patient")), + ) + } + } .addItem( Questionnaire.QuestionnaireItemComponent().apply { linkId = "patient-id" @@ -1522,7 +1651,7 @@ class ResourceMapperTest { ITEM_INITIAL_EXPRESSION_URL, Expression().apply { language = "text/fhirpath" - expression = "Patient.id" + expression = "%father.id" }, ), ) @@ -1531,7 +1660,7 @@ class ResourceMapperTest { val patientId = UUID.randomUUID().toString() val patient = Patient().apply { id = "Patient/$patientId" } - val questionnaireResponse = ResourceMapper.populate(questionnaire, patient) + val questionnaireResponse = ResourceMapper.populate(questionnaire, mapOf("father" to patient)) assertThat((questionnaireResponse.item[0].answer[0].value as StringType).value) .isEqualTo(patientId) @@ -1542,6 +1671,16 @@ class ResourceMapperTest { runBlocking { val questionnaire = Questionnaire() + .apply { + addExtension().apply { + url = EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT + extension = + listOf( + Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "father", "Father")), + Extension("type", CodeType("Patient")), + ) + } + } .addItem( Questionnaire.QuestionnaireItemComponent().apply { linkId = "patient-id" @@ -1552,7 +1691,7 @@ class ResourceMapperTest { ITEM_INITIAL_EXPRESSION_URL, Expression().apply { language = "text/fhirpath" - expression = "Patient.id" + expression = "%father.id" }, ), ) @@ -1561,7 +1700,7 @@ class ResourceMapperTest { val patientId = UUID.randomUUID().toString() val patient = Patient().apply { id = "Patient/$patientId" } - val questionnaireResponse = ResourceMapper.populate(questionnaire, patient) + val questionnaireResponse = ResourceMapper.populate(questionnaire, mapOf("father" to patient)) assertThat((questionnaireResponse.item[0].answer[0].value as Reference).reference) .isEqualTo(patient.idPart) @@ -1572,6 +1711,16 @@ class ResourceMapperTest { runBlocking { val questionnaire = Questionnaire() + .apply { + addExtension().apply { + url = EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT + extension = + listOf( + Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "father", "Father")), + Extension("type", CodeType("Patient")), + ) + } + } .addItem( Questionnaire.QuestionnaireItemComponent().apply { linkId = "patient-id" @@ -1582,7 +1731,7 @@ class ResourceMapperTest { ITEM_INITIAL_EXPRESSION_URL, Expression().apply { language = "text/fhirpath" - expression = "Patient.gender" + expression = "%father.gender" }, ), ) @@ -1597,7 +1746,9 @@ class ResourceMapperTest { } val errorMessage = - assertFailsWith { ResourceMapper.populate(questionnaire, patient) } + assertFailsWith { + ResourceMapper.populate(questionnaire, mapOf(Pair("father", patient))) + } .localizedMessage assertThat(errorMessage).isEqualTo("Expression supplied does not evaluate to IdType.") } @@ -1607,6 +1758,16 @@ class ResourceMapperTest { runBlocking { val questionnaire = Questionnaire() + .apply { + addExtension().apply { + url = EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT + extension = + listOf( + Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "patient", "Patient")), + Extension("type", CodeType("Patient")), + ) + } + } .addItem( Questionnaire.QuestionnaireItemComponent().apply { type = Questionnaire.QuestionnaireItemType.REFERENCE @@ -1615,17 +1776,18 @@ class ResourceMapperTest { ITEM_INITIAL_EXPRESSION_URL, Expression().apply { language = "text/fhirpath" - expression = "Patient" + expression = "%patient.id" }, ), ) }, ) val patient = Patient().apply { id = UUID.randomUUID().toString() } - val questionnaireResponse = ResourceMapper.populate(questionnaire, patient) + val questionnaireResponse = + ResourceMapper.populate(questionnaire, mapOf("patient" to patient)) assertThat(questionnaireResponse.itemFirstRep.answerFirstRep.valueReference.reference) - .isEqualTo("Patient/${patient.id}") + .isEqualTo(patient.id) } @Test @@ -1633,6 +1795,16 @@ class ResourceMapperTest { runBlocking { val questionnaire = Questionnaire() + .apply { + addExtension().apply { + url = EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT + extension = + listOf( + Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "father", "Father")), + Extension("type", CodeType("Patient")), + ) + } + } .addItem( Questionnaire.QuestionnaireItemComponent().apply { linkId = "patient-id" @@ -1643,7 +1815,7 @@ class ResourceMapperTest { ITEM_INITIAL_EXPRESSION_URL, Expression().apply { language = "text/fhirpath" - expression = "Patient.id" + expression = "%father.id" }, ), ) @@ -1652,7 +1824,7 @@ class ResourceMapperTest { val patientId = UUID.randomUUID().toString() val patient = Patient().apply { id = "Patient/$patientId/_history/2" } - val questionnaireResponse = ResourceMapper.populate(questionnaire, patient) + val questionnaireResponse = ResourceMapper.populate(questionnaire, mapOf("father" to patient)) assertThat((questionnaireResponse.item[0].answer[0].value as StringType).value) .isEqualTo(patientId) @@ -1663,6 +1835,16 @@ class ResourceMapperTest { runBlocking { val questionnaire = Questionnaire() + .apply { + addExtension().apply { + url = EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT + extension = + listOf( + Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "mother", "Mother")), + Extension("type", CodeType("Patient")), + ) + } + } .addItem( Questionnaire.QuestionnaireItemComponent().apply { linkId = "patient-gender" @@ -1673,7 +1855,7 @@ class ResourceMapperTest { ITEM_INITIAL_EXPRESSION_URL, Expression().apply { language = "text/fhirpath" - expression = "Patient.gender" + expression = "%mother.gender" }, ), ) @@ -1696,7 +1878,7 @@ class ResourceMapperTest { ) val patient = Patient().apply { gender = Enumerations.AdministrativeGender.FEMALE } - val questionnaireResponse = ResourceMapper.populate(questionnaire, patient) + val questionnaireResponse = ResourceMapper.populate(questionnaire, mapOf("mother" to patient)) assertThat((questionnaireResponse.item[0].answer[0].value as Coding).code).isEqualTo("female") assertThat((questionnaireResponse.item[0].answer[0].value as Coding).display) @@ -1707,6 +1889,16 @@ class ResourceMapperTest { fun `populate() should populate nested non-group questions`() = runBlocking { val questionnaire = Questionnaire() + .apply { + addExtension().apply { + url = EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT + extension = + listOf( + Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "mother", "Mother")), + Extension("type", CodeType("Patient")), + ) + } + } .addItem( Questionnaire.QuestionnaireItemComponent().apply { linkId = "patient-gender" @@ -1717,7 +1909,7 @@ class ResourceMapperTest { ITEM_INITIAL_EXPRESSION_URL, Expression().apply { language = "text/fhirpath" - expression = "Patient.gender" + expression = "%mother.gender" }, ), ) @@ -1747,7 +1939,7 @@ class ResourceMapperTest { ITEM_INITIAL_EXPRESSION_URL, Expression().apply { language = "text/fhirpath" - expression = "Patient.id" + expression = "%mother.id" }, ), ) @@ -1762,7 +1954,7 @@ class ResourceMapperTest { gender = Enumerations.AdministrativeGender.FEMALE id = "Patient/$patientId/_history/2" } - val questionnaireResponse = ResourceMapper.populate(questionnaire, patient) + val questionnaireResponse = ResourceMapper.populate(questionnaire, mapOf("mother" to patient)) assertThat((questionnaireResponse.item[0].answer[0].value as Coding).code).isEqualTo("female") assertThat((questionnaireResponse.item[0].answer[0].value as Coding).display) @@ -2649,6 +2841,16 @@ class ResourceMapperTest { Unit = runBlocking { val questionnaire = Questionnaire() + .apply { + addExtension().apply { + url = EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT + extension = + listOf( + Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "father", "Father")), + Extension("type", CodeType("Patient")), + ) + } + } .addItem( Questionnaire.QuestionnaireItemComponent().apply { linkId = "patient-gender" @@ -2659,7 +2861,7 @@ class ResourceMapperTest { ITEM_INITIAL_EXPRESSION_URL, Expression().apply { language = "text/fhirpath" - expression = "Patient.gender" + expression = "%father.gender" }, ), ) @@ -2669,7 +2871,9 @@ class ResourceMapperTest { val patient = Patient().apply { gender = Enumerations.AdministrativeGender.MALE } val errorMessage = - assertFailsWith { ResourceMapper.populate(questionnaire, patient) } + assertFailsWith { + ResourceMapper.populate(questionnaire, mapOf("father" to patient)) + } .localizedMessage assertThat(errorMessage) .isEqualTo( diff --git a/demo/src/main/assets/new-patient-registration-paginated.json b/demo/src/main/assets/new-patient-registration-paginated.json index 066ba2c778..324e5b4f0c 100644 --- a/demo/src/main/assets/new-patient-registration-paginated.json +++ b/demo/src/main/assets/new-patient-registration-paginated.json @@ -8,6 +8,23 @@ "Patient" ], "extension": [ + { + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-launchContext", + "extension": [ + { + "url": "name", + "valueCoding": { + "system": "http://hl7.org/fhir/uv/sdc/CodeSystem/launchContext", + "code": "client", + "display": "Client as a Patient" + } + }, + { + "url": "type", + "valueCode": "Patient" + } + ] + }, { "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemExtractionContext", "valueExpression": { @@ -51,7 +68,7 @@ "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression", "valueExpression": { "language": "text/fhirpath", - "expression": "Patient.name.given", + "expression": "%client.name.given", "name": "patientName" } } @@ -85,7 +102,7 @@ "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression", "valueExpression": { "language": "text/fhirpath", - "expression": "Patient.name.family", + "expression": "%client.name.family", "name": "patientFamily" } } @@ -122,7 +139,7 @@ "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression", "valueExpression": { "language": "text/fhirpath", - "expression": "Patient.birthDate", + "expression": "%client.birthDate", "name": "patientBirthDate" } } @@ -161,7 +178,7 @@ "valueExpression": { "system": "http://hl7.org/fhir/administrative-gender", "language": "text/fhirpath", - "expression": "Patient.gender", + "expression": "%client.gender", "name": "patientGender" } }, @@ -261,7 +278,7 @@ "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression", "valueExpression": { "language": "text/fhirpath", - "expression": "Patient.telecom.value", + "expression": "%client.telecom.value", "name": "patientTelecom" } } @@ -302,7 +319,7 @@ "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression", "valueExpression": { "language": "text/fhirpath", - "expression": "Patient.address.city", + "expression": "%client.address.city", "name": "patientCity" } } @@ -336,7 +353,7 @@ "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression", "valueExpression": { "language": "text/fhirpath", - "expression": "Patient.address.country", + "expression": "%client.address.country", "name": "patientCity" } } @@ -372,7 +389,7 @@ "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression", "valueExpression": { "language": "text/fhirpath", - "expression": "Patient.active", + "expression": "%client.active", "name": "patientActive" } } diff --git a/demo/src/main/java/com/google/android/fhir/demo/EditPatientViewModel.kt b/demo/src/main/java/com/google/android/fhir/demo/EditPatientViewModel.kt index 3517854f1b..456f480cf9 100644 --- a/demo/src/main/java/com/google/android/fhir/demo/EditPatientViewModel.kt +++ b/demo/src/main/java/com/google/android/fhir/demo/EditPatientViewModel.kt @@ -31,6 +31,7 @@ import kotlinx.coroutines.launch import org.hl7.fhir.r4.model.Patient import org.hl7.fhir.r4.model.Questionnaire import org.hl7.fhir.r4.model.QuestionnaireResponse +import org.hl7.fhir.r4.model.Resource /** * The ViewModel helper class for [EditPatientFragment], that is responsible for preparing data for @@ -45,14 +46,13 @@ class EditPatientViewModel(application: Application, private val state: SavedSta private suspend fun prepareEditPatient(): Pair { val patient = fhirEngine.get(patientId) + val launchContexts = mapOf("client" to patient) val question = readFileFromAssets("new-patient-registration-paginated.json").trimIndent() val parser = FhirContext.forCached(FhirVersionEnum.R4).newJsonParser() - val questionnaire = - parser.parseResource(org.hl7.fhir.r4.model.Questionnaire::class.java, question) - as Questionnaire + val questionnaire = parser.parseResource(Questionnaire::class.java, question) as Questionnaire val questionnaireResponse: QuestionnaireResponse = - ResourceMapper.populate(questionnaire, patient) + ResourceMapper.populate(questionnaire, launchContexts) val questionnaireResponseJson = parser.encodeResourceToString(questionnaireResponse) return question to questionnaireResponseJson }