Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
8bb8fca
implement answer-option init-selected
maimoonak Jun 12, 2023
0cf7a5a
Add catalog example
maimoonak Jun 13, 2023
e3dbe9e
Fix test failures
maimoonak Jun 13, 2023
ab3b1a8
Merge branch 'master' into option-init-selected
maimoonak Jun 21, 2023
6ac7e50
Merge branch 'master' into option-init-selected
maimoonak Jun 27, 2023
df31254
Merge branch 'master' into option-init-selected
maimoonak Jul 24, 2023
7b84061
Refactor initial answer component handling
maimoonak Jul 24, 2023
a6b2069
Merge branch 'option-init-selected' of https://github.com/opensrp/and…
maimoonak Jul 24, 2023
7479178
spotless apply
maimoonak Jul 25, 2023
7fde893
Merge branch 'master' into option-init-selected
maimoonak Aug 8, 2023
eaf0f92
Merge branch 'master' into option-init-selected
maimoonak Aug 22, 2023
7478363
Merge branch 'master' into option-init-selected
f-odhiambo Aug 31, 2023
a9297e7
Merge branch 'master' into option-init-selected
maimoonak Sep 11, 2023
e9d0c14
Update datacapture/src/main/java/com/google/android/fhir/datacapture/…
maimoonak Sep 11, 2023
7c84b19
Fix initial and initialSelected Rule
maimoonak Sep 11, 2023
a5213ad
Update datacapture/src/test/java/com/google/android/fhir/datacapture/…
maimoonak Sep 12, 2023
f28a1fd
Merge branch 'master' into option-init-selected
maimoonak Sep 12, 2023
42c149a
Update tests to handle cases separately
maimoonak Sep 12, 2023
a8ad945
Merge branch 'master' into option-init-selected
maimoonak Sep 12, 2023
58d08a0
Update datacapture/src/test/java/com/google/android/fhir/datacapture/…
jingtang10 Sep 12, 2023
f1437c5
Inline variables in test
jingtang10 Sep 12, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions catalog/src/main/assets/component_initial_value.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
{
"resourceType": "Questionnaire",
"item": [
{
"linkId": "1.0",
"type": "choice",
"text": "Initially selected single choice",
"answerOption": [
{
"valueCoding": {
"code": "Y",
"display": "Yes",
"system": "custom"
}
},
{
"valueCoding": {
"code": "N",
"display": "No",
"system": "custom"
},
"initialSelected": true
},
{
"valueCoding": {
"code": "unknown",
"display": "Unknown",
"system": "custom"
}
}
]
},
{
"linkId": "2.0",
"type": "choice",
"text": "Initially selected multiple choice",
"repeats": true,
"answerOption": [
{
"valueCoding": {
"code": "1st",
"display": "First",
"system": "custom"
}
},
{
"valueCoding": {
"code": "2nd",
"display": "Second",
"system": "custom"
},
"initialSelected": true
},
{
"valueCoding": {
"code": "3rd",
"display": "Third",
"system": "custom"
},
"initialSelected": true
}
]
},
{
"linkId": "3.0",
"type": "string",
"text": "Initially provided text value",
"initial": [
{
"valueString": "Here is a sample text"
}
]
},
{
"linkId": "4.0",
"type": "boolean",
"text": "Initially provided boolean value",
"initial": [
{
"valueBoolean": false
}
]
},
{
"linkId": "5.0",
"type": "date",
"text": "Initially provided date value",
"initial": [
{
"valueDate": "2022-01-22"
}
]
},
{
"linkId": "6.0",
"type": "quantity",
"text": "Initially provided quantity value",
"initial": [
{
"valueQuantity": {
"value": 30,
"unit": "$",
"system": "http://measureunit.org",
"code": "USD"
}
}
]
},
{
"linkId": "7.0",
"type": "quantity",
"text": "Initially provided quantity unit only",
"initial": [
{
"valueQuantity": {
"unit": "$",
"system": "http://measureunit.org",
"code": "USD"
}
}
]
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ class ComponentListViewModel(application: Application, private val state: SavedS
R.drawable.ic_item_answer_media,
R.string.component_name_item_answer_media,
""
),
INITIAL_VALUE(
R.drawable.ic_initial_value_component,
R.string.component_name_initial_value,
"component_initial_value.json"
)
}

Expand All @@ -164,5 +169,6 @@ class ComponentListViewModel(application: Application, private val state: SavedS
ViewItem.ComponentItem(Component.HELP),
ViewItem.ComponentItem(Component.ITEM_MEDIA),
ViewItem.ComponentItem(Component.ITEM_ANSWER_MEDIA),
ViewItem.ComponentItem(Component.INITIAL_VALUE),
)
}
23 changes: 23 additions & 0 deletions catalog/src/main/res/drawable/ic_initial_value_component.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="194dp"
android:height="128dp"
android:viewportWidth="194"
android:viewportHeight="128"
>
<group>
<clip-path
android:pathData="M0,64C0,49.641 11.641,38 26,38H194V90H26C11.641,90 0,78.359 0,64V64Z"
/>
<path
android:pathData="M66,52C66,48.134 69.134,45 73,45L195,45V83L73,83C69.134,83 66,79.866 66,76V52Z"
android:strokeWidth="2"
android:fillColor="#ffffff"
android:strokeColor="#4285F4"
/>
<path
android:pathData="M90.086,70V58.57H86.468V57.112H95.216V58.57H91.616V70H90.086ZM99.714,70.288C98.826,70.288 98.034,70.078 97.338,69.658C96.654,69.238 96.114,68.662 95.718,67.93C95.334,67.198 95.142,66.364 95.142,65.428C95.142,64.552 95.322,63.742 95.682,62.998C96.054,62.254 96.57,61.66 97.23,61.216C97.902,60.76 98.688,60.532 99.588,60.532C100.5,60.532 101.28,60.736 101.928,61.144C102.588,61.54 103.092,62.092 103.44,62.8C103.8,63.508 103.98,64.318 103.98,65.23C103.98,65.314 103.974,65.398 103.962,65.482C103.962,65.566 103.956,65.638 103.944,65.698H96.672C96.72,66.418 96.894,67.018 97.194,67.498C97.506,67.966 97.89,68.32 98.346,68.56C98.802,68.788 99.276,68.902 99.768,68.902C100.44,68.902 100.992,68.746 101.424,68.434C101.868,68.11 102.222,67.714 102.486,67.246L103.782,67.876C103.422,68.572 102.906,69.148 102.234,69.604C101.562,70.06 100.722,70.288 99.714,70.288ZM99.588,61.918C98.844,61.918 98.226,62.152 97.734,62.62C97.242,63.088 96.918,63.694 96.762,64.438H102.36C102.348,64.09 102.246,63.724 102.054,63.34C101.874,62.944 101.586,62.608 101.19,62.332C100.794,62.056 100.26,61.918 99.588,61.918ZM104.945,70L108.185,65.338L105.017,60.82H106.745L109.121,64.222L111.389,60.82H113.189L109.967,65.338L113.243,70H111.461L109.085,66.472L106.727,70H104.945ZM115.838,67.588V62.206H114.236V60.82H115.838V58.228H117.368V60.82H119.618V62.206H117.368V67.21C117.368,67.69 117.464,68.062 117.656,68.326C117.86,68.59 118.19,68.722 118.646,68.722C118.85,68.722 119.036,68.692 119.204,68.632C119.372,68.572 119.522,68.5 119.654,68.416V69.91C119.498,69.982 119.324,70.036 119.132,70.072C118.952,70.12 118.706,70.144 118.394,70.144C117.626,70.144 117.008,69.922 116.54,69.478C116.072,69.022 115.838,68.392 115.838,67.588Z"
android:fillColor="#1A73E8"
/>
</group>
</vector>
1 change: 1 addition & 0 deletions catalog/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
<string
name="behavior_name_dynamic_question_text"
>Dynamic question text</string>
<string name="component_name_initial_value">Initial Value</string>
<string
name="behavior_name_calculated_expression_info"
>Input age to automatically calculate birthdate until birthdate is updated manually.</string>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2022 Google LLC
* Copyright 2022-2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -24,6 +24,8 @@ import com.google.android.fhir.datacapture.R
import org.hl7.fhir.r4.model.Attachment
import org.hl7.fhir.r4.model.BooleanType
import org.hl7.fhir.r4.model.Questionnaire
import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemAnswerOptionComponent
import org.hl7.fhir.r4.model.Type

internal const val EXTENSION_OPTION_EXCLUSIVE_URL =
"http://hl7.org/fhir/StructureDefinition/questionnaire-optionExclusive"
Expand All @@ -42,6 +44,10 @@ internal val Questionnaire.QuestionnaireItemAnswerOptionComponent.optionExclusiv
return false
}

/** Get the answer options values with `initialSelected` set to true */
internal val List<QuestionnaireItemAnswerOptionComponent>.initialSelected: List<Type>
get() = this.filter { it.initialSelected }.map { it.value }

fun Questionnaire.QuestionnaireItemAnswerOptionComponent.itemAnswerOptionImage(
context: Context
): Drawable? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,9 @@ internal const val EXTENSION_SLIDER_STEP_VALUE_URL =

internal const val EXTENSION_VARIABLE_URL = "http://hl7.org/fhir/StructureDefinition/variable"

internal const val ITEM_INITIAL_EXPRESSION_URL: String =
"http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression"

// ********************************************************************************************** //
// //
// Rendering extensions: item control, choice orientation, etc. //
Expand All @@ -169,6 +172,17 @@ enum class ItemControlTypes(
PHONE_NUMBER("phone-number", QuestionnaireViewHolderType.PHONE_NUMBER),
}

/**
* The initial-expression extension on [QuestionnaireItemComponent] to allow dynamic selection of
* default or initially selected answers
*/
val Questionnaire.QuestionnaireItemComponent.initialExpression: Expression?
get() {
return this.extension
.firstOrNull { it.url == ITEM_INITIAL_EXPRESSION_URL }
?.let { it.value as Expression }
}

/**
* The [ItemControlTypes] of the questionnaire item if it is specified by the item control
* extension, or `null`.
Expand Down Expand Up @@ -834,11 +848,20 @@ fun Questionnaire.QuestionnaireItemComponent.createQuestionnaireResponseItem():
*/
private fun Questionnaire.QuestionnaireItemComponent.createQuestionnaireResponseItemAnswers():
MutableList<QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent>? {
// TODO https://github.com/google/android-fhir/issues/2161
// The rule can be by-passed if initial value was set by an initial-expression.
// The [ResourceMapper] at L260 wrongfully sets the initial property of questionnaire after
// evaluation of initial-expression.
require(answerOption.isEmpty() || initial.isEmpty() || initialExpression != null) {
"Questionnaire item $linkId has both initial value(s) and has answerOption. See rule que-11 at https://www.hl7.org/fhir/questionnaire-definitions.html#Questionnaire.item.initial."
}

// https://build.fhir.org/ig/HL7/sdc/behavior.html#initial
// quantity given as initial without value is for unit reference purpose only. Answer conversion
// not needed
if (initial.isEmpty() ||
(initialFirstRep.hasValueQuantity() && initialFirstRep.valueQuantity.value == null)
if (answerOption.initialSelected.isEmpty() &&
(initial.isEmpty() ||
(initialFirstRep.hasValueQuantity() && initialFirstRep.valueQuantity.value == null))
) {
return null
}
Expand All @@ -851,16 +874,16 @@ private fun Questionnaire.QuestionnaireItemComponent.createQuestionnaireResponse
)
}

if (initial.size > 1 && !repeats) {
if ((answerOption.initialSelected.size > 1 || initial.size > 1) && !repeats) {
throw IllegalArgumentException(
"Questionnaire item $linkId can only have multiple initial values for repeating items. See rule que-13 at https://www.hl7.org/fhir/questionnaire-definitions.html#Questionnaire.item.initial."
)
}

return initial
.map {
QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { value = it.value }
}
.map { it.value }
.plus(answerOption.initialSelected)
.map { QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { value = it } }
.toMutableList()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ 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.initialExpression
import com.google.android.fhir.datacapture.extensions.logicalId
import com.google.android.fhir.datacapture.extensions.targetStructureMap
import com.google.android.fhir.datacapture.extensions.toCodeType
Expand Down Expand Up @@ -280,13 +281,6 @@ object ResourceMapper {
?: resources.firstOrNull()
}

private val Questionnaire.QuestionnaireItemComponent.initialExpression: Expression?
get() {
return this.extension
.firstOrNull { it.url == ITEM_INITIAL_EXPRESSION_URL }
?.let { it.value as Expression }
}

/**
* Updates corresponding fields in [extractionContext] with answers in
* [questionnaireResponseItemList]. The fields are defined in the definitions in
Expand Down Expand Up @@ -721,9 +715,6 @@ private fun wrapAnswerInFieldType(answer: Base, fieldType: Field): Base {
return answer
}

internal const val ITEM_INITIAL_EXPRESSION_URL: String =
"http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression"

private val Field.isList: Boolean
get() = isParameterized && type == List::class.java

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2022 Google LLC
* Copyright 2022-2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -204,4 +204,54 @@ class MoreQuestionnaireItemAnswerOptionComponentsTest {

assertThat(questionnaire.item.single().answerOption.single().optionExclusive).isTrue()
}

@Test
fun `initialSelected should not select option with initialSelected as null`() {
val answerOptions =
listOf(answerOptionOf("test-code 1", "http://code.com", "Test Code 1", null))

assertThat(answerOptions.initialSelected).isEmpty()
}

@Test
fun `initialSelected should not select option with initialSelected as false`() {
val answerOptions =
listOf(answerOptionOf("test-code 1", "http://code.com", "Test Code 1", false))

assertThat(answerOptions.initialSelected).isEmpty()
}

@Test
fun `initialSelected should select option with initialSelected as true`() {
val answerOptions =
listOf(answerOptionOf("test-code 1", "http://code.com", "Test Code 1", true))

assertThat(answerOptions.initialSelected.map { (it as Coding).code })
.containsExactly("test-code 1")
}

@Test
fun `initialSelected should select multiple options with initialSelected as true`() {
val answerOptions =
listOf(
answerOptionOf("test-code 1", "http://code.com", "Test Code 1", null),
answerOptionOf("test-code 2", "http://code.com", "Test Code 2", true),
answerOptionOf("test-code 3", "http://code.com", "Test Code 3", false),
answerOptionOf("test-code 4", "http://code.com", "Test Code 4", true)
)

assertThat(answerOptions.initialSelected.map { (it as Coding).code })
.containsExactly("test-code 2", "test-code 4")
}

private fun answerOptionOf(
code: String,
url: String,
display: String,
initialSelected: Boolean?
) =
Questionnaire.QuestionnaireItemAnswerOptionComponent().apply {
value = Coding().setCode(code).setDisplay(display).setSystem(url)
initialSelected?.let { this.initialSelected = it }
}
}
Loading