From 7bf05ce4c3c39dc743f092be5ca255bc6be8229a Mon Sep 17 00:00:00 2001 From: maimoonak Date: Wed, 11 May 2022 11:20:39 +0500 Subject: [PATCH 01/34] Implement calculated-expression extension --- .../calculated_expression_questionnaire.json | 24 +++++++ .../fhir/catalog/ComponentListViewModel.kt | 5 ++ catalog/src/main/res/values/strings.xml | 1 + .../MoreQuestionnaireItemComponents.kt | 10 +++ .../datacapture/QuestionnaireViewModel.kt | 31 +++++++++ .../datacapture/mapping/ResourceMapper.kt | 3 +- .../datacapture/QuestionnaireViewModelTest.kt | 66 +++++++++++++++++++ 7 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 catalog/src/main/assets/calculated_expression_questionnaire.json diff --git a/catalog/src/main/assets/calculated_expression_questionnaire.json b/catalog/src/main/assets/calculated_expression_questionnaire.json new file mode 100644 index 0000000000..921d7fc9be --- /dev/null +++ b/catalog/src/main/assets/calculated_expression_questionnaire.json @@ -0,0 +1,24 @@ +{ + "resourceType": "Questionnaire", + "item": [ + { + "linkId": "a-birthdate", + "text": "Birth Date", + "type": "date" + }, + { + "linkId": "a-age-years", + "text": "Age years", + "type": "integer", + "extension": [ + { + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-calculatedExpression", + "valueExpression": { + "language": "text/fhirpath", + "expression": "today().toString().substring(0, 4).toInteger() - %resource.repeat(item).where(linkId='a-birthdate').answer.value.toString().substring(0, 4).toInteger()" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/catalog/src/main/java/com/google/android/fhir/catalog/ComponentListViewModel.kt b/catalog/src/main/java/com/google/android/fhir/catalog/ComponentListViewModel.kt index c5f89ac281..a5cfdd0762 100644 --- a/catalog/src/main/java/com/google/android/fhir/catalog/ComponentListViewModel.kt +++ b/catalog/src/main/java/com/google/android/fhir/catalog/ComponentListViewModel.kt @@ -77,5 +77,10 @@ class ComponentListViewModel(application: Application, private val state: SavedS ), SLIDER(R.drawable.ic_slider, R.string.component_name_slider, "slider_questionnaire.json"), IMAGE(R.drawable.ic_image, R.string.component_name_image, ""), + CALCULATED_EXPRESSION( + R.drawable.ic_unitoptions, + R.string.component_name_calculated_expression, + "calculated_expression_questionnaire.json" + ), } } diff --git a/catalog/src/main/res/values/strings.xml b/catalog/src/main/res/values/strings.xml index 9f33de96d6..773e124759 100644 --- a/catalog/src/main/res/values/strings.xml +++ b/catalog/src/main/res/values/strings.xml @@ -29,6 +29,7 @@ Slider Dropdown Image + Calculated Expression Default Paginated Review diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponents.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponents.kt index f09777d743..c5f4e4fca1 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponents.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponents.kt @@ -22,6 +22,7 @@ import com.google.android.fhir.getLocalizedText import org.hl7.fhir.r4.model.BooleanType import org.hl7.fhir.r4.model.CodeType import org.hl7.fhir.r4.model.CodeableConcept +import org.hl7.fhir.r4.model.Expression import org.hl7.fhir.r4.model.Questionnaire import org.hl7.fhir.r4.model.QuestionnaireResponse @@ -53,6 +54,15 @@ internal const val EXTENSION_ITEM_CONTROL_SYSTEM = "http://hl7.org/fhir/question internal const val EXTENSION_HIDDEN_URL = "http://hl7.org/fhir/StructureDefinition/questionnaire-hidden" +internal const val EXTENSION_CALCULATED_EXPRESSION_URL = + "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-calculatedExpression" + +internal val Questionnaire.QuestionnaireItemComponent.calculatedExpression: Expression? + get() = + this.getExtensionByUrl(EXTENSION_CALCULATED_EXPRESSION_URL)?.let { + it.castToExpression(it.value) + } + // Item control code, or null internal val Questionnaire.QuestionnaireItemComponent.itemControl: ItemControlTypes? get() { 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 5c4c2f3439..431c47050e 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 @@ -18,6 +18,7 @@ package com.google.android.fhir.datacapture import android.app.Application import android.net.Uri +import androidx.annotation.VisibleForTesting import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope @@ -31,12 +32,14 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn +import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext import org.hl7.fhir.r4.model.CodeableConcept import org.hl7.fhir.r4.model.Coding import org.hl7.fhir.r4.model.Questionnaire import org.hl7.fhir.r4.model.QuestionnaireResponse import org.hl7.fhir.r4.model.ResourceType import org.hl7.fhir.r4.model.ValueSet +import org.hl7.fhir.r4.utils.FHIRPathEngine import timber.log.Timber internal class QuestionnaireViewModel(application: Application, state: SavedStateHandle) : @@ -44,6 +47,11 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat /** The current questionnaire as questions are being answered. */ internal val questionnaire: Questionnaire + private val fhirPathEngine: FHIRPathEngine = + with(FhirContext.forCached(FhirVersionEnum.R4)) { + FHIRPathEngine(HapiWorkerContext(this, this.validationSupport)) + } + init { questionnaire = when { @@ -113,6 +121,7 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat * Callback function to update the UI which takes the linkId of the question whose answer(s) has * been changed. */ + @com.google.common.annotations.VisibleForTesting private val questionnaireResponseItemChangedCallback: (String) -> Unit = { linkId -> linkIdToQuestionnaireItemMap[linkId]?.let { questionnaireItem -> if (questionnaireItem.hasNestedItemsWithinAnswers) { @@ -126,6 +135,7 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat } } } + modificationCount.value += 1 } @@ -173,6 +183,21 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat ) ) + fun runCalculatedExpressions() { + val calculableItems = + linkIdToQuestionnaireItemMap.filter { it.value.calculatedExpression != null } + calculableItems.forEach { questionnaireItem -> + linkIdToQuestionnaireResponseItemMap[questionnaireItem.key]?.let { questionnaireResponseItem + -> + val expression = questionnaireItem.value.calculatedExpression!!.expression + fhirPathEngine.evaluate(null, questionnaireResponse, null, null, expression).run { + questionnaireResponseItem.answerFirstRep.value = + this.firstOrNull()?.let { it.castToType(it) } + } + } + } + } + @PublishedApi internal suspend fun resolveAnswerValueSet( uri: String @@ -254,6 +279,9 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat questionnaireResponseItemList: List, pagination: QuestionnairePagination?, ): QuestionnaireState { + + runCalculatedExpressions() + // TODO(kmost): validate pages before switching between next/prev pages var responseIndex = 0 val items: List = @@ -317,6 +345,7 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat .items } .toList() + return QuestionnaireState(items = items, pagination = pagination) } @@ -355,6 +384,8 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat * when first opening this questionnaire. Otherwise, returns `null`. */ private fun Questionnaire.getInitialPagination(): QuestionnairePagination? { + runCalculatedExpressions() + val usesPagination = item.any { item -> item.extension.any { extension -> 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 de2bad893e..975e0ab644 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,7 +18,6 @@ package com.google.android.fhir.datacapture.mapping import ca.uhn.fhir.context.FhirContext import ca.uhn.fhir.context.FhirVersionEnum -import ca.uhn.fhir.context.support.DefaultProfileValidationSupport import com.google.android.fhir.datacapture.DataCapture import com.google.android.fhir.datacapture.createQuestionnaireResponseItem import com.google.android.fhir.datacapture.targetStructureMap @@ -72,7 +71,7 @@ object ResourceMapper { private val fhirPathEngine: FHIRPathEngine = with(FhirContext.forCached(FhirVersionEnum.R4)) { - FHIRPathEngine(HapiWorkerContext(this, DefaultProfileValidationSupport(this))) + FHIRPathEngine(HapiWorkerContext(this, this.validationSupport)) } /** 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 19a783ce18..ffe805f68e 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 @@ -26,9 +26,12 @@ import ca.uhn.fhir.parser.IParser import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_QUESTIONNAIRE_JSON_STRING import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_QUESTIONNAIRE_JSON_URI import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_QUESTIONNAIRE_RESPONSE_JSON_STRING +import com.google.android.fhir.datacapture.common.datatype.asStringValue import com.google.android.fhir.datacapture.testing.DataCaptureTestApplication import com.google.common.truth.Truth.assertThat import java.io.File +import java.util.Calendar +import java.util.Date import kotlin.test.assertFailsWith import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking @@ -36,6 +39,8 @@ import org.hl7.fhir.instance.model.api.IBaseResource import org.hl7.fhir.r4.model.BooleanType import org.hl7.fhir.r4.model.CodeableConcept import org.hl7.fhir.r4.model.Coding +import org.hl7.fhir.r4.model.DateType +import org.hl7.fhir.r4.model.Expression import org.hl7.fhir.r4.model.Extension import org.hl7.fhir.r4.model.IntegerType import org.hl7.fhir.r4.model.Questionnaire @@ -1422,6 +1427,67 @@ class QuestionnaireViewModelTest(private val questionnaireSource: QuestionnaireS .isEqualTo("parent-question") } + @Test + fun questionnaireItem_calculatedExpressionExtension_shouldCalculateValue() = runBlocking { + val questionnaire = + Questionnaire().apply { + id = "a-questionnaire" + addItem( + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "a-birthdate" + type = Questionnaire.QuestionnaireItemType.DATE + addInitial( + Questionnaire.QuestionnaireItemInitialComponent( + DateType(Date()).apply { add(Calendar.YEAR, -2) } + ) + ) + } + ) + addItem( + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "a-age-years" + type = Questionnaire.QuestionnaireItemType.INTEGER + addExtension().apply { + url = EXTENSION_CALCULATED_EXPRESSION_URL + setValue( + Expression().apply { + this.language = "text/fhirpath" + this.expression = + "today().toString().substring(0, 4).toInteger() - %resource.repeat(item).where(linkId='a-birthdate').answer.value.toString().substring(0, 4).toInteger()" + } + ) + } + } + ) + } + val serializedQuestionnaire = printer.encodeResourceToString(questionnaire) + state.set(EXTRA_QUESTIONNAIRE_JSON_STRING, serializedQuestionnaire) + + val viewModel = QuestionnaireViewModel(context, state) + + assertThat( + viewModel + .getQuestionnaireResponse() + .item + .single { it.linkId == "a-birthdate" } + .answerFirstRep + .value + .asStringValue() + ) + .isEqualTo(DateType(Date()).apply { add(Calendar.YEAR, -2) }.asStringValue()) + + assertThat( + viewModel + .getQuestionnaireResponse() + .item + .single { it.linkId == "a-age-years" } + .answerFirstRep + .valueIntegerType + .value + ) + .isEqualTo(2) + } + private fun createQuestionnaireViewModel( questionnaire: Questionnaire, response: QuestionnaireResponse? = null From b569b8183e31e613416bfe94ab9a4d9698103c4b Mon Sep 17 00:00:00 2001 From: maimoonak Date: Thu, 12 May 2022 11:25:02 +0500 Subject: [PATCH 02/34] Fix form value update bug --- .../calculated_expression_questionnaire.json | 5 +- .../fhir/catalog/ComponentListViewModel.kt | 6 +-- catalog/src/main/res/values/strings.xml | 4 +- .../fhir/datacapture/QuestionnaireFragment.kt | 6 +++ .../datacapture/QuestionnaireViewModel.kt | 50 +++++++++++++++---- 5 files changed, 55 insertions(+), 16 deletions(-) diff --git a/catalog/src/main/assets/calculated_expression_questionnaire.json b/catalog/src/main/assets/calculated_expression_questionnaire.json index 921d7fc9be..ef82a90a14 100644 --- a/catalog/src/main/assets/calculated_expression_questionnaire.json +++ b/catalog/src/main/assets/calculated_expression_questionnaire.json @@ -4,7 +4,10 @@ { "linkId": "a-birthdate", "text": "Birth Date", - "type": "date" + "type": "date", + "initial": [{ + "valueDate": "2021-01-01" + }] }, { "linkId": "a-age-years", diff --git a/catalog/src/main/java/com/google/android/fhir/catalog/ComponentListViewModel.kt b/catalog/src/main/java/com/google/android/fhir/catalog/ComponentListViewModel.kt index a5cfdd0762..2eb45c198b 100644 --- a/catalog/src/main/java/com/google/android/fhir/catalog/ComponentListViewModel.kt +++ b/catalog/src/main/java/com/google/android/fhir/catalog/ComponentListViewModel.kt @@ -78,9 +78,9 @@ class ComponentListViewModel(application: Application, private val state: SavedS SLIDER(R.drawable.ic_slider, R.string.component_name_slider, "slider_questionnaire.json"), IMAGE(R.drawable.ic_image, R.string.component_name_image, ""), CALCULATED_EXPRESSION( - R.drawable.ic_unitoptions, - R.string.component_name_calculated_expression, - "calculated_expression_questionnaire.json" + R.drawable.ic_unitoptions, + R.string.component_name_calculated_expression, + "calculated_expression_questionnaire.json" ), } } diff --git a/catalog/src/main/res/values/strings.xml b/catalog/src/main/res/values/strings.xml index 773e124759..3a2f5b8d4b 100644 --- a/catalog/src/main/res/values/strings.xml +++ b/catalog/src/main/res/values/strings.xml @@ -29,7 +29,9 @@ Slider Dropdown Image - Calculated Expression + Calculated Expression Default Paginated Review 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 9d914e987c..fe1f40c1a7 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 @@ -82,6 +82,12 @@ open class QuestionnaireFragment : Fragment() { } } } + + viewLifecycleOwner.lifecycleScope.launchWhenCreated { + viewModel.questionnaireItemValueStateFlow.collect { index -> + if (index > -1 && !recyclerView.isComputingLayout) adapter.notifyItemChanged(index) + } + } } /** 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 431c47050e..cd74e2c625 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 @@ -18,7 +18,6 @@ package com.google.android.fhir.datacapture import android.app.Application import android.net.Uri -import androidx.annotation.VisibleForTesting import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope @@ -28,10 +27,15 @@ import com.google.android.fhir.datacapture.enablement.EnablementEvaluator import com.google.android.fhir.datacapture.validation.QuestionnaireResponseValidator.checkQuestionnaireResponse import com.google.android.fhir.datacapture.views.QuestionnaireItemViewItem import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext import org.hl7.fhir.r4.model.CodeableConcept import org.hl7.fhir.r4.model.Coding @@ -121,7 +125,6 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat * Callback function to update the UI which takes the linkId of the question whose answer(s) has * been changed. */ - @com.google.common.annotations.VisibleForTesting private val questionnaireResponseItemChangedCallback: (String) -> Unit = { linkId -> linkIdToQuestionnaireItemMap[linkId]?.let { questionnaireItem -> if (questionnaireItem.hasNestedItemsWithinAnswers) { @@ -136,6 +139,8 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat } } + runCalculatedExpressions() + modificationCount.value += 1 } @@ -183,7 +188,13 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat ) ) - fun runCalculatedExpressions() { + /** + * Notify the updated value index to notify about the item update for UI (adapter item changed) + */ + private val _questionnaireItemValueStateFlow = MutableSharedFlow() + internal val questionnaireItemValueStateFlow = _questionnaireItemValueStateFlow.asSharedFlow() + + private fun runCalculatedExpressions() { val calculableItems = linkIdToQuestionnaireItemMap.filter { it.value.calculatedExpression != null } calculableItems.forEach { questionnaireItem -> @@ -191,8 +202,30 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat -> val expression = questionnaireItem.value.calculatedExpression!!.expression fhirPathEngine.evaluate(null, questionnaireResponse, null, null, expression).run { - questionnaireResponseItem.answerFirstRep.value = - this.firstOrNull()?.let { it.castToType(it) } + with(this.firstOrNull()) { + // update only if answer has changed, otherwise app can stuck in loop to keep updating + // changed items + val evaluatedAnswer = this?.castToType(this) + val currentAnswer = + if (questionnaireResponseItem.hasAnswer()) + questionnaireResponseItem.answerFirstRep.value + else null + if (!(evaluatedAnswer == null && currentAnswer == null) && + evaluatedAnswer?.equalsDeep(currentAnswer) != true + ) { + questionnaireResponseItem.answerFirstRep.value = + evaluatedAnswer?.let { it.castToType(it) } + + // notify UI to update it value i.e. notify item changed to adapter + viewModelScope.launch { + questionnaireStateFlow.collectLatest { + it.items + .indexOfFirst { it.questionnaireItem.linkId == questionnaireItem.key } + .let { if (it > -1) _questionnaireItemValueStateFlow.emit(it) } + } + } + } + } } } } @@ -279,9 +312,6 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat questionnaireResponseItemList: List, pagination: QuestionnairePagination?, ): QuestionnaireState { - - runCalculatedExpressions() - // TODO(kmost): validate pages before switching between next/prev pages var responseIndex = 0 val items: List = @@ -317,7 +347,7 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat QuestionnaireItemViewItem( questionnaireItem, questionnaireResponseItem, - { resolveAnswerValueSet(it) } + { resolveAnswerValueSet(it) }, ) { questionnaireResponseItemChangedCallback(questionnaireItem.linkId) } ) + getQuestionnaireState( @@ -384,8 +414,6 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat * when first opening this questionnaire. Otherwise, returns `null`. */ private fun Questionnaire.getInitialPagination(): QuestionnairePagination? { - runCalculatedExpressions() - val usesPagination = item.any { item -> item.extension.any { extension -> From d4721907ba585618714631f0883c2061670f37e1 Mon Sep 17 00:00:00 2001 From: maimoonak Date: Thu, 12 May 2022 15:20:49 +0500 Subject: [PATCH 03/34] Detect cyclic dependency | fix on init value loading --- .../datacapture/QuestionnaireViewModel.kt | 33 +++++++++-- .../datacapture/QuestionnaireViewModelTest.kt | 57 +++++++++++++++++++ 2 files changed, 85 insertions(+), 5 deletions(-) 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 cd74e2c625..ba5da0aad1 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 @@ -210,6 +210,7 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat if (questionnaireResponseItem.hasAnswer()) questionnaireResponseItem.answerFirstRep.value else null + if (!(evaluatedAnswer == null && currentAnswer == null) && evaluatedAnswer?.equalsDeep(currentAnswer) != true ) { @@ -218,11 +219,12 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat // notify UI to update it value i.e. notify item changed to adapter viewModelScope.launch { - questionnaireStateFlow.collectLatest { - it.items - .indexOfFirst { it.questionnaireItem.linkId == questionnaireItem.key } - .let { if (it > -1) _questionnaireItemValueStateFlow.emit(it) } - } + if (modificationCount.value > 0) + questionnaireStateFlow.collectLatest { + it.items + .indexOfFirst { it.questionnaireItem.linkId == questionnaireItem.key } + .let { if (it > -1) _questionnaireItemValueStateFlow.emit(it) } + } } } } @@ -231,6 +233,24 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat } } + fun detectCalculatedExpressionCyclicDependency() { + val calculableItems = + linkIdToQuestionnaireItemMap.filter { it.value.calculatedExpression != null } + calculableItems.forEach { current -> + val currentExpression = current.value.calculatedExpression!!.expression + val otherDependent = + calculableItems.values.find { + it.calculatedExpression!!.expression.contains("'${current.key}'") + } + // if any calculable expression depends on this item and this item is referring to the + // dependent item in its own expression then raise error + if (otherDependent != null && currentExpression.contains("'${otherDependent.linkId}'")) + throw IllegalStateException( + "${current.key} and ${otherDependent.linkId} have cyclic dependency in calculated-expression extension" + ) + } + } + @PublishedApi internal suspend fun resolveAnswerValueSet( uri: String @@ -414,6 +434,9 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat * when first opening this questionnaire. Otherwise, returns `null`. */ private fun Questionnaire.getInitialPagination(): QuestionnairePagination? { + detectCalculatedExpressionCyclicDependency() + runCalculatedExpressions() + val usesPagination = item.any { item -> item.extension.any { extension -> 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 ffe805f68e..050f1e474d 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 @@ -47,6 +47,7 @@ import org.hl7.fhir.r4.model.Questionnaire import org.hl7.fhir.r4.model.QuestionnaireResponse import org.hl7.fhir.r4.model.StringType import org.hl7.fhir.r4.model.ValueSet +import org.junit.Assert import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -1488,6 +1489,62 @@ class QuestionnaireViewModelTest(private val questionnaireSource: QuestionnaireS .isEqualTo(2) } + @Test + fun questionnaireItem_calculatedExpressionExtension_shouldDetectCyclicDependency() = runBlocking { + val questionnaire = + Questionnaire().apply { + id = "a-questionnaire" + addItem( + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "a-birthdate" + type = Questionnaire.QuestionnaireItemType.DATE + addInitial( + Questionnaire.QuestionnaireItemInitialComponent( + DateType(Date()).apply { add(Calendar.YEAR, -2) } + ) + ) + addExtension().apply { + url = EXTENSION_CALCULATED_EXPRESSION_URL + setValue( + Expression().apply { + this.language = "text/fhirpath" + this.expression = + "%resource.repeat(item).where(linkId='a-birthdate').answer.value.toString()" + } + ) + } + } + ) + addItem( + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "a-age-years" + type = Questionnaire.QuestionnaireItemType.INTEGER + addExtension().apply { + url = EXTENSION_CALCULATED_EXPRESSION_URL + setValue( + Expression().apply { + this.language = "text/fhirpath" + this.expression = + "today().toString().substring(0, 4).toInteger() - %resource.repeat(item).where(linkId='a-birthdate').answer.value.toString().substring(0, 4).toInteger()" + } + ) + } + } + ) + } + val serializedQuestionnaire = printer.encodeResourceToString(questionnaire) + state.set(EXTRA_QUESTIONNAIRE_JSON_STRING, serializedQuestionnaire) + + val exception = + Assert.assertThrows(null, IllegalStateException::class.java) { + QuestionnaireViewModel(context, state) + } + assertThat(exception.message) + .isNotEqualTo( + "a-birthdate and a-age-years have cyclic dependency in calculated-expression extension" + ) + } + private fun createQuestionnaireViewModel( questionnaire: Questionnaire, response: QuestionnaireResponse? = null From 5536e96ab3b1383fd0a6d1ecc9c5a8d872f58e28 Mon Sep 17 00:00:00 2001 From: maimoonak Date: Thu, 12 May 2022 15:27:56 +0500 Subject: [PATCH 04/34] Fix merge conflict --- .../android/fhir/datacapture/QuestionnaireViewModelTest.kt | 1 + 1 file changed, 1 insertion(+) 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 3a9d9ec2ea..e2214a8af8 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 @@ -27,6 +27,7 @@ import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_QUESTIONNAIRE_JSON_URI import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_QUESTIONNAIRE_RESPONSE_JSON_STRING import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_QUESTIONNAIRE_RESPONSE_JSON_URI +import com.google.android.fhir.datacapture.common.datatype.asStringValue import com.google.android.fhir.datacapture.testing.DataCaptureTestApplication import com.google.common.truth.Truth.assertThat import java.io.File From bce9fd96550fe9f97b535876f403cef28a3108b1 Mon Sep 17 00:00:00 2001 From: maimoonak Date: Thu, 12 May 2022 20:09:04 +0500 Subject: [PATCH 05/34] Make birthdate age dependent | Handle and fix quantity values --- .../calculated_expression_questionnaire.json | 28 ++++++++++----- .../MoreQuestionnaireItemComponents.kt | 7 +++- ...ionnaireItemDatePickerViewHolderFactory.kt | 6 +++- ...reItemEditTextQuantityViewHolderFactory.kt | 10 +++++- .../datacapture/QuestionnaireViewModelTest.kt | 35 ++++++++++--------- 5 files changed, 57 insertions(+), 29 deletions(-) diff --git a/catalog/src/main/assets/calculated_expression_questionnaire.json b/catalog/src/main/assets/calculated_expression_questionnaire.json index ef82a90a14..3e72d87668 100644 --- a/catalog/src/main/assets/calculated_expression_questionnaire.json +++ b/catalog/src/main/assets/calculated_expression_questionnaire.json @@ -5,23 +5,33 @@ "linkId": "a-birthdate", "text": "Birth Date", "type": "date", - "initial": [{ - "valueDate": "2021-01-01" - }] - }, - { - "linkId": "a-age-years", - "text": "Age years", - "type": "integer", "extension": [ { "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-calculatedExpression", "valueExpression": { "language": "text/fhirpath", - "expression": "today().toString().substring(0, 4).toInteger() - %resource.repeat(item).where(linkId='a-birthdate').answer.value.toString().substring(0, 4).toInteger()" + "expression": "%resource.repeat(item).where(linkId='a-age-years' and answer.empty().not()).select(today() - answer.value)" } } ] + }, + { + "linkId": "a-age-years", + "text": "Age years", + "hint": "Input age to automatically calculate birthdate in case exact birthdate is not known", + "type": "quantity", + "initial": [{ + "valueQuantity": { + "unit": "months", + "system": "http://unitsofmeasure.org", + "code": "months" + } + }] + }, + { + "linkId": "a-age-note", + "text": "Input age to automatically calculate birthdate in case exact birthdate is not known", + "type": "display" } ] } \ No newline at end of file diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponents.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponents.kt index effe7b1fd5..e93a864e07 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponents.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponents.kt @@ -244,7 +244,12 @@ val Questionnaire.QuestionnaireItemComponent.enableWhenExpression: Expression? */ private fun Questionnaire.QuestionnaireItemComponent.createQuestionnaireResponseItemAnswers(): MutableList? { - if (initial.isEmpty()) { + // 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) + ) { return null } diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDatePickerViewHolderFactory.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDatePickerViewHolderFactory.kt index 76d9f75d3c..b47c54d476 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDatePickerViewHolderFactory.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDatePickerViewHolderFactory.kt @@ -82,7 +82,11 @@ internal object QuestionnaireItemDatePickerViewHolderFactory : header.bind(questionnaireItemViewItem.questionnaireItem) textInputEditText.setText( - questionnaireItemViewItem.singleAnswerOrNull?.valueDateType?.localDate?.localizedString + questionnaireItemViewItem.singleAnswerOrNull + ?.takeIf { it.hasValue() } + ?.valueDateType + ?.localDate + ?.localizedString ) questionnaireItemViewItem.questionnaireItem.entryFormat?.let { textInputLayout.helperText = it diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactory.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactory.kt index 9c3eeb78eb..1df529139d 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactory.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactory.kt @@ -35,8 +35,16 @@ internal object QuestionnaireItemEditTextQuantityViewHolderFactory : override fun getValue( text: String ): QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent? { + // https://build.fhir.org/ig/HL7/sdc/behavior.html#initial + // read default unit from initial, as ideally quantity must specify a unit return text.toDoubleOrNull()?.let { - QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().setValue(Quantity(it)) + val quantity = + with(questionnaireItemViewItem.questionnaireItem) { + if (this.hasInitial() && this.initialFirstRep.valueQuantity.hasCode()) + Quantity.fromUcum(text, this.initialFirstRep.valueQuantity.code) + else Quantity(it) + } + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().setValue(quantity) } } 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 e2214a8af8..d94311b505 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 @@ -1441,29 +1441,29 @@ class QuestionnaireViewModelTest( Questionnaire.QuestionnaireItemComponent().apply { linkId = "a-birthdate" type = Questionnaire.QuestionnaireItemType.DATE - addInitial( - Questionnaire.QuestionnaireItemInitialComponent( - DateType(Date()).apply { add(Calendar.YEAR, -2) } - ) - ) - } - ) - addItem( - Questionnaire.QuestionnaireItemComponent().apply { - linkId = "a-age-years" - type = Questionnaire.QuestionnaireItemType.INTEGER addExtension().apply { url = EXTENSION_CALCULATED_EXPRESSION_URL setValue( Expression().apply { this.language = "text/fhirpath" this.expression = - "today().toString().substring(0, 4).toInteger() - %resource.repeat(item).where(linkId='a-birthdate').answer.value.toString().substring(0, 4).toInteger()" + "%resource.repeat(item).where(linkId='a-age-years' and answer.empty().not()).select(today() - answer.value)" } ) } } ) + addItem( + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "a-age-years" + type = Questionnaire.QuestionnaireItemType.QUANTITY + /* addInitial( + Questionnaire.QuestionnaireItemInitialComponent( + Quantity.fromUcum("1", "year") + ) + )*/ + } + ) } val serializedQuestionnaire = printer.encodeResourceToString(questionnaire) state.set(EXTRA_QUESTIONNAIRE_JSON_STRING, serializedQuestionnaire) @@ -1479,7 +1479,7 @@ class QuestionnaireViewModelTest( .value .asStringValue() ) - .isEqualTo(DateType(Date()).apply { add(Calendar.YEAR, -2) }.asStringValue()) + .isEqualTo(DateType(Date()).apply { add(Calendar.YEAR, -1) }.asStringValue()) assertThat( viewModel @@ -1487,10 +1487,11 @@ class QuestionnaireViewModelTest( .item .single { it.linkId == "a-age-years" } .answerFirstRep - .valueIntegerType + .valueQuantity .value + .toString() ) - .isEqualTo(2) + .isEqualTo("1") } @Test @@ -1513,7 +1514,7 @@ class QuestionnaireViewModelTest( Expression().apply { this.language = "text/fhirpath" this.expression = - "%resource.repeat(item).where(linkId='a-birthdate').answer.value.toString()" + "%resource.repeat(item).where(linkId='a-age-years' and answer.empty().not()).select(today() - answer.value)" } ) } @@ -1544,7 +1545,7 @@ class QuestionnaireViewModelTest( QuestionnaireViewModel(context, state) } assertThat(exception.message) - .isNotEqualTo( + .isEqualTo( "a-birthdate and a-age-years have cyclic dependency in calculated-expression extension" ) } From 777489b49d0883617d0ff8c19b10ba8cbeb332fa Mon Sep 17 00:00:00 2001 From: maimoonak Date: Thu, 12 May 2022 20:22:03 +0500 Subject: [PATCH 06/34] Fix failing test --- .../fhir/datacapture/QuestionnaireViewModelTest.kt | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) 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 d94311b505..b0c37d56e3 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 @@ -44,6 +44,7 @@ import org.hl7.fhir.r4.model.DateType import org.hl7.fhir.r4.model.Expression import org.hl7.fhir.r4.model.Extension import org.hl7.fhir.r4.model.IntegerType +import org.hl7.fhir.r4.model.Quantity import org.hl7.fhir.r4.model.Questionnaire import org.hl7.fhir.r4.model.QuestionnaireResponse import org.hl7.fhir.r4.model.StringType @@ -1457,11 +1458,9 @@ class QuestionnaireViewModelTest( Questionnaire.QuestionnaireItemComponent().apply { linkId = "a-age-years" type = Questionnaire.QuestionnaireItemType.QUANTITY - /* addInitial( - Questionnaire.QuestionnaireItemInitialComponent( - Quantity.fromUcum("1", "year") - ) - )*/ + addInitial( + Questionnaire.QuestionnaireItemInitialComponent(Quantity.fromUcum("1", "year")) + ) } ) } From 0ee4c27e8ac3e53f002af422dbcd480e664f4187 Mon Sep 17 00:00:00 2001 From: maimoonak Date: Thu, 12 May 2022 23:17:03 +0500 Subject: [PATCH 07/34] quantity viewholder delegate test covergae --- .../datacapture/QuestionnaireViewModel.kt | 2 +- ...reItemEditTextQuantityViewHolderFactory.kt | 9 +++- .../QuestionnaireItemViewHolderFactory.kt | 2 +- ...emEditTextQuantityViewHolderFactoryTest.kt | 41 +++++++++++++++++++ 4 files changed, 51 insertions(+), 3 deletions(-) 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 e8cecd1e99..5fe795069d 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 @@ -378,7 +378,7 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat QuestionnaireItemViewItem( questionnaireItem, questionnaireResponseItem, - { resolveAnswerValueSet(it) }, + { resolveAnswerValueSet(it) } ) { questionnaireResponseItemChangedCallback(questionnaireItem.linkId) } ) + getQuestionnaireState( diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactory.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactory.kt index 1df529139d..90186f7b7c 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactory.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactory.kt @@ -17,6 +17,7 @@ package com.google.android.fhir.datacapture.views import android.text.InputType +import java.math.BigDecimal import org.hl7.fhir.r4.model.Quantity import org.hl7.fhir.r4.model.QuestionnaireResponse @@ -41,7 +42,13 @@ internal object QuestionnaireItemEditTextQuantityViewHolderFactory : val quantity = with(questionnaireItemViewItem.questionnaireItem) { if (this.hasInitial() && this.initialFirstRep.valueQuantity.hasCode()) - Quantity.fromUcum(text, this.initialFirstRep.valueQuantity.code) + this.initialFirstRep.valueQuantity.let { initial -> + Quantity().apply { + this.value = BigDecimal(text) + this.code = initial.code + this.system = initial.system + } + } else Quantity(it) } QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().setValue(quantity) diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemViewHolderFactory.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemViewHolderFactory.kt index 09c5e05d41..f8a44fb4aa 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemViewHolderFactory.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemViewHolderFactory.kt @@ -52,7 +52,7 @@ abstract class QuestionnaireItemViewHolderFactory(@LayoutRes val resId: Int) { */ open class QuestionnaireItemViewHolder( itemView: View, - private val delegate: QuestionnaireItemViewHolderDelegate + internal val delegate: QuestionnaireItemViewHolderDelegate ) : RecyclerView.ViewHolder(itemView) { init { delegate.init(itemView) diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactoryTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactoryTest.kt index 34ae56352e..a68c8f6f95 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactoryTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactoryTest.kt @@ -197,4 +197,45 @@ class QuestionnaireItemEditTextQuantityViewHolderFactoryTest { ) .isFalse() } + + @Test + fun getValue_WithInitial_shouldReturnQuantityWithUnitAndSystem() { + viewHolder.bind( + QuestionnaireItemViewItem( + Questionnaire.QuestionnaireItemComponent().apply { + required = true + addInitial( + Questionnaire.QuestionnaireItemInitialComponent( + Quantity().apply { + code = "months" + system = "http://unitofmeasure.com" + } + ) + ) + }, + QuestionnaireResponse.QuestionnaireResponseItemComponent() + ) {} + ) + val value = + (viewHolder.delegate as QuestionnaireItemEditTextViewHolderDelegate).getValue("22")!! + .valueQuantity + assertThat(value.code).isEqualTo("months") + assertThat(value.system).isEqualTo("http://unitofmeasure.com") + assertThat(value.value).isEqualTo(BigDecimal(22)) + } + + fun getValue_WithoutInitial_shouldReturnQuantityWithoutUnitAndSystem() { + viewHolder.bind( + QuestionnaireItemViewItem( + Questionnaire.QuestionnaireItemComponent().apply { required = true }, + QuestionnaireResponse.QuestionnaireResponseItemComponent() + ) {} + ) + val value = + (viewHolder.delegate as QuestionnaireItemEditTextViewHolderDelegate).getValue("22")!! + .valueQuantity + assertThat(value.code).isNull() + assertThat(value.system).isNull() + assertThat(value.value).isEqualTo(BigDecimal(22)) + } } From 3fe03fe472e58da01265b9b6ddab3b0bb3946ad2 Mon Sep 17 00:00:00 2001 From: maimoonak Date: Fri, 13 May 2022 15:13:18 +0500 Subject: [PATCH 08/34] Test coverage for update flow --- .../fhir/datacapture/QuestionnaireFragment.kt | 2 +- .../datacapture/QuestionnaireViewModel.kt | 1 + ...ionnaireItemDatePickerViewHolderFactory.kt | 3 +- .../datacapture/QuestionnaireViewModelTest.kt | 71 +++++++++++++++++-- 4 files changed, 67 insertions(+), 10 deletions(-) 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 1ce7f3db79..1394ed790f 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 @@ -85,7 +85,7 @@ open class QuestionnaireFragment : Fragment() { viewLifecycleOwner.lifecycleScope.launchWhenCreated { viewModel.questionnaireItemValueStateFlow.collect { index -> - if (index > -1 && !recyclerView.isComputingLayout) adapter.notifyItemChanged(index) + if (!recyclerView.isComputingLayout) adapter.notifyItemChanged(index) } } } 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 5fe795069d..e2a082d5be 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 @@ -36,6 +36,7 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext import org.hl7.fhir.r4.model.CodeableConcept diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDatePickerViewHolderFactory.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDatePickerViewHolderFactory.kt index b47c54d476..3cd4878f67 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDatePickerViewHolderFactory.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDatePickerViewHolderFactory.kt @@ -81,8 +81,7 @@ internal object QuestionnaireItemDatePickerViewHolderFactory : override fun bind(questionnaireItemViewItem: QuestionnaireItemViewItem) { header.bind(questionnaireItemViewItem.questionnaireItem) - textInputEditText.setText( - questionnaireItemViewItem.singleAnswerOrNull + textInputEditText.setText(questionnaireItemViewItem.singleAnswerOrNull ?.takeIf { it.hasValue() } ?.valueDateType ?.localDate 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 b0c37d56e3..69e855756b 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 @@ -1434,7 +1434,7 @@ class QuestionnaireViewModelTest( } @Test - fun questionnaireItem_calculatedExpressionExtension_shouldCalculateValue() = runBlocking { + fun questionnaireItem_calculatedExpressionExtension_shouldCalculateValue_onStart() = runBlocking { val questionnaire = Questionnaire().apply { id = "a-questionnaire" @@ -1464,10 +1464,8 @@ class QuestionnaireViewModelTest( } ) } - val serializedQuestionnaire = printer.encodeResourceToString(questionnaire) - state.set(EXTRA_QUESTIONNAIRE_JSON_STRING, serializedQuestionnaire) - val viewModel = QuestionnaireViewModel(context, state) + val viewModel = createQuestionnaireViewModel(questionnaire) assertThat( viewModel @@ -1493,6 +1491,67 @@ class QuestionnaireViewModelTest( .isEqualTo("1") } + @Test + fun questionnaireItem_calculatedExpressionExtension_shouldCalculateValue_onChange() = + runBlocking { + val questionnaire = + Questionnaire().apply { + id = "a-questionnaire" + addItem( + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "a-birthdate" + type = Questionnaire.QuestionnaireItemType.DATE + addExtension().apply { + url = EXTENSION_CALCULATED_EXPRESSION_URL + setValue( + Expression().apply { + this.language = "text/fhirpath" + this.expression = + "%resource.repeat(item).where(linkId='a-age-years' and answer.empty().not()).select(today() - answer.value)" + } + ) + } + } + ) + addItem( + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "a-age-years" + type = Questionnaire.QuestionnaireItemType.INTEGER + } + ) + } + + val viewModel = createQuestionnaireViewModel(questionnaire) + + val current = + viewModel + .getQuestionnaireItemViewItemList() + .first { it.questionnaireResponseItem.linkId == "a-birthdate" } + .apply { this.questionnaireResponseItemChangedCallback() } + + assertThat(current.questionnaireResponseItem.answer).isEmpty() + + viewModel + .getQuestionnaireItemViewItemList() + .first { it.questionnaireResponseItem.linkId == "a-age-years" } + .apply { + questionnaireResponseItem.addAnswer( + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { + this.value = Quantity.fromUcum("2", "years") + } + ) + + this.questionnaireResponseItemChangedCallback() + } + + val updated = + viewModel.getQuestionnaireItemViewItemList().first { + it.questionnaireResponseItem.linkId == "a-birthdate" + } + assertThat(updated.questionnaireResponseItem.answer.first().valueDateType.valueAsString) + .isEqualTo(DateType(Date()).apply { add(Calendar.YEAR, -2) }.valueAsString) + } + @Test fun questionnaireItem_calculatedExpressionExtension_shouldDetectCyclicDependency() = runBlocking { val questionnaire = @@ -1536,12 +1595,10 @@ class QuestionnaireViewModelTest( } ) } - val serializedQuestionnaire = printer.encodeResourceToString(questionnaire) - state.set(EXTRA_QUESTIONNAIRE_JSON_STRING, serializedQuestionnaire) val exception = Assert.assertThrows(null, IllegalStateException::class.java) { - QuestionnaireViewModel(context, state) + createQuestionnaireViewModel(questionnaire) } assertThat(exception.message) .isEqualTo( From 8ae31610e8a68fd197fe47b2e136117430999fa4 Mon Sep 17 00:00:00 2001 From: maimoonak Date: Fri, 13 May 2022 15:20:42 +0500 Subject: [PATCH 09/34] spotless fix --- .../views/QuestionnaireItemDatePickerViewHolderFactory.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDatePickerViewHolderFactory.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDatePickerViewHolderFactory.kt index 3cd4878f67..b47c54d476 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDatePickerViewHolderFactory.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDatePickerViewHolderFactory.kt @@ -81,7 +81,8 @@ internal object QuestionnaireItemDatePickerViewHolderFactory : override fun bind(questionnaireItemViewItem: QuestionnaireItemViewItem) { header.bind(questionnaireItemViewItem.questionnaireItem) - textInputEditText.setText(questionnaireItemViewItem.singleAnswerOrNull + textInputEditText.setText( + questionnaireItemViewItem.singleAnswerOrNull ?.takeIf { it.hasValue() } ?.valueDateType ?.localDate From e56bd98a8fb16c73ef7ca067d327a70e2e7b2648 Mon Sep 17 00:00:00 2001 From: maimoonak Date: Fri, 13 May 2022 15:20:42 +0500 Subject: [PATCH 10/34] spotless fix | re-run ci --- .../views/QuestionnaireItemDatePickerViewHolderFactory.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDatePickerViewHolderFactory.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDatePickerViewHolderFactory.kt index 3cd4878f67..b47c54d476 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDatePickerViewHolderFactory.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDatePickerViewHolderFactory.kt @@ -81,7 +81,8 @@ internal object QuestionnaireItemDatePickerViewHolderFactory : override fun bind(questionnaireItemViewItem: QuestionnaireItemViewItem) { header.bind(questionnaireItemViewItem.questionnaireItem) - textInputEditText.setText(questionnaireItemViewItem.singleAnswerOrNull + textInputEditText.setText( + questionnaireItemViewItem.singleAnswerOrNull ?.takeIf { it.hasValue() } ?.valueDateType ?.localDate From 219dd9e464e745a64d554660a57ab800ec2d05ea Mon Sep 17 00:00:00 2001 From: maimoonak Date: Fri, 20 May 2022 04:26:34 +0500 Subject: [PATCH 11/34] Test covergae for questionnaire fragment --- .../datacapture/QuestionnaireFragmentTest.kt | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireFragmentTest.kt diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireFragmentTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireFragmentTest.kt new file mode 100644 index 0000000000..dc8d37c549 --- /dev/null +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireFragmentTest.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.fhir.datacapture + +import android.os.Build +import androidx.core.os.bundleOf +import androidx.fragment.app.testing.launchFragment +import androidx.fragment.app.testing.withFragment +import androidx.lifecycle.Lifecycle +import ca.uhn.fhir.context.FhirContext +import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_QUESTIONNAIRE_JSON_STRING +import com.google.common.truth.Truth.assertThat +import org.hl7.fhir.r4.model.Questionnaire +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [Build.VERSION_CODES.P]) +class QuestionnaireFragmentTest { + + @Test + fun testFragment_ShouldBeAbleToBuildQuestionnaireResponse() { + val questionnaire = + Questionnaire().apply { + id = "a-questionnaire" + addItem( + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "a-link-id" + type = Questionnaire.QuestionnaireItemType.BOOLEAN + } + ) + } + val questionnaireJson = + FhirContext.forR4().newJsonParser().encodeResourceToString(questionnaire) + val scenario = + launchFragment( + bundleOf(EXTRA_QUESTIONNAIRE_JSON_STRING to questionnaireJson) + ) + scenario.moveToState(Lifecycle.State.RESUMED) + scenario.withFragment { + assertThat(this.getQuestionnaireResponse()).isNotNull() + assertThat(this.getQuestionnaireResponse().item.any { it.linkId == "a-link-id" }).isTrue() + } + } +} From 5fff50d9ffb04df87b544d89c1e0f85dcb4e77d8 Mon Sep 17 00:00:00 2001 From: maimoonak Date: Fri, 20 May 2022 13:34:06 +0500 Subject: [PATCH 12/34] test coverage for quantity types --- .../MoreQuestionnaireItemComponentsTest.kt | 58 +++++++++++++++++++ ...aireItemDatePickerViewHolderFactoryTest.kt | 18 ++++++ 2 files changed, 76 insertions(+) diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponentsTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponentsTest.kt index e5fa88846f..f988c2af88 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponentsTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponentsTest.kt @@ -19,6 +19,7 @@ package com.google.android.fhir.datacapture import android.os.Build import com.google.android.fhir.datacapture.mapping.ITEM_INITIAL_EXPRESSION_URL import com.google.common.truth.Truth.assertThat +import java.math.BigDecimal import java.util.Locale import org.hl7.fhir.r4.model.BooleanType import org.hl7.fhir.r4.model.CodeType @@ -27,6 +28,7 @@ import org.hl7.fhir.r4.model.Coding import org.hl7.fhir.r4.model.Enumeration import org.hl7.fhir.r4.model.Expression import org.hl7.fhir.r4.model.Extension +import org.hl7.fhir.r4.model.Quantity import org.hl7.fhir.r4.model.Questionnaire import org.hl7.fhir.r4.model.StringType import org.hl7.fhir.r4.utils.ToolingExtensions @@ -806,6 +808,62 @@ class MoreQuestionnaireItemComponentsTest { .isEqualTo(true) } + @Test + fun createQuestionResponseWithQuantityType_ShouldNotSetAnswer_WithValueEmpty() { + val question = + Questionnaire.QuestionnaireItemComponent( + StringType("age"), + Enumeration( + Questionnaire.QuestionnaireItemTypeEnumFactory(), + Questionnaire.QuestionnaireItemType.QUANTITY + ) + ) + .apply { + initial = + listOf( + Questionnaire.QuestionnaireItemInitialComponent( + Quantity().apply { + code = "months" + system = "http://unitofmeausre.org" + } + ) + ) + } + + val questionResponse = question.createQuestionnaireResponseItem() + + assertThat(questionResponse.answer).isEmpty() + } + + @Test + fun createQuestionResponseWithQuantityType_ShouldSetAnswer() { + val question = + Questionnaire.QuestionnaireItemComponent( + StringType("age"), + Enumeration( + Questionnaire.QuestionnaireItemTypeEnumFactory(), + Questionnaire.QuestionnaireItemType.QUANTITY + ) + ) + .apply { + initial = + listOf( + Questionnaire.QuestionnaireItemInitialComponent( + Quantity().apply { + code = "months" + system = "http://unitofmeausre.org" + value = BigDecimal("1") + } + ) + ) + } + + val questionResponse = question.createQuestionnaireResponseItem() + val answer = questionResponse.answerFirstRep.value as Quantity + assertThat(answer.value).isEqualTo(BigDecimal(1)) + assertThat(answer.code).isEqualTo("months") + } + @Test fun entryFormat_missingFormat_shouldReturnNull() { val questionnaireItem = diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDatePickerViewHolderFactoryTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDatePickerViewHolderFactoryTest.kt index 473e8b385b..ec8d81c0ac 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDatePickerViewHolderFactoryTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDatePickerViewHolderFactoryTest.kt @@ -65,6 +65,24 @@ class QuestionnaireItemDatePickerViewHolderFactoryTest { .isEqualTo("") } + @Test + fun shouldSetEmptyDateInput_WhenDateFieldInitialized_AndDateIsNull() { + viewHolder.bind( + QuestionnaireItemViewItem( + Questionnaire.QuestionnaireItemComponent().apply { text = "Question?" }, + QuestionnaireResponse.QuestionnaireResponseItemComponent() + .addAnswer( + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().setValue(DateType()) + ) + ) {} + ) + + assertThat( + viewHolder.itemView.findViewById(R.id.text_input_edit_text).text.toString() + ) + .isEqualTo("") + } + @Test fun shouldSetDateInput_localeUs() { setLocale(Locale.US) From 23ed2e3810eb15a1f380b75462ed9a0f9001b0d4 Mon Sep 17 00:00:00 2001 From: maimoonak Date: Fri, 20 May 2022 14:58:00 +0500 Subject: [PATCH 13/34] questionnaire fragment test with launchInFragmentContainer --- .../android/fhir/datacapture/QuestionnaireFragmentTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireFragmentTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireFragmentTest.kt index dc8d37c549..f7ea076010 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireFragmentTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireFragmentTest.kt @@ -18,7 +18,7 @@ package com.google.android.fhir.datacapture import android.os.Build import androidx.core.os.bundleOf -import androidx.fragment.app.testing.launchFragment +import androidx.fragment.app.testing.launchFragmentInContainer import androidx.fragment.app.testing.withFragment import androidx.lifecycle.Lifecycle import ca.uhn.fhir.context.FhirContext @@ -49,7 +49,7 @@ class QuestionnaireFragmentTest { val questionnaireJson = FhirContext.forR4().newJsonParser().encodeResourceToString(questionnaire) val scenario = - launchFragment( + launchFragmentInContainer( bundleOf(EXTRA_QUESTIONNAIRE_JSON_STRING to questionnaireJson) ) scenario.moveToState(Lifecycle.State.RESUMED) From 1f97b8d3e460c356726fd2e43d1c5ab9378df405 Mon Sep 17 00:00:00 2001 From: maimoonak <4829880+maimoonak@users.noreply.github.com> Date: Mon, 30 May 2022 19:12:05 -0400 Subject: [PATCH 14/34] Update datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt Co-authored-by: aditya-07 --- .../datacapture/QuestionnaireViewModel.kt | 37 +++++++------------ 1 file changed, 13 insertions(+), 24 deletions(-) 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 e2a082d5be..6b8c7a46fc 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 @@ -207,42 +207,31 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat internal val questionnaireItemValueStateFlow = _questionnaireItemValueStateFlow.asSharedFlow() private fun runCalculatedExpressions() { - val calculableItems = - linkIdToQuestionnaireItemMap.filter { it.value.calculatedExpression != null } - calculableItems.forEach { questionnaireItem -> - linkIdToQuestionnaireResponseItemMap[questionnaireItem.key]?.let { questionnaireResponseItem - -> - val expression = questionnaireItem.value.calculatedExpression!!.expression - fhirPathEngine.evaluate(null, questionnaireResponse, null, null, expression).run { - with(this.firstOrNull()) { - // update only if answer has changed, otherwise app can stuck in loop to keep updating - // changed items - val evaluatedAnswer = this?.castToType(this) - val currentAnswer = - if (questionnaireResponseItem.hasAnswer()) - questionnaireResponseItem.answerFirstRep.value - else null - - if (!(evaluatedAnswer == null && currentAnswer == null) && - evaluatedAnswer?.equalsDeep(currentAnswer) != true - ) { - questionnaireResponseItem.answerFirstRep.value = - evaluatedAnswer?.let { it.castToType(it) } - + linkIdToQuestionnaireItemMap.filter { it.value.calculatedExpression != null } + .forEach { questionnaireItem -> + linkIdToQuestionnaireResponseItemMap[questionnaireItem.key]?.let { questionnaireResponseItem + -> + questionnaireItem.value.calculatedExpression?.expression?.let { expression -> + fhirPathEngine.evaluate(null, questionnaireResponse, null, null, expression).firstOrNull()?.let { + val evaluatedAnswer = it.castToType(it) + val currentAnswer = if (questionnaireResponseItem.hasAnswer()) questionnaireResponseItem.answerFirstRep.value else null + if (!evaluatedAnswer.equalsDeep(currentAnswer)) { + questionnaireResponseItem.answerFirstRep.value = evaluatedAnswer + } // notify UI to update it value i.e. notify item changed to adapter viewModelScope.launch { - if (modificationCount.value > 0) + if (modificationCount.value > 0) { questionnaireStateFlow.collectLatest { it.items .indexOfFirst { it.questionnaireItem.linkId == questionnaireItem.key } .let { if (it > -1) _questionnaireItemValueStateFlow.emit(it) } } + } } } } } } - } } fun detectCalculatedExpressionCyclicDependency() { From 9e3181904f232baf0559465a367aed06d455019e Mon Sep 17 00:00:00 2001 From: maimoonak Date: Tue, 31 May 2022 18:02:51 +0500 Subject: [PATCH 15/34] Move widget to LayoutList | Run Calculation after state-change --- .../calculated_expression_questionnaire.json | 5 +- .../fhir/catalog/ComponentListViewModel.kt | 5 -- .../fhir/catalog/LayoutListViewModel.kt | 5 ++ .../datacapture/QuestionnaireViewModel.kt | 63 ++++++++++++------- 4 files changed, 48 insertions(+), 30 deletions(-) diff --git a/catalog/src/main/assets/calculated_expression_questionnaire.json b/catalog/src/main/assets/calculated_expression_questionnaire.json index 3e72d87668..c09021b0dc 100644 --- a/catalog/src/main/assets/calculated_expression_questionnaire.json +++ b/catalog/src/main/assets/calculated_expression_questionnaire.json @@ -5,6 +5,7 @@ "linkId": "a-birthdate", "text": "Birth Date", "type": "date", + "readOnly": true, "extension": [ { "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-calculatedExpression", @@ -22,9 +23,9 @@ "type": "quantity", "initial": [{ "valueQuantity": { - "unit": "months", + "unit": "years", "system": "http://unitsofmeasure.org", - "code": "months" + "code": "years" } }] }, diff --git a/catalog/src/main/java/com/google/android/fhir/catalog/ComponentListViewModel.kt b/catalog/src/main/java/com/google/android/fhir/catalog/ComponentListViewModel.kt index ab0abfdacd..c1f9f72bae 100644 --- a/catalog/src/main/java/com/google/android/fhir/catalog/ComponentListViewModel.kt +++ b/catalog/src/main/java/com/google/android/fhir/catalog/ComponentListViewModel.kt @@ -78,10 +78,5 @@ class ComponentListViewModel(application: Application, private val state: SavedS ), SLIDER(R.drawable.ic_slider, R.string.component_name_slider, "slider_questionnaire.json"), IMAGE(R.drawable.ic_image, R.string.component_name_image, ""), - CALCULATED_EXPRESSION( - R.drawable.ic_unitoptions, - R.string.component_name_calculated_expression, - "calculated_expression_questionnaire.json" - ), } } diff --git a/catalog/src/main/java/com/google/android/fhir/catalog/LayoutListViewModel.kt b/catalog/src/main/java/com/google/android/fhir/catalog/LayoutListViewModel.kt index b734ecf614..2ffd979a67 100644 --- a/catalog/src/main/java/com/google/android/fhir/catalog/LayoutListViewModel.kt +++ b/catalog/src/main/java/com/google/android/fhir/catalog/LayoutListViewModel.kt @@ -48,5 +48,10 @@ class LayoutListViewModel(application: Application, private val state: SavedStat ), REVIEW(R.drawable.ic_reviewlayout, R.string.layout_name_review, ""), READ_ONLY(R.drawable.ic_readonlylayout, R.string.layout_name_read_only, ""), + CALCULATED_EXPRESSION( + R.drawable.ic_unitoptions, + R.string.component_name_calculated_expression, + "calculated_expression_questionnaire.json" + ), } } 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 6b8c7a46fc..c020858456 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 @@ -150,9 +150,6 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat } } } - - runCalculatedExpressions() - modificationCount.value += 1 } @@ -188,6 +185,7 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat questionnaireResponseItemList = questionnaireResponse.item, pagination = pagination, ) + .also { runCalculatedExpressions() } } .stateIn( viewModelScope, @@ -207,34 +205,50 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat internal val questionnaireItemValueStateFlow = _questionnaireItemValueStateFlow.asSharedFlow() private fun runCalculatedExpressions() { - linkIdToQuestionnaireItemMap.filter { it.value.calculatedExpression != null } - .forEach { questionnaireItem -> - linkIdToQuestionnaireResponseItemMap[questionnaireItem.key]?.let { questionnaireResponseItem - -> - questionnaireItem.value.calculatedExpression?.expression?.let { expression -> - fhirPathEngine.evaluate(null, questionnaireResponse, null, null, expression).firstOrNull()?.let { - val evaluatedAnswer = it.castToType(it) - val currentAnswer = if (questionnaireResponseItem.hasAnswer()) questionnaireResponseItem.answerFirstRep.value else null - if (!evaluatedAnswer.equalsDeep(currentAnswer)) { + linkIdToQuestionnaireItemMap.filter { it.value.calculatedExpression != null }.forEach { + questionnaireItem -> + linkIdToQuestionnaireResponseItemMap[questionnaireItem.key]?.let { questionnaireResponseItem + -> + questionnaireItem.value.calculatedExpression?.expression?.let { expression -> + fhirPathEngine + .evaluate(null, questionnaireResponse, null, null, expression) + .firstOrNull() + .let { + val evaluatedAnswer = it?.castToType(it) + val currentAnswer = + if (questionnaireResponseItem.hasAnswer()) + questionnaireResponseItem.answerFirstRep.value + else null + + // update and notify only if answer has changed to prevent any loop or needless + // iterations + // the answer must be changed if both answers are different i.e. any of them is null + // or has a different value + if (!(evaluatedAnswer == null && currentAnswer == null) && + evaluatedAnswer?.equalsDeep(currentAnswer) != true + ) { questionnaireResponseItem.answerFirstRep.value = evaluatedAnswer - } - // notify UI to update it value i.e. notify item changed to adapter - viewModelScope.launch { - if (modificationCount.value > 0) { - questionnaireStateFlow.collectLatest { - it.items - .indexOfFirst { it.questionnaireItem.linkId == questionnaireItem.key } - .let { if (it > -1) _questionnaireItemValueStateFlow.emit(it) } + 1 + // notify UI to update it value i.e. notify item changed to adapter + viewModelScope.launch { + if (modificationCount.value > 0) { + questionnaireStateFlow.collectLatest { + it.items + .indexOfFirst { it.questionnaireItem.linkId == questionnaireItem.key } + .let { if (it > -1) _questionnaireItemValueStateFlow.emit(it) } + } } } } } - } } } + } } - fun detectCalculatedExpressionCyclicDependency() { + fun detectCalculatedExpressionCyclicDependency( + linkIdToQuestionnaireItemMap: Map + ) { val calculableItems = linkIdToQuestionnaireItemMap.filter { it.value.calculatedExpression != null } calculableItems.forEach { current -> @@ -305,6 +319,7 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat ) } } + return linkIdToQuestionnaireResponseItemMap } @@ -316,6 +331,9 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat for (item in questionnaireItemList) { linkIdToQuestionnaireItemMap.putAll(createLinkIdToQuestionnaireItemMap(item.item)) } + + detectCalculatedExpressionCyclicDependency(linkIdToQuestionnaireItemMap) + return linkIdToQuestionnaireItemMap } @@ -435,7 +453,6 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat * when first opening this questionnaire. Otherwise, returns `null`. */ private fun Questionnaire.getInitialPagination(): QuestionnairePagination? { - detectCalculatedExpressionCyclicDependency() runCalculatedExpressions() val usesPagination = From 501f4ef680ff4a1e85f890809d8ae06b94638d3f Mon Sep 17 00:00:00 2001 From: maimoonak Date: Tue, 31 May 2022 20:17:24 +0500 Subject: [PATCH 16/34] Revert the run-expression after state-flow --- .../android/fhir/datacapture/QuestionnaireViewModel.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 c020858456..1587ce62d1 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 @@ -151,6 +151,8 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat } } modificationCount.value += 1 + + runCalculatedExpressions() } private val pageFlow = MutableStateFlow(questionnaire.getInitialPagination()) @@ -185,7 +187,6 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat questionnaireResponseItemList = questionnaireResponse.item, pagination = pagination, ) - .also { runCalculatedExpressions() } } .stateIn( viewModelScope, @@ -196,6 +197,7 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat questionnaireResponseItemList = questionnaireResponse.item, pagination = questionnaire.getInitialPagination(), ) + .also { runCalculatedExpressions() } ) /** @@ -228,7 +230,7 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat evaluatedAnswer?.equalsDeep(currentAnswer) != true ) { questionnaireResponseItem.answerFirstRep.value = evaluatedAnswer - 1 + // notify UI to update it value i.e. notify item changed to adapter viewModelScope.launch { if (modificationCount.value > 0) { @@ -453,8 +455,6 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat * when first opening this questionnaire. Otherwise, returns `null`. */ private fun Questionnaire.getInitialPagination(): QuestionnairePagination? { - runCalculatedExpressions() - val usesPagination = item.any { item -> item.extension.any { extension -> From 041496164580e03e1aca9abc1497729e0a9b0634 Mon Sep 17 00:00:00 2001 From: maimoonak Date: Tue, 5 Jul 2022 00:55:36 +0500 Subject: [PATCH 17/34] remove empty line changes --- .../fhir/catalog/ComponentListViewModel.kt | 5 + .../fhir/catalog/LayoutListViewModel.kt | 5 - ...xtQuantityViewHolderFactoryEspressoTest.kt | 134 ++++++++++++++++++ .../datacapture/QuestionnaireViewModel.kt | 11 +- ...reItemEditTextQuantityViewHolderFactory.kt | 4 +- .../QuestionnaireItemViewHolderFactory.kt | 2 +- .../MoreQuestionnaireItemComponentsTest.kt | 31 ++++ ...emEditTextQuantityViewHolderFactoryTest.kt | 41 ------ 8 files changed, 176 insertions(+), 57 deletions(-) create mode 100644 datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest.kt diff --git a/catalog/src/main/java/com/google/android/fhir/catalog/ComponentListViewModel.kt b/catalog/src/main/java/com/google/android/fhir/catalog/ComponentListViewModel.kt index 7dbd958037..06fe2bf91f 100644 --- a/catalog/src/main/java/com/google/android/fhir/catalog/ComponentListViewModel.kt +++ b/catalog/src/main/java/com/google/android/fhir/catalog/ComponentListViewModel.kt @@ -83,5 +83,10 @@ class ComponentListViewModel(application: Application, private val state: SavedS R.string.component_name_auto_complete, "auto_complete_questionnaire.json" ), + CALCULATED_EXPRESSION( + R.drawable.ic_unitoptions, + R.string.component_name_calculated_expression, + "calculated_expression_questionnaire.json" + ), } } diff --git a/catalog/src/main/java/com/google/android/fhir/catalog/LayoutListViewModel.kt b/catalog/src/main/java/com/google/android/fhir/catalog/LayoutListViewModel.kt index 2ffd979a67..b734ecf614 100644 --- a/catalog/src/main/java/com/google/android/fhir/catalog/LayoutListViewModel.kt +++ b/catalog/src/main/java/com/google/android/fhir/catalog/LayoutListViewModel.kt @@ -48,10 +48,5 @@ class LayoutListViewModel(application: Application, private val state: SavedStat ), REVIEW(R.drawable.ic_reviewlayout, R.string.layout_name_review, ""), READ_ONLY(R.drawable.ic_readonlylayout, R.string.layout_name_read_only, ""), - CALCULATED_EXPRESSION( - R.drawable.ic_unitoptions, - R.string.component_name_calculated_expression, - "calculated_expression_questionnaire.json" - ), } } diff --git a/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest.kt b/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest.kt new file mode 100644 index 0000000000..3900941cee --- /dev/null +++ b/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest.kt @@ -0,0 +1,134 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.fhir.datacapture.views + +import android.view.View +import android.widget.FrameLayout +import android.widget.TextView +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.action.ViewActions.typeText +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.RootMatchers.isPlatformPopup +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.platform.app.InstrumentationRegistry +import com.google.android.fhir.datacapture.R +import com.google.android.fhir.datacapture.TestActivity +import com.google.android.fhir.datacapture.utilities.showDropDown +import com.google.common.truth.Truth.assertThat +import org.hl7.fhir.r4.model.Coding +import org.hl7.fhir.r4.model.Quantity +import org.hl7.fhir.r4.model.Questionnaire +import org.hl7.fhir.r4.model.QuestionnaireResponse +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import java.math.BigDecimal + +class QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest { + @Rule + @JvmField + var activityScenarioRule: ActivityScenarioRule = + ActivityScenarioRule(TestActivity::class.java) + + private lateinit var parent: FrameLayout + private lateinit var viewHolder: QuestionnaireItemViewHolder + + @Before + fun setup() { + activityScenarioRule.scenario.onActivity { activity -> parent = FrameLayout(activity) } + viewHolder = QuestionnaireItemEditTextQuantityViewHolderFactory.create(parent) + setTestLayout(viewHolder.itemView) + } + + @Test + fun getValue_WithInitial_shouldReturnQuantityWithUnitAndSystem() { + val questionnaireItemViewItem = QuestionnaireItemViewItem( + Questionnaire.QuestionnaireItemComponent().apply { + required = true + addInitial( + Questionnaire.QuestionnaireItemInitialComponent( + Quantity().apply { + code = "months" + system = "http://unitofmeasure.com" + } + ) + ) + }, + QuestionnaireResponse.QuestionnaireResponseItemComponent() + ) {} + runOnUI { viewHolder.bind(questionnaireItemViewItem) } + + onView(withId(R.id.text_input_edit_text)).perform(click()) + onView(withId(R.id.text_input_edit_text)).perform(typeText("22")) + assertThat(viewHolder.itemView.findViewById(R.id.text_input_edit_text).text.toString()) + .isEqualTo("22") + + val delegateValue = + (viewHolder.delegate as QuestionnaireItemEditTextViewHolderDelegate).getValue("22")!! + .valueQuantity + assertThat(delegateValue.code).isEqualTo("months") + assertThat(delegateValue.system).isEqualTo("http://unitofmeasure.com") + assertThat(delegateValue.value).isEqualTo(BigDecimal(22)) + + val responseValue = questionnaireItemViewItem.questionnaireResponseItem.answer.first().valueQuantity + assertThat(responseValue.code).isEqualTo("months") + assertThat(responseValue.system).isEqualTo("http://unitofmeasure.com") + assertThat(responseValue.value).isEqualTo(BigDecimal(22)) + } + + @Test + fun getValue_WithoutInitial_shouldReturnQuantityWithoutUnitAndSystem() { + val questionnaireItemViewItem = + QuestionnaireItemViewItem( + Questionnaire.QuestionnaireItemComponent().apply { required = true }, + QuestionnaireResponse.QuestionnaireResponseItemComponent() + ) {} + runOnUI { viewHolder.bind(questionnaireItemViewItem) } + + onView(withId(R.id.text_input_edit_text)).perform(click()) + onView(withId(R.id.text_input_edit_text)).perform(typeText("22")) + assertThat(viewHolder.itemView.findViewById(R.id.text_input_edit_text).text.toString()) + .isEqualTo("22") + + val delegateValue = + (viewHolder.delegate as QuestionnaireItemEditTextViewHolderDelegate).getValue("22")!! + .valueQuantity + assertThat(delegateValue.code).isNull() + assertThat(delegateValue.system).isNull() + assertThat(delegateValue.value).isEqualTo(BigDecimal(22)) + + val responseValue = questionnaireItemViewItem.questionnaireResponseItem.answer.first().valueQuantity + assertThat(delegateValue.code).isNull() + assertThat(delegateValue.system).isNull() + assertThat(responseValue.value).isEqualTo(BigDecimal(22)) + } + + /** Method to run code snippet on UI/main thread */ + private fun runOnUI(action: () -> Unit) { + activityScenarioRule.scenario.onActivity { action() } + } + + /** Method to set content view for test activity */ + private fun setTestLayout(view: View) { + activityScenarioRule.scenario.onActivity { activity -> activity.setContentView(view) } + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + } +} 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 1587ce62d1..c9aa25cc9e 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 @@ -34,9 +34,7 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext import org.hl7.fhir.r4.model.CodeableConcept @@ -261,10 +259,9 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat } // if any calculable expression depends on this item and this item is referring to the // dependent item in its own expression then raise error - if (otherDependent != null && currentExpression.contains("'${otherDependent.linkId}'")) - throw IllegalStateException( - "${current.key} and ${otherDependent.linkId} have cyclic dependency in calculated-expression extension" - ) + check(otherDependent != null && currentExpression.contains("'${otherDependent.linkId}'")) { + "${current.key} and ${otherDependent!!.linkId} have cyclic dependency in calculated-expression extension" + } } } @@ -321,7 +318,6 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat ) } } - return linkIdToQuestionnaireResponseItemMap } @@ -416,7 +412,6 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat .items } .toList() - return QuestionnaireState(items = items, pagination = pagination) } diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactory.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactory.kt index 90186f7b7c..29f5ce6429 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactory.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactory.kt @@ -38,7 +38,7 @@ internal object QuestionnaireItemEditTextQuantityViewHolderFactory : ): QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent? { // https://build.fhir.org/ig/HL7/sdc/behavior.html#initial // read default unit from initial, as ideally quantity must specify a unit - return text.toDoubleOrNull()?.let { + return text.let { val quantity = with(questionnaireItemViewItem.questionnaireItem) { if (this.hasInitial() && this.initialFirstRep.valueQuantity.hasCode()) @@ -49,7 +49,7 @@ internal object QuestionnaireItemEditTextQuantityViewHolderFactory : this.system = initial.system } } - else Quantity(it) + else Quantity().apply { this.value = BigDecimal(text) } } QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().setValue(quantity) } diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemViewHolderFactory.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemViewHolderFactory.kt index 48a990626a..f1d117c5be 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemViewHolderFactory.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemViewHolderFactory.kt @@ -52,7 +52,7 @@ abstract class QuestionnaireItemViewHolderFactory(@LayoutRes val resId: Int) { */ open class QuestionnaireItemViewHolder( itemView: View, - internal val delegate: QuestionnaireItemViewHolderDelegate + private val delegate: QuestionnaireItemViewHolderDelegate ) : RecyclerView.ViewHolder(itemView) { init { delegate.init(itemView) diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponentsTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponentsTest.kt index f988c2af88..9e988fca00 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponentsTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponentsTest.kt @@ -614,6 +614,37 @@ class MoreQuestionnaireItemComponentsTest { assertThat(questionItem.itemFirstRep.enableWhenExpression).isNull() } + @Test + fun calculatedExpression_shouldReturnExpression() { + val questionnaire = + Questionnaire.QuestionnaireItemComponent().apply { + addExtension( + EXTENSION_CALCULATED_EXPRESSION_URL, + Expression().apply { + this.expression = "today()" + this.language = "text/fhirpath" + } + ) + } + assertThat(questionnaire.calculatedExpression).isNotNull() + assertThat(questionnaire.calculatedExpression!!.expression).isEqualTo("today()") + } + + @Test + fun calculatedExpression_shouldReturnNull() { + val questionnaire = + Questionnaire.QuestionnaireItemComponent().apply { + addExtension( + ITEM_INITIAL_EXPRESSION_URL, + Expression().apply { + this.expression = "today()" + this.language = "text/fhirpath" + } + ) + } + assertThat(questionnaire.calculatedExpression).isNull() + } + @Test fun localizedFlyoverSpanned_matchingLocale_shouldReturnFlyover() { val questionItemList = diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactoryTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactoryTest.kt index 7b94616762..1c29f3ddb1 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactoryTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactoryTest.kt @@ -212,45 +212,4 @@ class QuestionnaireItemEditTextQuantityViewHolderFactoryTest { ) .isFalse() } - - @Test - fun getValue_WithInitial_shouldReturnQuantityWithUnitAndSystem() { - viewHolder.bind( - QuestionnaireItemViewItem( - Questionnaire.QuestionnaireItemComponent().apply { - required = true - addInitial( - Questionnaire.QuestionnaireItemInitialComponent( - Quantity().apply { - code = "months" - system = "http://unitofmeasure.com" - } - ) - ) - }, - QuestionnaireResponse.QuestionnaireResponseItemComponent() - ) {} - ) - val value = - (viewHolder.delegate as QuestionnaireItemEditTextViewHolderDelegate).getValue("22")!! - .valueQuantity - assertThat(value.code).isEqualTo("months") - assertThat(value.system).isEqualTo("http://unitofmeasure.com") - assertThat(value.value).isEqualTo(BigDecimal(22)) - } - - fun getValue_WithoutInitial_shouldReturnQuantityWithoutUnitAndSystem() { - viewHolder.bind( - QuestionnaireItemViewItem( - Questionnaire.QuestionnaireItemComponent().apply { required = true }, - QuestionnaireResponse.QuestionnaireResponseItemComponent() - ) {} - ) - val value = - (viewHolder.delegate as QuestionnaireItemEditTextViewHolderDelegate).getValue("22")!! - .valueQuantity - assertThat(value.code).isNull() - assertThat(value.system).isNull() - assertThat(value.value).isEqualTo(BigDecimal(22)) - } } From bbda724553c9cb242853e6963b155098caeaa7ce Mon Sep 17 00:00:00 2001 From: maimoonak Date: Tue, 5 Jul 2022 01:01:12 +0500 Subject: [PATCH 18/34] spotless fix --- .../fhir/catalog/ComponentListViewModel.kt | 8 +- ...xtQuantityViewHolderFactoryEspressoTest.kt | 73 ++++++++++--------- .../MoreQuestionnaireItemComponents.kt | 2 +- .../fhir/datacapture/QuestionnaireFragment.kt | 2 +- .../datacapture/mapping/ResourceMapper.kt | 2 +- ...ionnaireItemDatePickerViewHolderFactory.kt | 2 +- ...reItemEditTextQuantityViewHolderFactory.kt | 2 +- .../MoreQuestionnaireItemComponentsTest.kt | 62 ++++++++-------- .../datacapture/QuestionnaireFragmentTest.kt | 2 +- ...aireItemDatePickerViewHolderFactoryTest.kt | 2 +- 10 files changed, 79 insertions(+), 78 deletions(-) diff --git a/catalog/src/main/java/com/google/android/fhir/catalog/ComponentListViewModel.kt b/catalog/src/main/java/com/google/android/fhir/catalog/ComponentListViewModel.kt index 06fe2bf91f..04972903eb 100644 --- a/catalog/src/main/java/com/google/android/fhir/catalog/ComponentListViewModel.kt +++ b/catalog/src/main/java/com/google/android/fhir/catalog/ComponentListViewModel.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Google LLC + * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -84,9 +84,9 @@ class ComponentListViewModel(application: Application, private val state: SavedS "auto_complete_questionnaire.json" ), CALCULATED_EXPRESSION( - R.drawable.ic_unitoptions, - R.string.component_name_calculated_expression, - "calculated_expression_questionnaire.json" + R.drawable.ic_unitoptions, + R.string.component_name_calculated_expression, + "calculated_expression_questionnaire.json" ), } } diff --git a/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest.kt b/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest.kt index 3900941cee..44cd10dfca 100644 --- a/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest.kt +++ b/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Google LLC + * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,25 +22,19 @@ import android.widget.TextView import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.typeText -import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.matcher.RootMatchers.isPlatformPopup -import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withId -import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.platform.app.InstrumentationRegistry import com.google.android.fhir.datacapture.R import com.google.android.fhir.datacapture.TestActivity -import com.google.android.fhir.datacapture.utilities.showDropDown import com.google.common.truth.Truth.assertThat -import org.hl7.fhir.r4.model.Coding +import java.math.BigDecimal import org.hl7.fhir.r4.model.Quantity import org.hl7.fhir.r4.model.Questionnaire import org.hl7.fhir.r4.model.QuestionnaireResponse import org.junit.Before import org.junit.Rule import org.junit.Test -import java.math.BigDecimal class QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest { @Rule @@ -60,35 +54,39 @@ class QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest { @Test fun getValue_WithInitial_shouldReturnQuantityWithUnitAndSystem() { - val questionnaireItemViewItem = QuestionnaireItemViewItem( - Questionnaire.QuestionnaireItemComponent().apply { - required = true - addInitial( - Questionnaire.QuestionnaireItemInitialComponent( - Quantity().apply { - code = "months" - system = "http://unitofmeasure.com" - } - ) - ) - }, - QuestionnaireResponse.QuestionnaireResponseItemComponent() - ) {} + val questionnaireItemViewItem = + QuestionnaireItemViewItem( + Questionnaire.QuestionnaireItemComponent().apply { + required = true + addInitial( + Questionnaire.QuestionnaireItemInitialComponent( + Quantity().apply { + code = "months" + system = "http://unitofmeasure.com" + } + ) + ) + }, + QuestionnaireResponse.QuestionnaireResponseItemComponent() + ) {} runOnUI { viewHolder.bind(questionnaireItemViewItem) } onView(withId(R.id.text_input_edit_text)).perform(click()) onView(withId(R.id.text_input_edit_text)).perform(typeText("22")) - assertThat(viewHolder.itemView.findViewById(R.id.text_input_edit_text).text.toString()) - .isEqualTo("22") + assertThat( + viewHolder.itemView.findViewById(R.id.text_input_edit_text).text.toString() + ) + .isEqualTo("22") val delegateValue = - (viewHolder.delegate as QuestionnaireItemEditTextViewHolderDelegate).getValue("22")!! - .valueQuantity + (viewHolder.delegate as QuestionnaireItemEditTextViewHolderDelegate).getValue("22")!! + .valueQuantity assertThat(delegateValue.code).isEqualTo("months") assertThat(delegateValue.system).isEqualTo("http://unitofmeasure.com") assertThat(delegateValue.value).isEqualTo(BigDecimal(22)) - val responseValue = questionnaireItemViewItem.questionnaireResponseItem.answer.first().valueQuantity + val responseValue = + questionnaireItemViewItem.questionnaireResponseItem.answer.first().valueQuantity assertThat(responseValue.code).isEqualTo("months") assertThat(responseValue.system).isEqualTo("http://unitofmeasure.com") assertThat(responseValue.value).isEqualTo(BigDecimal(22)) @@ -97,25 +95,28 @@ class QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest { @Test fun getValue_WithoutInitial_shouldReturnQuantityWithoutUnitAndSystem() { val questionnaireItemViewItem = - QuestionnaireItemViewItem( - Questionnaire.QuestionnaireItemComponent().apply { required = true }, - QuestionnaireResponse.QuestionnaireResponseItemComponent() - ) {} + QuestionnaireItemViewItem( + Questionnaire.QuestionnaireItemComponent().apply { required = true }, + QuestionnaireResponse.QuestionnaireResponseItemComponent() + ) {} runOnUI { viewHolder.bind(questionnaireItemViewItem) } onView(withId(R.id.text_input_edit_text)).perform(click()) onView(withId(R.id.text_input_edit_text)).perform(typeText("22")) - assertThat(viewHolder.itemView.findViewById(R.id.text_input_edit_text).text.toString()) - .isEqualTo("22") + assertThat( + viewHolder.itemView.findViewById(R.id.text_input_edit_text).text.toString() + ) + .isEqualTo("22") val delegateValue = - (viewHolder.delegate as QuestionnaireItemEditTextViewHolderDelegate).getValue("22")!! - .valueQuantity + (viewHolder.delegate as QuestionnaireItemEditTextViewHolderDelegate).getValue("22")!! + .valueQuantity assertThat(delegateValue.code).isNull() assertThat(delegateValue.system).isNull() assertThat(delegateValue.value).isEqualTo(BigDecimal(22)) - val responseValue = questionnaireItemViewItem.questionnaireResponseItem.answer.first().valueQuantity + val responseValue = + questionnaireItemViewItem.questionnaireResponseItem.answer.first().valueQuantity assertThat(delegateValue.code).isNull() assertThat(delegateValue.system).isNull() assertThat(responseValue.value).isEqualTo(BigDecimal(22)) diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponents.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponents.kt index 5cd33dd4dd..0c4a7c60ef 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponents.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponents.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Google LLC + * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. 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 75c22915aa..3bc4936f01 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 @@ -1,5 +1,5 @@ /* - * Copyright 2021 Google LLC + * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. 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 79ef73515d..9d37b5bdd8 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 @@ -1,5 +1,5 @@ /* - * Copyright 2021 Google LLC + * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDatePickerViewHolderFactory.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDatePickerViewHolderFactory.kt index 4aa9ee3a88..c2cda1a886 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDatePickerViewHolderFactory.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDatePickerViewHolderFactory.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Google LLC + * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactory.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactory.kt index 29f5ce6429..bb9968ce05 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactory.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactory.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Google LLC + * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponentsTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponentsTest.kt index 9e988fca00..c5b1e20e7c 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponentsTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponentsTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Google LLC + * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -614,36 +614,36 @@ class MoreQuestionnaireItemComponentsTest { assertThat(questionItem.itemFirstRep.enableWhenExpression).isNull() } - @Test - fun calculatedExpression_shouldReturnExpression() { - val questionnaire = - Questionnaire.QuestionnaireItemComponent().apply { - addExtension( - EXTENSION_CALCULATED_EXPRESSION_URL, - Expression().apply { - this.expression = "today()" - this.language = "text/fhirpath" - } - ) - } - assertThat(questionnaire.calculatedExpression).isNotNull() - assertThat(questionnaire.calculatedExpression!!.expression).isEqualTo("today()") - } - - @Test - fun calculatedExpression_shouldReturnNull() { - val questionnaire = - Questionnaire.QuestionnaireItemComponent().apply { - addExtension( - ITEM_INITIAL_EXPRESSION_URL, - Expression().apply { - this.expression = "today()" - this.language = "text/fhirpath" - } - ) - } - assertThat(questionnaire.calculatedExpression).isNull() - } + @Test + fun calculatedExpression_shouldReturnExpression() { + val questionnaire = + Questionnaire.QuestionnaireItemComponent().apply { + addExtension( + EXTENSION_CALCULATED_EXPRESSION_URL, + Expression().apply { + this.expression = "today()" + this.language = "text/fhirpath" + } + ) + } + assertThat(questionnaire.calculatedExpression).isNotNull() + assertThat(questionnaire.calculatedExpression!!.expression).isEqualTo("today()") + } + + @Test + fun calculatedExpression_shouldReturnNull() { + val questionnaire = + Questionnaire.QuestionnaireItemComponent().apply { + addExtension( + ITEM_INITIAL_EXPRESSION_URL, + Expression().apply { + this.expression = "today()" + this.language = "text/fhirpath" + } + ) + } + assertThat(questionnaire.calculatedExpression).isNull() + } @Test fun localizedFlyoverSpanned_matchingLocale_shouldReturnFlyover() { diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireFragmentTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireFragmentTest.kt index f7ea076010..5e34825194 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireFragmentTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireFragmentTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Google LLC + * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDatePickerViewHolderFactoryTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDatePickerViewHolderFactoryTest.kt index ec8d81c0ac..5d1e84600b 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDatePickerViewHolderFactoryTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDatePickerViewHolderFactoryTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Google LLC + * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From 1fd726cde059bdc1223c2c5589913db0057cd320 Mon Sep 17 00:00:00 2001 From: maimoonak Date: Tue, 5 Jul 2022 04:29:11 +0500 Subject: [PATCH 19/34] Esperesso test | Fix failing test --- ...extQuantityViewHolderFactoryEspressoTest.kt | 18 ++---------------- .../fhir/datacapture/QuestionnaireViewModel.kt | 16 +++++++--------- ...ireItemEditTextQuantityViewHolderFactory.kt | 7 ++++--- .../QuestionnaireItemViewHolderFactory.kt | 4 +++- ...temEditTextQuantityViewHolderFactoryTest.kt | 4 ++-- 5 files changed, 18 insertions(+), 31 deletions(-) diff --git a/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest.kt b/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest.kt index 44cd10dfca..ed06863a77 100644 --- a/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest.kt +++ b/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest.kt @@ -78,13 +78,6 @@ class QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest { ) .isEqualTo("22") - val delegateValue = - (viewHolder.delegate as QuestionnaireItemEditTextViewHolderDelegate).getValue("22")!! - .valueQuantity - assertThat(delegateValue.code).isEqualTo("months") - assertThat(delegateValue.system).isEqualTo("http://unitofmeasure.com") - assertThat(delegateValue.value).isEqualTo(BigDecimal(22)) - val responseValue = questionnaireItemViewItem.questionnaireResponseItem.answer.first().valueQuantity assertThat(responseValue.code).isEqualTo("months") @@ -108,17 +101,10 @@ class QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest { ) .isEqualTo("22") - val delegateValue = - (viewHolder.delegate as QuestionnaireItemEditTextViewHolderDelegate).getValue("22")!! - .valueQuantity - assertThat(delegateValue.code).isNull() - assertThat(delegateValue.system).isNull() - assertThat(delegateValue.value).isEqualTo(BigDecimal(22)) - val responseValue = questionnaireItemViewItem.questionnaireResponseItem.answer.first().valueQuantity - assertThat(delegateValue.code).isNull() - assertThat(delegateValue.system).isNull() + assertThat(responseValue.code).isNull() + assertThat(responseValue.system).isNull() assertThat(responseValue.value).isEqualTo(BigDecimal(22)) } 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 da1b9ff6c1..e337f21946 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 @@ -150,9 +150,9 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat } } } - modificationCount.value += 1 - runCalculatedExpressions() + + modificationCount.value += 1 } private val pageFlow = MutableStateFlow(questionnaire.getInitialPagination()) @@ -233,12 +233,10 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat // notify UI to update it value i.e. notify item changed to adapter viewModelScope.launch { - if (modificationCount.value > 0) { - questionnaireStateFlow.collectLatest { - it.items - .indexOfFirst { it.questionnaireItem.linkId == questionnaireItem.key } - .let { if (it > -1) _questionnaireItemValueStateFlow.emit(it) } - } + questionnaireStateFlow.collectLatest { + it.items + .indexOfFirst { it.questionnaireItem.linkId == questionnaireItem.key } + .let { if (it > -1) _questionnaireItemValueStateFlow.emit(it) } } } } @@ -261,7 +259,7 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat } // if any calculable expression depends on this item and this item is referring to the // dependent item in its own expression then raise error - check(otherDependent != null && currentExpression.contains("'${otherDependent.linkId}'")) { + check(otherDependent == null || !currentExpression.contains("'${otherDependent.linkId}'")) { "${current.key} and ${otherDependent!!.linkId} have cyclic dependency in calculated-expression extension" } } diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactory.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactory.kt index bb9968ce05..c209af5194 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactory.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactory.kt @@ -38,18 +38,19 @@ internal object QuestionnaireItemEditTextQuantityViewHolderFactory : ): QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent? { // https://build.fhir.org/ig/HL7/sdc/behavior.html#initial // read default unit from initial, as ideally quantity must specify a unit - return text.let { + return text.takeIf { it.isNotBlank() }?.let { + val value = BigDecimal(text) val quantity = with(questionnaireItemViewItem.questionnaireItem) { if (this.hasInitial() && this.initialFirstRep.valueQuantity.hasCode()) this.initialFirstRep.valueQuantity.let { initial -> Quantity().apply { - this.value = BigDecimal(text) + this.value = value this.code = initial.code this.system = initial.system } } - else Quantity().apply { this.value = BigDecimal(text) } + else Quantity().apply { this.value = value } } QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().setValue(quantity) } diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemViewHolderFactory.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemViewHolderFactory.kt index f1d117c5be..c244277d64 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemViewHolderFactory.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemViewHolderFactory.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Google LLC + * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ import androidx.annotation.LayoutRes import androidx.recyclerview.widget.RecyclerView import com.google.android.fhir.datacapture.validation.QuestionnaireResponseItemValidator import com.google.android.fhir.datacapture.validation.ValidationResult +import com.google.common.annotations.VisibleForTesting /** * Factory for [QuestionnaireItemViewHolder]. @@ -52,6 +53,7 @@ abstract class QuestionnaireItemViewHolderFactory(@LayoutRes val resId: Int) { */ open class QuestionnaireItemViewHolder( itemView: View, + @org.jetbrains.annotations.VisibleForTesting private val delegate: QuestionnaireItemViewHolderDelegate ) : RecyclerView.ViewHolder(itemView) { init { diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactoryTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactoryTest.kt index 1c29f3ddb1..8ceb952a7b 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactoryTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactoryTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Google LLC + * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -120,7 +120,7 @@ class QuestionnaireItemEditTextQuantityViewHolderFactoryTest { val answer = questionnaireItemViewItem.questionnaireResponseItem.answer assertThat(answer.size).isEqualTo(1) - assertThat(answer[0].valueQuantity!!.value!!.toString()).isEqualTo("10.0") + assertThat(answer[0].valueQuantity!!.value!!.toString()).isEqualTo("10") } @Test From e738864c46f589fcb835d7f5900d947fbbae0620 Mon Sep 17 00:00:00 2001 From: maimoonak <4829880+maimoonak@users.noreply.github.com> Date: Wed, 20 Jul 2022 10:45:49 -0400 Subject: [PATCH 20/34] Remove unnessary changes --- .../android/fhir/datacapture/QuestionnaireFragment.kt | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) 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 a3a8bdfcde..3a48f8da09 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 @@ -1,5 +1,5 @@ /* - * Copyright 2022 Google LLC + * Copyright 2021 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -103,12 +103,6 @@ open class QuestionnaireFragment : Fragment() { } } } - - viewLifecycleOwner.lifecycleScope.launchWhenCreated { - /* TODO viewModel.questionnaireItemValueStateFlow.collect { index -> - if (!recyclerView.isComputingLayout) adapter.notifyItemChanged(index) - }*/ - } } /** From 4b4aaaf1355dd9786b4300a2bea3d0b1926c0c13 Mon Sep 17 00:00:00 2001 From: maimoonak Date: Wed, 20 Jul 2022 20:25:05 +0500 Subject: [PATCH 21/34] Fix espresso tests --- ...tTextQuantityViewHolderFactoryEspressoTest.kt | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest.kt b/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest.kt index ed06863a77..be029a2c3c 100644 --- a/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest.kt +++ b/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest.kt @@ -67,8 +67,10 @@ class QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest { ) ) }, - QuestionnaireResponse.QuestionnaireResponseItemComponent() - ) {} + QuestionnaireResponse.QuestionnaireResponseItemComponent(), + validationResult = null, + answersChangedCallback = { _, _, _ -> }, + ) runOnUI { viewHolder.bind(questionnaireItemViewItem) } onView(withId(R.id.text_input_edit_text)).perform(click()) @@ -79,7 +81,7 @@ class QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest { .isEqualTo("22") val responseValue = - questionnaireItemViewItem.questionnaireResponseItem.answer.first().valueQuantity + questionnaireItemViewItem.answers.first().valueQuantity assertThat(responseValue.code).isEqualTo("months") assertThat(responseValue.system).isEqualTo("http://unitofmeasure.com") assertThat(responseValue.value).isEqualTo(BigDecimal(22)) @@ -90,8 +92,10 @@ class QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest { val questionnaireItemViewItem = QuestionnaireItemViewItem( Questionnaire.QuestionnaireItemComponent().apply { required = true }, - QuestionnaireResponse.QuestionnaireResponseItemComponent() - ) {} + QuestionnaireResponse.QuestionnaireResponseItemComponent(), + validationResult = null, + answersChangedCallback = { _, _, _ -> }, + ) runOnUI { viewHolder.bind(questionnaireItemViewItem) } onView(withId(R.id.text_input_edit_text)).perform(click()) @@ -102,7 +106,7 @@ class QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest { .isEqualTo("22") val responseValue = - questionnaireItemViewItem.questionnaireResponseItem.answer.first().valueQuantity + questionnaireItemViewItem.answers.first().valueQuantity assertThat(responseValue.code).isNull() assertThat(responseValue.system).isNull() assertThat(responseValue.value).isEqualTo(BigDecimal(22)) From c6032aaf478436331891083b1c38e0be87088c23 Mon Sep 17 00:00:00 2001 From: maimoonak Date: Wed, 20 Jul 2022 21:38:09 +0500 Subject: [PATCH 22/34] Ignore Failing tests --- ...TextQuantityViewHolderFactoryEspressoTest.kt | 17 +++++++++-------- .../fhir/datacapture/QuestionnaireViewModel.kt | 2 +- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest.kt b/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest.kt index be029a2c3c..1a5e485224 100644 --- a/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest.kt +++ b/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest.kt @@ -33,6 +33,7 @@ import org.hl7.fhir.r4.model.Quantity import org.hl7.fhir.r4.model.Questionnaire import org.hl7.fhir.r4.model.QuestionnaireResponse import org.junit.Before +import org.junit.Ignore import org.junit.Rule import org.junit.Test @@ -53,6 +54,7 @@ class QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest { } @Test + @Ignore("EditText does not call onChange https://github.com/google/android-fhir/issues/1498") fun getValue_WithInitial_shouldReturnQuantityWithUnitAndSystem() { val questionnaireItemViewItem = QuestionnaireItemViewItem( @@ -68,8 +70,8 @@ class QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest { ) }, QuestionnaireResponse.QuestionnaireResponseItemComponent(), - validationResult = null, - answersChangedCallback = { _, _, _ -> }, + validationResult = null, + answersChangedCallback = { _, _, _ -> }, ) runOnUI { viewHolder.bind(questionnaireItemViewItem) } @@ -80,21 +82,21 @@ class QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest { ) .isEqualTo("22") - val responseValue = - questionnaireItemViewItem.answers.first().valueQuantity + val responseValue = questionnaireItemViewItem.answers.first().valueQuantity assertThat(responseValue.code).isEqualTo("months") assertThat(responseValue.system).isEqualTo("http://unitofmeasure.com") assertThat(responseValue.value).isEqualTo(BigDecimal(22)) } @Test + @Ignore("EditText does not call onChange https://github.com/google/android-fhir/issues/1498") fun getValue_WithoutInitial_shouldReturnQuantityWithoutUnitAndSystem() { val questionnaireItemViewItem = QuestionnaireItemViewItem( Questionnaire.QuestionnaireItemComponent().apply { required = true }, QuestionnaireResponse.QuestionnaireResponseItemComponent(), - validationResult = null, - answersChangedCallback = { _, _, _ -> }, + validationResult = null, + answersChangedCallback = { _, _, _ -> }, ) runOnUI { viewHolder.bind(questionnaireItemViewItem) } @@ -105,8 +107,7 @@ class QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest { ) .isEqualTo("22") - val responseValue = - questionnaireItemViewItem.answers.first().valueQuantity + val responseValue = questionnaireItemViewItem.answers.first().valueQuantity assertThat(responseValue.code).isNull() assertThat(responseValue.system).isNull() assertThat(responseValue.value).isEqualTo(BigDecimal(22)) 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 2e3e173778..4974bb6793 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 @@ -269,7 +269,7 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat Triple( item, modifiedQuestionnaireResponseItemSet.none { it.linkId == item.linkId }, - getQuestionnaireResponseItem(item.linkId) + questionnaireResponseItemPreOrderList.find { it.linkId == item.linkId } ) } .filter { calculable -> From 9302aa0c046c113178f25f8fd35b7bd8f4f66f2a Mon Sep 17 00:00:00 2001 From: maimoonak Date: Fri, 5 Aug 2022 16:23:58 +0500 Subject: [PATCH 23/34] Revert ignore test | merge main | refactor --- ...eItemEditTextQuantityViewHolderFactoryEspressoTest.kt | 3 --- .../fhir/datacapture/MoreQuestionnaireItemComponents.kt | 9 +++++++++ .../android/fhir/datacapture/QuestionnaireViewModel.kt | 5 ----- ...QuestionnaireItemEditTextQuantityViewHolderFactory.kt | 2 +- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest.kt b/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest.kt index 1a5e485224..6dfda5dc2e 100644 --- a/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest.kt +++ b/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest.kt @@ -33,7 +33,6 @@ import org.hl7.fhir.r4.model.Quantity import org.hl7.fhir.r4.model.Questionnaire import org.hl7.fhir.r4.model.QuestionnaireResponse import org.junit.Before -import org.junit.Ignore import org.junit.Rule import org.junit.Test @@ -54,7 +53,6 @@ class QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest { } @Test - @Ignore("EditText does not call onChange https://github.com/google/android-fhir/issues/1498") fun getValue_WithInitial_shouldReturnQuantityWithUnitAndSystem() { val questionnaireItemViewItem = QuestionnaireItemViewItem( @@ -89,7 +87,6 @@ class QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest { } @Test - @Ignore("EditText does not call onChange https://github.com/google/android-fhir/issues/1498") fun getValue_WithoutInitial_shouldReturnQuantityWithoutUnitAndSystem() { val questionnaireItemViewItem = QuestionnaireItemViewItem( diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponents.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponents.kt index af66a42a9f..d1a751a307 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponents.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponents.kt @@ -301,6 +301,15 @@ fun QuestionnaireResponse.QuestionnaireResponseItemComponent.addNestedItemsToAns } } +/** + * Flatten a nested list of [QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent] + * recursively and returns a flat list of all items into list embedded at any level + */ +fun List.flattened(): + List { + return this + this.flatMap { if (it.hasItem()) it.item.flattened() else it.item } +} + /** * Creates a list of [QuestionnaireResponse.QuestionnaireResponseItemComponent]s from the nested * items in the [Questionnaire.QuestionnaireItemComponent]. 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 4974bb6793..17b2495e9e 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 @@ -312,11 +312,6 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat } } - fun List.flattened(): - List { - return this + this.flatMap { if (it.hasItem()) it.item.flattened() else it.item } - } - fun detectCalculatedExpressionCyclicDependency( items: List ) { diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactory.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactory.kt index fb818e470a..cedc976d9c 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactory.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactory.kt @@ -17,8 +17,8 @@ package com.google.android.fhir.datacapture.views import android.text.InputType -import java.math.BigDecimal import com.google.android.fhir.datacapture.R +import java.math.BigDecimal import org.hl7.fhir.r4.model.Quantity import org.hl7.fhir.r4.model.QuestionnaireResponse From 77b163e3749c8a5be8db0047ecd7b2a901e6137a Mon Sep 17 00:00:00 2001 From: maimoonak Date: Fri, 2 Sep 2022 16:16:30 +0500 Subject: [PATCH 24/34] spotless fix --- .../android/fhir/datacapture/QuestionnaireViewModelTest.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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 60e473c693..fcec1b3f56 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 @@ -2946,9 +2946,7 @@ class QuestionnaireViewModelTest( createQuestionnaireViewModel(questionnaire) } assertThat(exception.message) - .isEqualTo( - "a-birthdate and a-age-years have cyclic dependency in expression based extension" - ) + .isEqualTo("a-birthdate and a-age-years have cyclic dependency in expression based extension") } private fun createQuestionnaireViewModel( From b9f40b6de0c3e427c478413e67f84bf1607c5924 Mon Sep 17 00:00:00 2001 From: maimoonak Date: Fri, 2 Sep 2022 20:43:02 +0500 Subject: [PATCH 25/34] Rename tests --- ...eItemEditTextQuantityViewHolderFactoryEspressoTest.kt | 4 ++-- .../views/QuestionnaireItemViewHolderFactory.kt | 2 -- .../datacapture/MoreQuestionnaireItemComponentsTest.kt | 8 ++++---- .../fhir/datacapture/QuestionnaireFragmentTest.kt | 2 +- .../fhir/datacapture/QuestionnaireViewModelTest.kt | 9 +++++---- .../QuestionnaireItemDatePickerViewHolderFactoryTest.kt | 2 +- 6 files changed, 13 insertions(+), 14 deletions(-) diff --git a/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest.kt b/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest.kt index 43dedf109d..9e48e3c71a 100644 --- a/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest.kt +++ b/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest.kt @@ -54,7 +54,7 @@ class QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest { } @Test - fun getValue_WithInitial_shouldReturnQuantityWithUnitAndSystem() { + fun getValue_WithInitial_shouldReturn_Quantity_With_UnitAndSystem() { val questionnaireItemViewItem = QuestionnaireItemViewItem( Questionnaire.QuestionnaireItemComponent().apply { @@ -88,7 +88,7 @@ class QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest { } @Test - fun getValue_WithoutInitial_shouldReturnQuantityWithoutUnitAndSystem() { + fun getValue_WithoutInitial_shouldReturn_Quantity_Without_UnitAndSystem() { val questionnaireItemViewItem = QuestionnaireItemViewItem( Questionnaire.QuestionnaireItemComponent().apply { required = true }, diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemViewHolderFactory.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemViewHolderFactory.kt index 6d04e041db..127e1f164b 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemViewHolderFactory.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemViewHolderFactory.kt @@ -22,7 +22,6 @@ import android.view.ViewGroup import androidx.annotation.LayoutRes import androidx.recyclerview.widget.RecyclerView import com.google.android.fhir.datacapture.validation.ValidationResult -import com.google.common.annotations.VisibleForTesting /** * Factory for [QuestionnaireItemViewHolder]. @@ -51,7 +50,6 @@ abstract class QuestionnaireItemViewHolderFactory(@LayoutRes open val resId: Int */ open class QuestionnaireItemViewHolder( itemView: View, - @org.jetbrains.annotations.VisibleForTesting private val delegate: QuestionnaireItemViewHolderDelegate ) : RecyclerView.ViewHolder(itemView) { init { diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponentsTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponentsTest.kt index c84f71cda1..759d15d1d0 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponentsTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponentsTest.kt @@ -708,7 +708,7 @@ class MoreQuestionnaireItemComponentsTest { } @Test - fun calculatedExpression_shouldReturnExpression() { + fun `calculatedExpression should return expression for valid extension url`() { val questionnaire = Questionnaire.QuestionnaireItemComponent().apply { addExtension( @@ -724,7 +724,7 @@ class MoreQuestionnaireItemComponentsTest { } @Test - fun calculatedExpression_shouldReturnNull() { + fun `calculatedExpression should return null for other extension url`() { val questionnaire = Questionnaire.QuestionnaireItemComponent().apply { addExtension( @@ -933,7 +933,7 @@ class MoreQuestionnaireItemComponentsTest { } @Test - fun createQuestionResponseWithQuantityType_ShouldNotSetAnswer_WithValueEmpty() { + fun `createQuestionResponse should not set answer for quantity type with missing value `() { val question = Questionnaire.QuestionnaireItemComponent( StringType("age"), @@ -960,7 +960,7 @@ class MoreQuestionnaireItemComponentsTest { } @Test - fun createQuestionResponseWithQuantityType_ShouldSetAnswer() { + fun `createQuestionResponse should set answer with quantity type`() { val question = Questionnaire.QuestionnaireItemComponent( StringType("age"), diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireFragmentTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireFragmentTest.kt index 5e34825194..b14ebf4d06 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireFragmentTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireFragmentTest.kt @@ -35,7 +35,7 @@ import org.robolectric.annotation.Config class QuestionnaireFragmentTest { @Test - fun testFragment_ShouldBeAbleToBuildQuestionnaireResponse() { + fun `fragment should have valid questionnaire response`() { val questionnaire = Questionnaire().apply { id = "a-questionnaire" 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 fcec1b3f56..e470ae7fc7 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 @@ -2713,7 +2713,8 @@ class QuestionnaireViewModelTest( } @Test - fun questionnaireItem_calculatedExpressionExtension_shouldCalculateValue_onStart() = runBlocking { + fun `should calculate value on start for questionnaire item with calculated expression extension`() = + runBlocking { val questionnaire = Questionnaire().apply { id = "a-questionnaire" @@ -2771,7 +2772,7 @@ class QuestionnaireViewModelTest( } @Test - fun questionnaireItem_calculatedExpressionExtension_shouldCalculateValue_onChange() = + fun `should calculate value on change for questionnaire item with calculated expression extension`() = runBlocking { val questionnaire = Questionnaire().apply { @@ -2833,7 +2834,7 @@ class QuestionnaireViewModelTest( } @Test - fun questionnaireItem_calculatedExpressionExtensionInFlatList_shouldDetectCyclicDependency() = + fun `should detect cyclic dependency for questionnaire item with calculated expression extension in flat list`() = runBlocking { val questionnaire = Questionnaire().apply { @@ -2887,7 +2888,7 @@ class QuestionnaireViewModelTest( } @Test - fun questionnaireItem_calculatedExpressionExtensionInNestedItems_shouldDetectCyclicDependency() = + fun `should detect cyclic dependency for questionnaire item with calculated expression extension in nested list`() = runBlocking { val questionnaire = Questionnaire().apply { diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDatePickerViewHolderFactoryTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDatePickerViewHolderFactoryTest.kt index 36c87da299..e92a129105 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDatePickerViewHolderFactoryTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDatePickerViewHolderFactoryTest.kt @@ -72,7 +72,7 @@ class QuestionnaireItemDatePickerViewHolderFactoryTest { } @Test - fun shouldSetEmptyDateInput_WhenDateFieldInitialized_AndDateIsNull() { + fun `should set text field empty when date field is initialized but answer date value is null`() { viewHolder.bind( QuestionnaireItemViewItem( Questionnaire.QuestionnaireItemComponent().apply { text = "Question?" }, From 54a8f1196876646d4ae99cee974836d155388c1d Mon Sep 17 00:00:00 2001 From: maimoonak Date: Sat, 3 Sep 2022 02:05:43 +0500 Subject: [PATCH 26/34] Add tests and docs --- .../MoreQuestionnaireItemComponents.kt | 2 + .../fhirpath/ExpressionEvaluator.kt | 5 + .../MoreQuestionnaireItemComponentsTest.kt | 90 +++++++++++- .../fhirpath/ExpressionEvaluatorTest.kt | 132 ++++++++++++++++++ 4 files changed, 223 insertions(+), 6 deletions(-) diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponents.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponents.kt index 3b1fd5b242..78b24660dd 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponents.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponents.kt @@ -85,12 +85,14 @@ internal fun Questionnaire.QuestionnaireItemComponent.findVariableExpression( return variableExpressions.find { it.name == variableName } } +/** Returns Calculated expression, or null */ internal val Questionnaire.QuestionnaireItemComponent.calculatedExpression: Expression? get() = this.getExtensionByUrl(EXTENSION_CALCULATED_EXPRESSION_URL)?.let { it.castToExpression(it.value) } +/** Returns list of extensions whose value is of type [Expression] */ internal val Questionnaire.QuestionnaireItemComponent.expressionBasedExtensions get() = this.extension.filter { it.value is Expression } diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluator.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluator.kt index 51be0488c6..af7a8b6b02 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluator.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluator.kt @@ -68,6 +68,7 @@ object ExpressionEvaluator { } } + /** Detects if any item into list is referencing a dependent item in its calculated expression */ internal fun detectExpressionCyclicDependency( items: List ) { @@ -84,6 +85,10 @@ object ExpressionEvaluator { } } + /** + * Returns a pair of item and the calculated and evaluated value for all items with calculated + * expression extension, which is dependent on value of updated response + */ fun evaluateCalculatedExpressions( updatedQuestionnaireItem: Questionnaire.QuestionnaireItemComponent, questionnaire: Questionnaire, diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponentsTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponentsTest.kt index 759d15d1d0..cb4600d3df 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponentsTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponentsTest.kt @@ -709,7 +709,7 @@ class MoreQuestionnaireItemComponentsTest { @Test fun `calculatedExpression should return expression for valid extension url`() { - val questionnaire = + val item = Questionnaire.QuestionnaireItemComponent().apply { addExtension( EXTENSION_CALCULATED_EXPRESSION_URL, @@ -719,13 +719,13 @@ class MoreQuestionnaireItemComponentsTest { } ) } - assertThat(questionnaire.calculatedExpression).isNotNull() - assertThat(questionnaire.calculatedExpression!!.expression).isEqualTo("today()") + assertThat(item.calculatedExpression).isNotNull() + assertThat(item.calculatedExpression!!.expression).isEqualTo("today()") } @Test fun `calculatedExpression should return null for other extension url`() { - val questionnaire = + val item = Questionnaire.QuestionnaireItemComponent().apply { addExtension( ITEM_INITIAL_EXPRESSION_URL, @@ -735,7 +735,85 @@ class MoreQuestionnaireItemComponentsTest { } ) } - assertThat(questionnaire.calculatedExpression).isNull() + assertThat(item.calculatedExpression).isNull() + } + + @Test + fun `expressionBasedExtensions should return all extension of type expression`() { + val item = + Questionnaire.QuestionnaireItemComponent().apply { + addExtension(EXTENSION_HIDDEN_URL, BooleanType(true)) + addExtension( + EXTENSION_CALCULATED_EXPRESSION_URL, + Expression().apply { + this.expression = "today()" + this.language = "text/fhirpath" + } + ) + addExtension( + EXTENSION_ENABLE_WHEN_EXPRESSION_URL, + Expression().apply { + this.expression = "%resource.status == 'draft'" + this.language = "text/fhirpath" + } + ) + } + + val result = item.expressionBasedExtensions + + assertThat(result.count()).isEqualTo(2) + assertThat(result.first().url).isEqualTo(EXTENSION_CALCULATED_EXPRESSION_URL) + assertThat(result.last().url).isEqualTo(EXTENSION_ENABLE_WHEN_EXPRESSION_URL) + } + + @Test + fun `isReferencedBy should return true`() { + val item1 = + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "A" + addExtension( + EXTENSION_CALCULATED_EXPRESSION_URL, + Expression().apply { + this.expression = "%resource.item.where(linkId='B')" + this.language = "text/fhirpath" + } + ) + } + val item2 = Questionnaire.QuestionnaireItemComponent().apply { linkId = "B" } + assertThat(item2.isReferencedBy(item1)).isTrue() + } + + @Test + fun `isReferencedBy should return false`() { + val item1 = + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "A" + addExtension( + EXTENSION_CALCULATED_EXPRESSION_URL, + Expression().apply { + this.expression = "%resource.item.where(answer.value.empty())" + this.language = "text/fhirpath" + } + ) + } + val item2 = Questionnaire.QuestionnaireItemComponent().apply { linkId = "B" } + assertThat(item2.isReferencedBy(item1)).isFalse() + } + + @Test + fun `flattened should return linear list`() { + val items = + listOf( + Questionnaire.QuestionnaireItemComponent().apply { linkId = "A" }, + Questionnaire.QuestionnaireItemComponent() + .apply { linkId = "B" } + .addItem( + Questionnaire.QuestionnaireItemComponent() + .apply { linkId = "C" } + .addItem(Questionnaire.QuestionnaireItemComponent().apply { linkId = "D" }) + ) + ) + assertThat(items.flattened().map { it.linkId }).containsExactly("A", "B", "C", "D") } @Test @@ -933,7 +1011,7 @@ class MoreQuestionnaireItemComponentsTest { } @Test - fun `createQuestionResponse should not set answer for quantity type with missing value `() { + fun `createQuestionResponse should not set answer for quantity type with missing value`() { val question = Questionnaire.QuestionnaireItemComponent( StringType("age"), diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluatorTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluatorTest.kt index f876f12100..19ea00397d 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluatorTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluatorTest.kt @@ -20,6 +20,7 @@ import com.google.android.fhir.datacapture.EXTENSION_CALCULATED_EXPRESSION_URL import com.google.android.fhir.datacapture.EXTENSION_VARIABLE_URL import com.google.android.fhir.datacapture.common.datatype.asStringValue import com.google.android.fhir.datacapture.fhirpath.ExpressionEvaluator.detectExpressionCyclicDependency +import com.google.android.fhir.datacapture.fhirpath.ExpressionEvaluator.evaluateCalculatedExpressions import com.google.android.fhir.datacapture.variableExpressions import com.google.common.truth.Truth.assertThat import java.util.Calendar @@ -28,6 +29,7 @@ import kotlinx.coroutines.runBlocking import org.hl7.fhir.r4.model.DateType import org.hl7.fhir.r4.model.Expression import org.hl7.fhir.r4.model.IntegerType +import org.hl7.fhir.r4.model.Quantity import org.hl7.fhir.r4.model.Questionnaire import org.hl7.fhir.r4.model.QuestionnaireResponse import org.hl7.fhir.r4.model.Type @@ -477,6 +479,136 @@ class ExpressionEvaluatorTest { assertThat((result as Type).asStringValue()).isEqualTo("2") } + @Test + fun `evaluateCalculatedExpressions should return list of calculated values`() = runBlocking { + val questionnaire = + Questionnaire().apply { + id = "a-questionnaire" + addItem( + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "a-birthdate" + type = Questionnaire.QuestionnaireItemType.DATE + addExtension().apply { + url = EXTENSION_CALCULATED_EXPRESSION_URL + setValue( + Expression().apply { + this.language = "text/fhirpath" + this.expression = + "%resource.repeat(item).where(linkId='a-age-years' and answer.empty().not()).select(today() - answer.value)" + } + ) + } + } + ) + addItem( + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "a-age-years" + type = Questionnaire.QuestionnaireItemType.QUANTITY + } + ) + } + + val questionnaireResponse = + QuestionnaireResponse().apply { + addItem( + QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { + linkId = "a-birthdate" + } + ) + addItem( + QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { + linkId = "a-age-years" + answer = + listOf( + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { + this.value = Quantity(1).apply { unit = "year" } + } + ) + } + ) + } + + val result = + evaluateCalculatedExpressions( + questionnaire.item.elementAt(1), + questionnaire, + questionnaireResponse, + emptySet(), + emptyMap() + ) + + assertThat(result.first().second.first().asStringValue()) + .isEqualTo(DateType(Date()).apply { add(Calendar.YEAR, -1) }.asStringValue()) + } + + @Test + fun `evaluateCalculatedExpressions should not include item in list when item has been modified`() = + runBlocking { + val questionnaire = + Questionnaire().apply { + id = "a-questionnaire" + addItem( + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "a-birthdate" + type = Questionnaire.QuestionnaireItemType.DATE + addExtension().apply { + url = EXTENSION_CALCULATED_EXPRESSION_URL + setValue( + Expression().apply { + this.language = "text/fhirpath" + this.expression = + "%resource.repeat(item).where(linkId='a-age-years' and answer.empty().not()).select(today() - answer.value)" + } + ) + } + } + ) + addItem( + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "a-age-years" + type = Questionnaire.QuestionnaireItemType.QUANTITY + } + ) + } + + val questionnaireResponse = + QuestionnaireResponse().apply { + addItem( + QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { + linkId = "a-birthdate" + answer = + listOf( + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { + value = DateType(Date()) + } + ) + } + ) + addItem( + QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { + linkId = "a-age-years" + answer = + listOf( + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { + this.value = Quantity(1).apply { unit = "year" } + } + ) + } + ) + } + + val result = + evaluateCalculatedExpressions( + questionnaire.item.elementAt(1), + questionnaire, + questionnaireResponse, + setOf(questionnaireResponse.itemFirstRep), + emptyMap() + ) + + assertThat(result).isEmpty() + } + @Test fun `detectExpressionCyclicDependency() should throw illegal argument exception when item with calculated expression have cyclic dependency`() { val questionnaire = From e80c3f4d56477a40de0d2c14317133596144055e Mon Sep 17 00:00:00 2001 From: maimoonak Date: Fri, 23 Sep 2022 15:03:25 +0500 Subject: [PATCH 27/34] Move catalog calculation to behavior tab --- .../google/android/fhir/catalog/BehaviorListViewModel.kt | 8 ++++++-- .../google/android/fhir/catalog/ComponentListViewModel.kt | 5 ----- catalog/src/main/res/values/strings.xml | 7 +++---- .../fhir/datacapture/MoreQuestionnaireItemComponents.kt | 5 ++++- .../fhir/datacapture/fhirpath/ExpressionEvaluator.kt | 6 ++---- 5 files changed, 15 insertions(+), 16 deletions(-) diff --git a/catalog/src/main/java/com/google/android/fhir/catalog/BehaviorListViewModel.kt b/catalog/src/main/java/com/google/android/fhir/catalog/BehaviorListViewModel.kt index 57022cc977..f2636ae05d 100644 --- a/catalog/src/main/java/com/google/android/fhir/catalog/BehaviorListViewModel.kt +++ b/catalog/src/main/java/com/google/android/fhir/catalog/BehaviorListViewModel.kt @@ -33,7 +33,11 @@ class BehaviorListViewModel(application: Application) : AndroidViewModel(applica val questionnaireFileName: String, val workFlow: WorkflowType = WorkflowType.DEFAULT ) { - CALCULATIONS(R.drawable.ic_calculations_behavior, R.string.behavior_name_calculation, ""), - SKIP_LOGIC(R.drawable.ic_skiplogic_behavior, R.string.behavior_name_skip_logic, "") + SKIP_LOGIC(R.drawable.ic_skiplogic_behavior, R.string.behavior_name_skip_logic, ""), + CALCULATED_EXPRESSION( + R.drawable.ic_calculations_behavior, + R.string.behavior_name_calculated_expression, + "calculated_expression_questionnaire.json" + ), } } diff --git a/catalog/src/main/java/com/google/android/fhir/catalog/ComponentListViewModel.kt b/catalog/src/main/java/com/google/android/fhir/catalog/ComponentListViewModel.kt index 507d9f236a..94fcc3251a 100644 --- a/catalog/src/main/java/com/google/android/fhir/catalog/ComponentListViewModel.kt +++ b/catalog/src/main/java/com/google/android/fhir/catalog/ComponentListViewModel.kt @@ -108,10 +108,5 @@ class ComponentListViewModel(application: Application, private val state: SavedS "auto_complete_questionnaire.json", "auto_complete_with_validation_questionnaire.json" ), - CALCULATED_EXPRESSION( - R.drawable.ic_unitoptions, - R.string.component_name_calculated_expression, - "calculated_expression_questionnaire.json" - ), } } diff --git a/catalog/src/main/res/values/strings.xml b/catalog/src/main/res/values/strings.xml index 3129ce1d30..8409df7050 100644 --- a/catalog/src/main/res/values/strings.xml +++ b/catalog/src/main/res/values/strings.xml @@ -30,16 +30,15 @@ Slider Dropdown Image - Calculated Expression Auto Complete Default Paginated Review Read only - Calculation Skip logic + Calculated Expression Structured data capture \n Catalog + items: List ) { items.flattened().filter { it.expressionBasedExtensions.isNotEmpty() }.run { - forEach { current -> - - } + forEach { current -> } } } From e3f303af747e362d979ea5d376659ac007e8ac02 Mon Sep 17 00:00:00 2001 From: maimoonak Date: Fri, 23 Sep 2022 15:53:58 +0500 Subject: [PATCH 28/34] spotless fix --- .../android/fhir/datacapture/fhirpath/ExpressionEvaluator.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluator.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluator.kt index 453e6dd847..02ff8b554d 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluator.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluator.kt @@ -105,7 +105,7 @@ object ExpressionEvaluator { item.calculatedExpression != null && modifiedResponses.none { it.linkId == item.linkId } && (updatedQuestionnaireItem.isReferencedBy(item) || - findDependentVariables(item.calculatedExpression!!).isNotEmpty()) + findDependentVariables(item.calculatedExpression!!).isNotEmpty()) } .map { calculable -> val appContext = From 4c16c3ae4279e8cfc99af314645d309bed7bf1e3 Mon Sep 17 00:00:00 2001 From: maimoonak <4829880+maimoonak@users.noreply.github.com> Date: Fri, 30 Sep 2022 15:58:17 -0400 Subject: [PATCH 29/34] Update datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponents.kt Co-authored-by: Jing Tang --- .../android/fhir/datacapture/MoreQuestionnaireItemComponents.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponents.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponents.kt index be493e8436..6b8cd1176a 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponents.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponents.kt @@ -105,7 +105,7 @@ internal val Questionnaire.QuestionnaireItemComponent.expressionBasedExtensions get() = this.extension.filter { it.value is Expression } /** - * Check if given [item] has calculable expression based extension and is referencing current item + * Whether [item] has any expression directly referencing the current questionnaire item by link ID (e.g. if [item] has an expression `%resource.item.where(linkId='this-question')` where `this-question` is the link ID of the current questionnaire item). */ internal fun Questionnaire.QuestionnaireItemComponent.isReferencedBy( item: Questionnaire.QuestionnaireItemComponent From 0a3c836de1b945164269867b8cc77a8316ccdcd5 Mon Sep 17 00:00:00 2001 From: maimoonak Date: Tue, 4 Oct 2022 01:14:10 +0500 Subject: [PATCH 30/34] Resolve feedback for naming --- .../fhir/catalog/BehaviorListViewModel.kt | 2 +- .../fhir/datacapture/QuestionnaireViewModel.kt | 11 +++++------ .../datacapture/fhirpath/ExpressionEvaluator.kt | 16 ++++++++-------- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/catalog/src/main/java/com/google/android/fhir/catalog/BehaviorListViewModel.kt b/catalog/src/main/java/com/google/android/fhir/catalog/BehaviorListViewModel.kt index f2636ae05d..71bdd0267f 100644 --- a/catalog/src/main/java/com/google/android/fhir/catalog/BehaviorListViewModel.kt +++ b/catalog/src/main/java/com/google/android/fhir/catalog/BehaviorListViewModel.kt @@ -33,11 +33,11 @@ class BehaviorListViewModel(application: Application) : AndroidViewModel(applica val questionnaireFileName: String, val workFlow: WorkflowType = WorkflowType.DEFAULT ) { - SKIP_LOGIC(R.drawable.ic_skiplogic_behavior, R.string.behavior_name_skip_logic, ""), CALCULATED_EXPRESSION( R.drawable.ic_calculations_behavior, R.string.behavior_name_calculated_expression, "calculated_expression_questionnaire.json" ), + SKIP_LOGIC(R.drawable.ic_skiplogic_behavior, R.string.behavior_name_skip_logic, "") } } 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 9ef2fde9f6..92f3b284e9 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 @@ -382,20 +382,19 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat modifiedQuestionnaireResponseItemSet, questionnaireItemParentMap ) - .forEach { updatedCalculable -> + .forEach { (questionnaireItem, calculatedAnswers) -> val updatedCalculableResponse = - questionnaireResponseItemPreOrderList.find { it.linkId == updatedCalculable.first.linkId } + questionnaireResponseItemPreOrderList.find { it.linkId == questionnaireItem.linkId } - val evaluatedAnswer = updatedCalculable.second val currentAnswer = updatedCalculableResponse?.answer?.map { it.value } ?: emptyList() // update and notify only if new answer has changed to prevent any event loop - if (evaluatedAnswer.size != currentAnswer.size || - evaluatedAnswer.zip(currentAnswer).any { (v1, v2) -> v1.equalsDeep(v2).not() } + if (calculatedAnswers.size != currentAnswer.size || + calculatedAnswers.zip(currentAnswer).any { (v1, v2) -> v1.equalsDeep(v2).not() } ) { updatedCalculableResponse?.let { it.answer = - evaluatedAnswer.map { + calculatedAnswers.map { QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { value = it } diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluator.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluator.kt index 02ff8b554d..f34bc9c14e 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluator.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluator.kt @@ -98,24 +98,24 @@ object ExpressionEvaluator { .item .flattened() .filter { item -> - // 1- item is calculable - // 2- item answer is not modified and touched by user; + // Condition 1. item is calculable + // Condition 2. item answer is not modified and touched by user; // https://build.fhir.org/ig/HL7/sdc/StructureDefinition-sdc-questionnaire-calculatedExpression.html - // 3- item answer depends on the updated item answer OR has a variable dependency + // Condition 3. item answer depends on the updated item answer OR has a variable dependency item.calculatedExpression != null && modifiedResponses.none { it.linkId == item.linkId } && (updatedQuestionnaireItem.isReferencedBy(item) || findDependentVariables(item.calculatedExpression!!).isNotEmpty()) } - .map { calculable -> + .map { questionnaireItem -> val appContext = mutableMapOf().apply { extractDependentVariables( - calculable.calculatedExpression!!, + questionnaireItem.calculatedExpression!!, questionnaire, questionnaireResponse, questionnaireItemParentMap, - calculable, + questionnaireItem, this ) } @@ -126,10 +126,10 @@ object ExpressionEvaluator { questionnaireResponse, null, null, - calculable.calculatedExpression!!.expression + questionnaireItem.calculatedExpression!!.expression ) .map { it.castToType(it) } - calculable to updatedAnswer + questionnaireItem to updatedAnswer } } From e20451dce74be25d78c05481db74dfe84512ab31 Mon Sep 17 00:00:00 2001 From: maimoonak Date: Wed, 5 Oct 2022 02:15:47 +0500 Subject: [PATCH 31/34] Resolve feedback for doc and naming --- .../MoreQuestionnaireItemComponents.kt | 4 +-- .../datacapture/QuestionnaireViewModel.kt | 4 +-- .../fhirpath/ExpressionEvaluator.kt | 12 ++++---- .../datacapture/utilities/MoreExpressions.kt | 28 ------------------- 4 files changed, 10 insertions(+), 38 deletions(-) delete mode 100644 datacapture/src/main/java/com/google/android/fhir/datacapture/utilities/MoreExpressions.kt diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponents.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponents.kt index be493e8436..88dc4cfe00 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponents.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponents.kt @@ -400,8 +400,8 @@ fun QuestionnaireResponse.QuestionnaireResponseItemComponent.addNestedItemsToAns } /** - * Flatten a nested list of [QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent] - * recursively and returns a flat list of all items into list embedded at any level + * Flatten a nested list of [Questionnaire.QuestionnaireItemComponent] recursively and returns a + * flat list of all items into list embedded at any level */ fun List.flattened(): List { 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 92f3b284e9..7a7e7fa35c 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 @@ -390,11 +390,11 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat // update and notify only if new answer has changed to prevent any event loop if (calculatedAnswers.size != currentAnswer.size || - calculatedAnswers.zip(currentAnswer).any { (v1, v2) -> v1.equalsDeep(v2).not() } + calculatedAnswers.zip(currentAnswer).any { (v1, v2) -> v1.equalsDeep(v2).not() } ) { updatedCalculableResponse?.let { it.answer = - calculatedAnswers.map { + calculatedAnswers.map { QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { value = it } diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluator.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluator.kt index f34bc9c14e..5181f9ec0c 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluator.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluator.kt @@ -83,8 +83,8 @@ object ExpressionEvaluator { } /** - * Returns a pair of item and the calculated and evaluated value for all items with calculated - * expression extension, which is dependent on value of updated response + * Returns a list of pair of item and the calculated and evaluated value for all items with + * calculated expression extension, which is dependent on value of updated response */ fun evaluateCalculatedExpressions( updatedQuestionnaireItem: Questionnaire.QuestionnaireItemComponent, @@ -111,11 +111,11 @@ object ExpressionEvaluator { val appContext = mutableMapOf().apply { extractDependentVariables( - questionnaireItem.calculatedExpression!!, + questionnaireItem.calculatedExpression!!, questionnaire, questionnaireResponse, questionnaireItemParentMap, - questionnaireItem, + questionnaireItem, this ) } @@ -126,10 +126,10 @@ object ExpressionEvaluator { questionnaireResponse, null, null, - questionnaireItem.calculatedExpression!!.expression + questionnaireItem.calculatedExpression!!.expression ) .map { it.castToType(it) } - questionnaireItem to updatedAnswer + questionnaireItem to updatedAnswer } } diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/utilities/MoreExpressions.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/utilities/MoreExpressions.kt deleted file mode 100644 index 11d6e56005..0000000000 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/utilities/MoreExpressions.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.android.fhir.datacapture.utilities - -import org.hl7.fhir.r4.model.Expression - -/** - * An expression which does not refer to a specific linkId or variable and derives value using a - * generic expression - */ -internal val Expression.hasDynamicExpression - get() = - this.expression.replace(" ", "").contains("linkId='").not() || - this.expression.matches(Regex(".*%\\w+.*")).not() From cfe4c4a2b7ab18f4edac80c3a117e81a8221d441 Mon Sep 17 00:00:00 2001 From: maimoonak Date: Wed, 5 Oct 2022 03:08:00 +0500 Subject: [PATCH 32/34] Refactor the update answer handling logic --- ...uestionnaireResponseItemAnswerComponent.kt | 8 +++ .../datacapture/QuestionnaireViewModel.kt | 28 +++++----- .../fhirpath/ExpressionEvaluator.kt | 5 +- ...ionnaireResponseItemAnswerComponentTest.kt | 52 +++++++++++++++++++ 4 files changed, 76 insertions(+), 17 deletions(-) diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireResponseItemAnswerComponent.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireResponseItemAnswerComponent.kt index a47080a3b5..6904d108fc 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireResponseItemAnswerComponent.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireResponseItemAnswerComponent.kt @@ -33,6 +33,7 @@ import org.hl7.fhir.r4.model.Quantity import org.hl7.fhir.r4.model.QuestionnaireResponse import org.hl7.fhir.r4.model.StringType import org.hl7.fhir.r4.model.TimeType +import org.hl7.fhir.r4.model.Type import org.hl7.fhir.r4.model.UriType internal fun QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent.displayString( @@ -69,3 +70,10 @@ internal fun QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent.disp else -> context.getString(R.string.not_answered) } } + +internal fun List< + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent>.hasDifferentAnswerSet( + answers: List +) = + this.size != answers.size || + this.map { it.value }.zip(answers).any { (v1, v2) -> v1.equalsDeep(v2).not() } 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 7a7e7fa35c..7d29132103 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 @@ -383,24 +383,20 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat questionnaireItemParentMap ) .forEach { (questionnaireItem, calculatedAnswers) -> - val updatedCalculableResponse = - questionnaireResponseItemPreOrderList.find { it.linkId == questionnaireItem.linkId } - - val currentAnswer = updatedCalculableResponse?.answer?.map { it.value } ?: emptyList() - - // update and notify only if new answer has changed to prevent any event loop - if (calculatedAnswers.size != currentAnswer.size || - calculatedAnswers.zip(currentAnswer).any { (v1, v2) -> v1.equalsDeep(v2).not() } - ) { - updatedCalculableResponse?.let { - it.answer = - calculatedAnswers.map { - QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { - value = it + // update all response item with updated values + questionnaireResponseItemPreOrderList + .filter { it.linkId == questionnaireItem.linkId } + .forEach { questionnaireResponseItem -> + // update and notify only if new answer has changed to prevent any event loop + if (questionnaireResponseItem.answer.hasDifferentAnswerSet(calculatedAnswers)) { + questionnaireResponseItem.answer = + calculatedAnswers.map { + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { + value = it + } } - } + } } - } } } diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluator.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluator.kt index 5181f9ec0c..6b8c1fe28c 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluator.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluator.kt @@ -93,7 +93,7 @@ object ExpressionEvaluator { modifiedResponses: Set, questionnaireItemParentMap: Map - ): List>> { + ): List { return questionnaire .item .flattened() @@ -378,3 +378,6 @@ object ExpressionEvaluator { null } } + +/** Pair of a [Questionnaire.QuestionnaireItemComponent] with its evaluated answers */ +internal typealias ItemToAnswersPair = Pair> diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/MoreQuestionnaireResponseItemAnswerComponentTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/MoreQuestionnaireResponseItemAnswerComponentTest.kt index 9e1a539ec9..0a98c2822e 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/MoreQuestionnaireResponseItemAnswerComponentTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/MoreQuestionnaireResponseItemAnswerComponentTest.kt @@ -221,4 +221,56 @@ class MoreQuestionnaireResponseItemAnswerComponentTest { assertThat(answer.displayString(context)).isEqualTo(context.getString(R.string.not_answered)) } + + @Test + fun `hasDifferentAnswerSet() should return false when both list values are exactly same`() { + val list1 = + listOf( + createCodingQuestionnaireResponseItemAnswerComponent("http://abc.org", "code1", "Code 1"), + createCodingQuestionnaireResponseItemAnswerComponent("http://abc.org", "code2", "Code 2") + ) + val list2 = + listOf( + Coding("http://abc.org", "code1", "Code 1"), + Coding("http://abc.org", "code2", "Code 2") + ) + assertThat(list1.hasDifferentAnswerSet(list2)).isFalse() + } + + @Test + fun `hasDifferentAnswerSet() should return true when both list sizes are different`() { + val list1 = + listOf( + createCodingQuestionnaireResponseItemAnswerComponent("http://abc.org", "code1", "Code 1"), + ) + val list2 = + listOf( + Coding("http://abc.org", "code1", "Code 1"), + Coding("http://abc.org", "code2", "Code 2") + ) + assertThat(list1.hasDifferentAnswerSet(list2)).isTrue() + } + + @Test + fun `hasDifferentAnswerSet() should return true when both list sizes are same with different items`() { + val list1 = + listOf( + createCodingQuestionnaireResponseItemAnswerComponent("http://abc.org", "code1", "Code 1"), + createCodingQuestionnaireResponseItemAnswerComponent("http://abc.org", "code2", "Code 2"), + ) + val list2 = + listOf( + Coding("http://abc.org", "code1", "Code 1"), + Coding("http://abc.org", "code4", "Code 4") + ) + assertThat(list1.hasDifferentAnswerSet(list2)).isTrue() + } + + private fun createCodingQuestionnaireResponseItemAnswerComponent( + url: String, + code: String, + display: String + ) = + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent() + .setValue(Coding(url, code, display)) } From 800bba1ffd99458b140f3bb28941bb945f2f4181 Mon Sep 17 00:00:00 2001 From: maimoonak Date: Mon, 10 Oct 2022 22:26:21 +0500 Subject: [PATCH 33/34] Resolve feedback and merge master --- .../MoreQuestionnaireItemComponents.kt | 7 +- ...uestionnaireResponseItemAnswerComponent.kt | 6 +- .../datacapture/QuestionnaireViewModel.kt | 61 +- .../fhirpath/ExpressionEvaluator.kt | 40 +- .../datacapture/mapping/ResourceMapper.kt | 2 + .../datacapture/validation/ValidationUtil.kt | 10 +- ...ionnaireItemDatePickerViewHolderFactory.kt | 9 +- ...reItemEditTextQuantityViewHolderFactory.kt | 32 +- .../MoreQuestionnaireItemComponentsTest.kt | 2 +- .../datacapture/QuestionnaireViewModelTest.kt | 417 +++++---- .../fhirpath/ExpressionEvaluatorTest.kt | 808 +++++++++--------- .../MaxValueConstraintValidatorTest.kt | 22 +- 12 files changed, 750 insertions(+), 666 deletions(-) diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponents.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponents.kt index 8179d4400a..3570c29e4a 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponents.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponents.kt @@ -118,13 +118,16 @@ internal val Questionnaire.QuestionnaireItemComponent.expressionBasedExtensions get() = this.extension.filter { it.value is Expression } /** - * Whether [item] has any expression directly referencing the current questionnaire item by link ID (e.g. if [item] has an expression `%resource.item.where(linkId='this-question')` where `this-question` is the link ID of the current questionnaire item). + * Whether [item] has any expression directly referencing the current questionnaire item by link ID + * (e.g. if [item] has an expression `%resource.item.where(linkId='this-question')` where + * `this-question` is the link ID of the current questionnaire item). */ internal fun Questionnaire.QuestionnaireItemComponent.isReferencedBy( item: Questionnaire.QuestionnaireItemComponent ) = item.expressionBasedExtensions.any { - it.castToExpression(it.value) + it + .castToExpression(it.value) .expression .replace(" ", "") .contains(Regex(".*linkId='${this.linkId}'.*")) diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireResponseItemAnswerComponent.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireResponseItemAnswerComponent.kt index 4524f3940d..2b015d90da 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireResponseItemAnswerComponent.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireResponseItemAnswerComponent.kt @@ -75,9 +75,7 @@ internal fun QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent.disp } } -internal fun List< - QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent>.hasDifferentAnswerSet( - answers: List -) = +internal fun List + .hasDifferentAnswerSet(answers: List) = this.size != answers.size || this.map { it.value }.zip(answers).any { (v1, v2) -> v1.equalsDeep(v2).not() } 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 6f10041141..8e41033d09 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 @@ -268,19 +268,20 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat ( Questionnaire.QuestionnaireItemComponent, QuestionnaireResponse.QuestionnaireResponseItemComponent, - List) -> Unit = - { questionnaireItem, questionnaireResponseItem, answers -> - // TODO(jingtang10): update the questionnaire response item pre-order list and the parent map - questionnaireResponseItem.answer = answers.toList() - if (questionnaireItem.hasNestedItemsWithinAnswers) { - questionnaireResponseItem.addNestedItemsToAnswer(questionnaireItem) - } - modifiedQuestionnaireResponseItemSet.add(questionnaireResponseItem) + List + ) -> Unit = + { questionnaireItem, questionnaireResponseItem, answers -> + // TODO(jingtang10): update the questionnaire response item pre-order list and the parent map + questionnaireResponseItem.answer = answers.toList() + if (questionnaireItem.hasNestedItemsWithinAnswers) { + questionnaireResponseItem.addNestedItemsToAnswer(questionnaireItem) + } + modifiedQuestionnaireResponseItemSet.add(questionnaireResponseItem) - runCalculations(questionnaireItem) + updateDependentQuestionnaireResponseItems(questionnaireItem) - modificationCount.update { it + 1 } - } + modificationCount.update { it + 1 } + } private val answerValueSetMap = mutableMapOf>() @@ -380,27 +381,37 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat SharingStarted.Lazily, initialValue = getQuestionnaireState( - questionnaireItemList = questionnaire.item, - questionnaireResponseItemList = questionnaireResponse.item, - currentPageIndex = getInitialPageIndex(), - reviewMode = enableReviewPage - ) + questionnaireItemList = questionnaire.item, + questionnaireResponseItemList = questionnaireResponse.item, + currentPageIndex = getInitialPageIndex(), + reviewMode = enableReviewPage + ) .also { detectExpressionCyclicDependency(questionnaire.item) } - .also { questionnaire.item.flattened().forEach { runCalculations(it) } } + .also { + questionnaire.item.flattened().forEach { + updateDependentQuestionnaireResponseItems(it) + } + } ) - fun runCalculations(updatedQuestionnaireItem: Questionnaire.QuestionnaireItemComponent) { + fun updateDependentQuestionnaireResponseItems( + updatedQuestionnaireItem: Questionnaire.QuestionnaireItemComponent + ) { evaluateCalculatedExpressions( - updatedQuestionnaireItem, - questionnaire, - questionnaireResponse, - modifiedQuestionnaireResponseItemSet, - questionnaireItemParentMap - ) + updatedQuestionnaireItem, + questionnaire, + questionnaireResponse, + questionnaireItemParentMap + ) .forEach { (questionnaireItem, calculatedAnswers) -> // update all response item with updated values questionnaireResponseItemPreOrderList - .filter { it.linkId == questionnaireItem.linkId } + // Item answer should not be modified and touched by user; + // https://build.fhir.org/ig/HL7/sdc/StructureDefinition-sdc-questionnaire-calculatedExpression.html + .filter { + it.linkId == questionnaireItem.linkId && + !modifiedQuestionnaireResponseItemSet.contains(it) + } .forEach { questionnaireResponseItem -> // update and notify only if new answer has changed to prevent any event loop if (questionnaireResponseItem.answer.hasDifferentAnswerSet(calculatedAnswers)) { diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluator.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluator.kt index 6b8c1fe28c..93bd9d45f6 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluator.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluator.kt @@ -69,17 +69,20 @@ object ExpressionEvaluator { internal fun detectExpressionCyclicDependency( items: List ) { - items.flattened().filter { it.calculatedExpression != null }.run { - forEach { current -> - // no calculable item depending on current item should be used as dependency into current - // item - this.forEach { dependent -> - check(!(current.isReferencedBy(dependent) && dependent.isReferencedBy(current))) { - "${current.linkId} and ${dependent.linkId} have cyclic dependency in expression based extension" + items + .flattened() + .filter { it.calculatedExpression != null } + .run { + forEach { current -> + // no calculable item depending on current item should be used as dependency into current + // item + this.forEach { dependent -> + check(!(current.isReferencedBy(dependent) && dependent.isReferencedBy(current))) { + "${current.linkId} and ${dependent.linkId} have cyclic dependency in expression based extension" + } } } } - } } /** @@ -90,20 +93,15 @@ object ExpressionEvaluator { updatedQuestionnaireItem: Questionnaire.QuestionnaireItemComponent, questionnaire: Questionnaire, questionnaireResponse: QuestionnaireResponse, - modifiedResponses: Set, questionnaireItemParentMap: Map ): List { - return questionnaire - .item + return questionnaire.item .flattened() .filter { item -> // Condition 1. item is calculable - // Condition 2. item answer is not modified and touched by user; - // https://build.fhir.org/ig/HL7/sdc/StructureDefinition-sdc-questionnaire-calculatedExpression.html - // Condition 3. item answer depends on the updated item answer OR has a variable dependency + // Condition 2. item answer depends on the updated item answer OR has a variable dependency item.calculatedExpression != null && - modifiedResponses.none { it.linkId == item.linkId } && (updatedQuestionnaireItem.isReferencedBy(item) || findDependentVariables(item.calculatedExpression!!).isNotEmpty()) } @@ -121,7 +119,8 @@ object ExpressionEvaluator { } val updatedAnswer = - fhirPathEngine.evaluate( + fhirPathEngine + .evaluate( appContext, questionnaireResponse, null, @@ -258,10 +257,11 @@ object ExpressionEvaluator { } private fun findDependentVariables(expression: Expression) = - variableRegex.findAll(expression.expression).map { it.groupValues[1] }.toList().filterNot { - variable -> - reservedVariables.contains(variable) - } + variableRegex + .findAll(expression.expression) + .map { it.groupValues[1] } + .toList() + .filterNot { variable -> reservedVariables.contains(variable) } /** * Finds the dependent variables at questionnaire item level first, then in ancestors and then at 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 6851a63bbb..5616f33bf2 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 @@ -31,6 +31,7 @@ import java.lang.reflect.Method import java.lang.reflect.ParameterizedType import java.util.Locale import org.hl7.fhir.r4.context.IWorkerContext +import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext import org.hl7.fhir.r4.model.Base import org.hl7.fhir.r4.model.Bundle import org.hl7.fhir.r4.model.CodeType @@ -50,6 +51,7 @@ import org.hl7.fhir.r4.model.Resource import org.hl7.fhir.r4.model.StringType import org.hl7.fhir.r4.model.Type import org.hl7.fhir.r4.model.UriType +import org.hl7.fhir.r4.utils.FHIRPathEngine import org.hl7.fhir.r4.utils.StructureMapUtilities /** diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/ValidationUtil.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/ValidationUtil.kt index d95bb798f4..4039028db3 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/ValidationUtil.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/ValidationUtil.kt @@ -27,10 +27,12 @@ import org.hl7.fhir.r4.utils.FHIRPathEngine fun Type.valueOrCalculateValue(): Type? { return if (this.hasExtension()) { - this.extension.firstOrNull { it.url == EXTENSION_CQF_CALCULATED_VALUE_URL }?.let { - val expression = (it.value as Expression).expression - fhirPathEngine.evaluate(this, expression).firstOrNull()?.let { it as Type } - } + this.extension + .firstOrNull { it.url == EXTENSION_CQF_CALCULATED_VALUE_URL } + ?.let { + val expression = (it.value as Expression).expression + fhirPathEngine.evaluate(this, expression).firstOrNull()?.let { it as Type } + } } else { this } diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDatePickerViewHolderFactory.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDatePickerViewHolderFactory.kt index 7945398591..fccc63e6f5 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDatePickerViewHolderFactory.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDatePickerViewHolderFactory.kt @@ -118,8 +118,7 @@ internal object QuestionnaireItemDatePickerViewHolderFactory : ) ) { textInputEditText.setText( - questionnaireItemViewItem - .answers + questionnaireItemViewItem.answers .singleOrNull() ?.takeIf { it.hasValue() } ?.valueDateType @@ -133,7 +132,8 @@ internal object QuestionnaireItemDatePickerViewHolderFactory : override fun displayValidationResult(validationResult: ValidationResult) { textInputLayout.error = when (validationResult) { - is NotValidated, Valid -> null + is NotValidated, + Valid -> null is Invalid -> validationResult.getSingleStringValidationMessage() } } @@ -145,8 +145,7 @@ internal object QuestionnaireItemDatePickerViewHolderFactory : private fun createMaterialDatePicker(): MaterialDatePicker { val selectedDate = - questionnaireItemViewItem - .answers + questionnaireItemViewItem.answers .singleOrNull() ?.valueDateType ?.localDate diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactory.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactory.kt index fdd8e36b17..58a63ddf9d 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactory.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactory.kt @@ -37,22 +37,24 @@ internal object QuestionnaireItemEditTextQuantityViewHolderFactory : ): QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent? { // https://build.fhir.org/ig/HL7/sdc/behavior.html#initial // read default unit from initial, as ideally quantity must specify a unit - return text.takeIf { it.isNotBlank() }?.let { - val value = BigDecimal(text) - val quantity = - with(questionnaireItemViewItem.questionnaireItem) { - if (this.hasInitial() && this.initialFirstRep.valueQuantity.hasCode()) - this.initialFirstRep.valueQuantity.let { initial -> - Quantity().apply { - this.value = value - this.code = initial.code - this.system = initial.system + return text + .takeIf { it.isNotBlank() } + ?.let { + val value = BigDecimal(text) + val quantity = + with(questionnaireItemViewItem.questionnaireItem) { + if (this.hasInitial() && this.initialFirstRep.valueQuantity.hasCode()) + this.initialFirstRep.valueQuantity.let { initial -> + Quantity().apply { + this.value = value + this.code = initial.code + this.system = initial.system + } } - } - else Quantity().apply { this.value = value } - } - QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().setValue(quantity) - } + else Quantity().apply { this.value = value } + } + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().setValue(quantity) + } } override fun getText( diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponentsTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponentsTest.kt index 49e6e05095..08cafe7446 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponentsTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponentsTest.kt @@ -28,8 +28,8 @@ import org.hl7.fhir.r4.model.Coding import org.hl7.fhir.r4.model.Enumeration import org.hl7.fhir.r4.model.Expression import org.hl7.fhir.r4.model.Extension -import org.hl7.fhir.r4.model.Quantity import org.hl7.fhir.r4.model.Patient +import org.hl7.fhir.r4.model.Quantity import org.hl7.fhir.r4.model.Questionnaire import org.hl7.fhir.r4.model.StringType import org.hl7.fhir.r4.utils.ToolingExtensions 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 c90bcce51b..dcca930af5 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 @@ -59,8 +59,8 @@ 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.IntegerType -import org.hl7.fhir.r4.model.Quantity import org.hl7.fhir.r4.model.Practitioner +import org.hl7.fhir.r4.model.Quantity import org.hl7.fhir.r4.model.Questionnaire import org.hl7.fhir.r4.model.QuestionnaireResponse import org.hl7.fhir.r4.model.StringType @@ -2952,186 +2952,199 @@ class QuestionnaireViewModelTest( @Test fun `should calculate value on start for questionnaire item with calculated expression extension`() = - runBlocking { - val questionnaire = - Questionnaire().apply { - id = "a-questionnaire" - addItem( - Questionnaire.QuestionnaireItemComponent().apply { - linkId = "a-birthdate" - type = Questionnaire.QuestionnaireItemType.DATE - addExtension().apply { - url = EXTENSION_CALCULATED_EXPRESSION_URL - setValue( - Expression().apply { - this.language = "text/fhirpath" - this.expression = - "%resource.repeat(item).where(linkId='a-age-years' and answer.empty().not()).select(today() - answer.value)" - } + runBlocking { + val questionnaire = + Questionnaire().apply { + id = "a-questionnaire" + addItem( + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "a-birthdate" + type = Questionnaire.QuestionnaireItemType.DATE + addExtension().apply { + url = EXTENSION_CALCULATED_EXPRESSION_URL + setValue( + Expression().apply { + this.language = "text/fhirpath" + this.expression = + "%resource.repeat(item).where(linkId='a-age-years' and answer.empty().not()).select(today() - answer.value)" + } + ) + } + } + ) + addItem( + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "a-age-years" + type = Questionnaire.QuestionnaireItemType.QUANTITY + addInitial( + Questionnaire.QuestionnaireItemInitialComponent(Quantity.fromUcum("1", "year")) ) } - } - ) - addItem( - Questionnaire.QuestionnaireItemComponent().apply { - linkId = "a-age-years" - type = Questionnaire.QuestionnaireItemType.QUANTITY - addInitial( - Questionnaire.QuestionnaireItemInitialComponent(Quantity.fromUcum("1", "year")) - ) - } - ) - } + ) + } - val viewModel = createQuestionnaireViewModel(questionnaire) + val viewModel = createQuestionnaireViewModel(questionnaire) - assertThat( - viewModel - .getQuestionnaireResponse() - .item - .single { it.linkId == "a-birthdate" } - .answerFirstRep - .value - .asStringValue() - ) - .isEqualTo(DateType(Date()).apply { add(Calendar.YEAR, -1) }.asStringValue()) - - assertThat( - viewModel - .getQuestionnaireResponse() - .item - .single { it.linkId == "a-age-years" } - .answerFirstRep - .valueQuantity - .value - .toString() - ) - .isEqualTo("1") - } + assertThat( + viewModel + .getQuestionnaireResponse() + .item + .single { it.linkId == "a-birthdate" } + .answerFirstRep.value.asStringValue() + ) + .isEqualTo(DateType(Date()).apply { add(Calendar.YEAR, -1) }.asStringValue()) + + assertThat( + viewModel + .getQuestionnaireResponse() + .item + .single { it.linkId == "a-age-years" } + .answerFirstRep.valueQuantity.value.toString() + ) + .isEqualTo("1") + } @Test fun `should calculate value on change for questionnaire item with calculated expression extension`() = - runBlocking { - val questionnaire = - Questionnaire().apply { - id = "a-questionnaire" - addItem( - Questionnaire.QuestionnaireItemComponent().apply { - linkId = "a-birthdate" - type = Questionnaire.QuestionnaireItemType.DATE - addExtension().apply { - url = EXTENSION_CALCULATED_EXPRESSION_URL - setValue( - Expression().apply { - this.language = "text/fhirpath" - this.expression = - "%resource.repeat(item).where(linkId='a-age-years' and answer.empty().not()).select(today() - answer.value)" - } - ) + runBlocking { + val questionnaire = + Questionnaire().apply { + id = "a-questionnaire" + addItem( + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "a-birthdate" + type = Questionnaire.QuestionnaireItemType.DATE + addExtension().apply { + url = EXTENSION_CALCULATED_EXPRESSION_URL + setValue( + Expression().apply { + this.language = "text/fhirpath" + this.expression = + "%resource.repeat(item).where(linkId='a-age-years' and answer.empty().not()).select(today() - answer.value)" + } + ) + } } - } - ) - addItem( - Questionnaire.QuestionnaireItemComponent().apply { - linkId = "a-age-years" - type = Questionnaire.QuestionnaireItemType.INTEGER - } - ) - } + ) + addItem( + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "a-age-years" + type = Questionnaire.QuestionnaireItemType.INTEGER + } + ) + } - val viewModel = createQuestionnaireViewModel(questionnaire) + val viewModel = createQuestionnaireViewModel(questionnaire) - val current = - viewModel.getQuestionnaireItemViewItemList().first { - it.questionnaireItem.linkId == "a-birthdate" - } + val birthdateItem = + viewModel.getQuestionnaireItemViewItemList().first { + it.questionnaireItem.linkId == "a-birthdate" + } - assertThat(current.getQuestionnaireResponseItem().answer).isEmpty() + assertThat(birthdateItem.getQuestionnaireResponseItem().answer).isEmpty() - viewModel - .getQuestionnaireItemViewItemList() - .first { it.questionnaireItem.linkId == "a-age-years" } - .apply { + viewModel + .getQuestionnaireItemViewItemList() + .first { it.questionnaireItem.linkId == "a-age-years" } + .apply { + this.answersChangedCallback( + this.questionnaireItem, + this.getQuestionnaireResponseItem(), + listOf( + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { + this.value = Quantity.fromUcum("2", "years") + } + ) + ) + } + + assertThat( + birthdateItem.getQuestionnaireResponseItem().answer.first().valueDateType.valueAsString + ) + .isEqualTo(DateType(Date()).apply { add(Calendar.YEAR, -2) }.valueAsString) + } + + @Test + fun `should not change value for modified questionnaire items with calculated expression extension`() = + runBlocking { + val questionnaire = + Questionnaire().apply { + id = "a-questionnaire" + addItem( + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "a-birthdate" + type = Questionnaire.QuestionnaireItemType.DATE + addExtension().apply { + url = EXTENSION_CALCULATED_EXPRESSION_URL + setValue( + Expression().apply { + this.language = "text/fhirpath" + this.expression = + "%resource.repeat(item).where(linkId='a-age-years' and answer.empty().not()).select(today() - answer.value)" + } + ) + } + } + ) + addItem( + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "a-age-years" + type = Questionnaire.QuestionnaireItemType.INTEGER + } + ) + } + + val viewModel = createQuestionnaireViewModel(questionnaire) + val birthdateItem = + viewModel.getQuestionnaireItemViewItemList().first { + it.questionnaireItem.linkId == "a-birthdate" + } + val birthdateValue = DateType(Date()) + birthdateItem.apply { this.answersChangedCallback( this.questionnaireItem, this.getQuestionnaireResponseItem(), listOf( QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { - this.value = Quantity.fromUcum("2", "years") + this.value = birthdateValue } ) ) } - val updated = - viewModel.getQuestionnaireItemViewItemList().first { - it.questionnaireItem.linkId == "a-birthdate" - } - assertThat(updated.getQuestionnaireResponseItem().answer.first().valueDateType.valueAsString) - .isEqualTo(DateType(Date()).apply { add(Calendar.YEAR, -2) }.valueAsString) - } - - @Test - fun `should detect cyclic dependency for questionnaire item with calculated expression extension in flat list`() = - runBlocking { - val questionnaire = - Questionnaire().apply { - id = "a-questionnaire" - addItem( - Questionnaire.QuestionnaireItemComponent().apply { - linkId = "a-birthdate" - type = Questionnaire.QuestionnaireItemType.DATE - addInitial( - Questionnaire.QuestionnaireItemInitialComponent( - DateType(Date()).apply { add(Calendar.YEAR, -2) } - ) + assertThat( + birthdateItem.getQuestionnaireResponseItem().answer.first().valueDateType.valueAsString + ) + .isEqualTo(birthdateValue.valueAsString) + + viewModel + .getQuestionnaireItemViewItemList() + .first { it.questionnaireItem.linkId == "a-age-years" } + .apply { + this.answersChangedCallback( + this.questionnaireItem, + this.getQuestionnaireResponseItem(), + listOf( + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { + this.value = Quantity.fromUcum("2", "years") + } ) - addExtension().apply { - url = EXTENSION_CALCULATED_EXPRESSION_URL - setValue( - Expression().apply { - this.language = "text/fhirpath" - this.expression = - "%resource.repeat(item).where(linkId='a-age-years' and answer.empty().not()).select(today() - answer.value)" - } - ) - } - } - ) + ) + } - addItem( - Questionnaire.QuestionnaireItemComponent().apply { - linkId = "a-age-years" - type = Questionnaire.QuestionnaireItemType.INTEGER - addExtension().apply { - url = EXTENSION_CALCULATED_EXPRESSION_URL - setValue( - Expression().apply { - this.language = "text/fhirpath" - this.expression = - "today().toString().substring(0, 4).toInteger() - %resource.repeat(item).where(linkId='a-birthdate').answer.value.toString().substring(0, 4).toInteger()" - } - ) - } - } + assertThat( + birthdateItem.getQuestionnaireResponseItem().answer.first().valueDateType.valueAsString ) - } - - val exception = - Assert.assertThrows(null, IllegalStateException::class.java) { - createQuestionnaireViewModel(questionnaire) - } - assertThat(exception.message) - .isEqualTo("a-birthdate and a-age-years have cyclic dependency in expression based extension") - } + .isEqualTo(birthdateValue.valueAsString) + } @Test - fun `should detect cyclic dependency for questionnaire item with calculated expression extension in nested list`() = - runBlocking { - val questionnaire = - Questionnaire().apply { - id = "a-questionnaire" - addItem( + fun `should detect cyclic dependency for questionnaire item with calculated expression extension in flat list`() = + runBlocking { + val questionnaire = + Questionnaire().apply { + id = "a-questionnaire" + addItem( Questionnaire.QuestionnaireItemComponent().apply { linkId = "a-birthdate" type = Questionnaire.QuestionnaireItemType.DATE @@ -3152,17 +3165,8 @@ class QuestionnaireViewModelTest( } } ) - .addItem() - .apply { - linkId = "a.1" - type = Questionnaire.QuestionnaireItemType.GROUP - } - .addItem() - .apply { - linkId = "a.1.1" - type = Questionnaire.QuestionnaireItemType.GROUP - } - .addItem( + + addItem( Questionnaire.QuestionnaireItemComponent().apply { linkId = "a-age-years" type = Questionnaire.QuestionnaireItemType.INTEGER @@ -3178,15 +3182,82 @@ class QuestionnaireViewModelTest( } } ) - } + } - val exception = - Assert.assertThrows(null, IllegalStateException::class.java) { - createQuestionnaireViewModel(questionnaire) - } - assertThat(exception.message) - .isEqualTo("a-birthdate and a-age-years have cyclic dependency in expression based extension") - } + val exception = + Assert.assertThrows(null, IllegalStateException::class.java) { + createQuestionnaireViewModel(questionnaire) + } + assertThat(exception.message) + .isEqualTo( + "a-birthdate and a-age-years have cyclic dependency in expression based extension" + ) + } + + @Test + fun `should detect cyclic dependency for questionnaire item with calculated expression extension in nested list`() = + runBlocking { + val questionnaire = + Questionnaire().apply { + id = "a-questionnaire" + addItem( + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "a-birthdate" + type = Questionnaire.QuestionnaireItemType.DATE + addInitial( + Questionnaire.QuestionnaireItemInitialComponent( + DateType(Date()).apply { add(Calendar.YEAR, -2) } + ) + ) + addExtension().apply { + url = EXTENSION_CALCULATED_EXPRESSION_URL + setValue( + Expression().apply { + this.language = "text/fhirpath" + this.expression = + "%resource.repeat(item).where(linkId='a-age-years' and answer.empty().not()).select(today() - answer.value)" + } + ) + } + } + ) + .addItem() + .apply { + linkId = "a.1" + type = Questionnaire.QuestionnaireItemType.GROUP + } + .addItem() + .apply { + linkId = "a.1.1" + type = Questionnaire.QuestionnaireItemType.GROUP + } + .addItem( + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "a-age-years" + type = Questionnaire.QuestionnaireItemType.INTEGER + addExtension().apply { + url = EXTENSION_CALCULATED_EXPRESSION_URL + setValue( + Expression().apply { + this.language = "text/fhirpath" + this.expression = + "today().toString().substring(0, 4).toInteger() - %resource.repeat(item).where(linkId='a-birthdate').answer.value.toString().substring(0, 4).toInteger()" + } + ) + } + } + ) + } + + val exception = + Assert.assertThrows(null, IllegalStateException::class.java) { + createQuestionnaireViewModel(questionnaire) + } + assertThat(exception.message) + .isEqualTo( + "a-birthdate and a-age-years have cyclic dependency in expression based extension" + ) + } private fun createQuestionnaireViewModel( questionnaire: Questionnaire, diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluatorTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluatorTest.kt index 50d49802b5..92edf9efb3 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluatorTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluatorTest.kt @@ -39,277 +39,277 @@ import org.junit.Test class ExpressionEvaluatorTest { @Test fun `should return not null value with simple variable expression for questionnaire root level`() = - runBlocking { - val questionnaire = - Questionnaire().apply { - addExtension().apply { - url = EXTENSION_VARIABLE_URL - setValue( - Expression().apply { - name = "A" - language = "text/fhirpath" - expression = "1" - } - ) + runBlocking { + val questionnaire = + Questionnaire().apply { + addExtension().apply { + url = EXTENSION_VARIABLE_URL + setValue( + Expression().apply { + name = "A" + language = "text/fhirpath" + expression = "1" + } + ) + } } - } - val result = - ExpressionEvaluator.evaluateQuestionnaireVariableExpression( - questionnaire.variableExpressions.first(), - questionnaire, - QuestionnaireResponse() - ) + val result = + ExpressionEvaluator.evaluateQuestionnaireVariableExpression( + questionnaire.variableExpressions.first(), + questionnaire, + QuestionnaireResponse() + ) - assertThat((result as Type).asStringValue()).isEqualTo("1") - } + assertThat((result as Type).asStringValue()).isEqualTo("1") + } @Test fun `should return not null value with variables dependent on other variables for questionnaire root level`() = - runBlocking { - val questionnaire = - Questionnaire().apply { - addExtension().apply { - url = EXTENSION_VARIABLE_URL - setValue( - Expression().apply { - name = "A" - language = "text/fhirpath" - expression = "1" - } - ) - } - addExtension().apply { - url = EXTENSION_VARIABLE_URL - setValue( - Expression().apply { - name = "B" - language = "text/fhirpath" - expression = "%A + 1" - } - ) + runBlocking { + val questionnaire = + Questionnaire().apply { + addExtension().apply { + url = EXTENSION_VARIABLE_URL + setValue( + Expression().apply { + name = "A" + language = "text/fhirpath" + expression = "1" + } + ) + } + addExtension().apply { + url = EXTENSION_VARIABLE_URL + setValue( + Expression().apply { + name = "B" + language = "text/fhirpath" + expression = "%A + 1" + } + ) + } } - } - val result = - ExpressionEvaluator.evaluateQuestionnaireVariableExpression( - questionnaire.variableExpressions.last(), - questionnaire, - QuestionnaireResponse() - ) + val result = + ExpressionEvaluator.evaluateQuestionnaireVariableExpression( + questionnaire.variableExpressions.last(), + questionnaire, + QuestionnaireResponse() + ) - assertThat((result as Type).asStringValue()).isEqualTo("2") - } + assertThat((result as Type).asStringValue()).isEqualTo("2") + } @Test fun `should return not null value with variables dependent on other variables in parent for questionnaire item level`() = - runBlocking { - val questionnaire = - Questionnaire().apply { - id = "a-questionnaire" - addItem( - Questionnaire.QuestionnaireItemComponent().apply { - linkId = "a-group-item" - text = "a question" - type = Questionnaire.QuestionnaireItemType.GROUP - addExtension().apply { - url = EXTENSION_VARIABLE_URL - setValue( - Expression().apply { - name = "A" - language = "text/fhirpath" - expression = "1" + runBlocking { + val questionnaire = + Questionnaire().apply { + id = "a-questionnaire" + addItem( + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "a-group-item" + text = "a question" + type = Questionnaire.QuestionnaireItemType.GROUP + addExtension().apply { + url = EXTENSION_VARIABLE_URL + setValue( + Expression().apply { + name = "A" + language = "text/fhirpath" + expression = "1" + } + ) + } + addItem( + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "an-item" + text = "a question" + type = Questionnaire.QuestionnaireItemType.TEXT + addExtension().apply { + url = EXTENSION_VARIABLE_URL + setValue( + Expression().apply { + name = "B" + language = "text/fhirpath" + expression = "%A + 1" + } + ) + } } ) } - addItem( - Questionnaire.QuestionnaireItemComponent().apply { - linkId = "an-item" - text = "a question" - type = Questionnaire.QuestionnaireItemType.TEXT - addExtension().apply { - url = EXTENSION_VARIABLE_URL - setValue( - Expression().apply { - name = "B" - language = "text/fhirpath" - expression = "%A + 1" - } - ) - } - } - ) - } - ) - } + ) + } - val result = - ExpressionEvaluator.evaluateQuestionnaireItemVariableExpression( - questionnaire.item[0].item[0].variableExpressions.last(), - questionnaire, - QuestionnaireResponse(), - mapOf(questionnaire.item[0].item[0] to questionnaire.item[0]), - questionnaire.item[0].item[0] - ) + val result = + ExpressionEvaluator.evaluateQuestionnaireItemVariableExpression( + questionnaire.item[0].item[0].variableExpressions.last(), + questionnaire, + QuestionnaireResponse(), + mapOf(questionnaire.item[0].item[0] to questionnaire.item[0]), + questionnaire.item[0].item[0] + ) - assertThat((result as Type).asStringValue()).isEqualTo("2") - } + assertThat((result as Type).asStringValue()).isEqualTo("2") + } @Test fun `should return not null value with variables dependent on multiple variables for questionnaire root level`() = - runBlocking { - val questionnaire = - Questionnaire().apply { - addExtension().apply { - url = EXTENSION_VARIABLE_URL - setValue( - Expression().apply { - name = "A" - language = "text/fhirpath" - expression = "1" - } - ) - } - addExtension().apply { - url = EXTENSION_VARIABLE_URL - setValue( - Expression().apply { - name = "B" - language = "text/fhirpath" - expression = "2" - } - ) - } - addExtension().apply { - url = EXTENSION_VARIABLE_URL - setValue( - Expression().apply { - name = "C" - language = "text/fhirpath" - expression = "%A + %B" - } - ) + runBlocking { + val questionnaire = + Questionnaire().apply { + addExtension().apply { + url = EXTENSION_VARIABLE_URL + setValue( + Expression().apply { + name = "A" + language = "text/fhirpath" + expression = "1" + } + ) + } + addExtension().apply { + url = EXTENSION_VARIABLE_URL + setValue( + Expression().apply { + name = "B" + language = "text/fhirpath" + expression = "2" + } + ) + } + addExtension().apply { + url = EXTENSION_VARIABLE_URL + setValue( + Expression().apply { + name = "C" + language = "text/fhirpath" + expression = "%A + %B" + } + ) + } } - } - val result = - ExpressionEvaluator.evaluateQuestionnaireVariableExpression( - questionnaire.variableExpressions.last(), - questionnaire, - QuestionnaireResponse() - ) + val result = + ExpressionEvaluator.evaluateQuestionnaireVariableExpression( + questionnaire.variableExpressions.last(), + questionnaire, + QuestionnaireResponse() + ) - assertThat((result as Type).asStringValue()).isEqualTo("3") - } + assertThat((result as Type).asStringValue()).isEqualTo("3") + } @Test fun `should return null with variables dependent on missing variables for questionnaire root level`() = - runBlocking { - val questionnaire = - Questionnaire().apply { - addExtension().apply { - url = EXTENSION_VARIABLE_URL - setValue( - Expression().apply { - name = "A" - language = "text/fhirpath" - expression = "%B + 1" - } - ) + runBlocking { + val questionnaire = + Questionnaire().apply { + addExtension().apply { + url = EXTENSION_VARIABLE_URL + setValue( + Expression().apply { + name = "A" + language = "text/fhirpath" + expression = "%B + 1" + } + ) + } } - } - val result = - ExpressionEvaluator.evaluateQuestionnaireVariableExpression( - questionnaire.variableExpressions.last(), - questionnaire, - QuestionnaireResponse() - ) + val result = + ExpressionEvaluator.evaluateQuestionnaireVariableExpression( + questionnaire.variableExpressions.last(), + questionnaire, + QuestionnaireResponse() + ) - assertThat(result).isEqualTo(null) - } + assertThat(result).isEqualTo(null) + } @Test fun `should return not null value with variables dependent on other variables at origin for questionnaire item level`() = - runBlocking { - val questionnaire = - Questionnaire().apply { - addItem( - Questionnaire.QuestionnaireItemComponent().apply { - linkId = "an-item" - text = "a question" - type = Questionnaire.QuestionnaireItemType.TEXT - addExtension().apply { - url = EXTENSION_VARIABLE_URL - setValue( - Expression().apply { - name = "B" - language = "text/fhirpath" - expression = "1" - } - ) - } - addExtension().apply { - url = EXTENSION_VARIABLE_URL - setValue( - Expression().apply { - name = "A" - language = "text/fhirpath" - expression = "%B + 1" - } - ) + runBlocking { + val questionnaire = + Questionnaire().apply { + addItem( + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "an-item" + text = "a question" + type = Questionnaire.QuestionnaireItemType.TEXT + addExtension().apply { + url = EXTENSION_VARIABLE_URL + setValue( + Expression().apply { + name = "B" + language = "text/fhirpath" + expression = "1" + } + ) + } + addExtension().apply { + url = EXTENSION_VARIABLE_URL + setValue( + Expression().apply { + name = "A" + language = "text/fhirpath" + expression = "%B + 1" + } + ) + } } - } - ) - } + ) + } - val result = - ExpressionEvaluator.evaluateQuestionnaireItemVariableExpression( - questionnaire.item[0].variableExpressions.last(), - questionnaire, - QuestionnaireResponse(), - mapOf(), - questionnaire.item[0] - ) + val result = + ExpressionEvaluator.evaluateQuestionnaireItemVariableExpression( + questionnaire.item[0].variableExpressions.last(), + questionnaire, + QuestionnaireResponse(), + mapOf(), + questionnaire.item[0] + ) - assertThat((result as Type).asStringValue()).isEqualTo("2") - } + assertThat((result as Type).asStringValue()).isEqualTo("2") + } @Test fun `should return null with variables dependent on missing variables at origin for questionnaire item level`() = - runBlocking { - val questionnaire = - Questionnaire().apply { - addItem( - Questionnaire.QuestionnaireItemComponent().apply { - linkId = "an-item" - text = "a question" - type = Questionnaire.QuestionnaireItemType.TEXT - addExtension().apply { - url = EXTENSION_VARIABLE_URL - setValue( - Expression().apply { - name = "A" - language = "text/fhirpath" - expression = "%B + 1" - } - ) + runBlocking { + val questionnaire = + Questionnaire().apply { + addItem( + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "an-item" + text = "a question" + type = Questionnaire.QuestionnaireItemType.TEXT + addExtension().apply { + url = EXTENSION_VARIABLE_URL + setValue( + Expression().apply { + name = "A" + language = "text/fhirpath" + expression = "%B + 1" + } + ) + } } - } - ) - } + ) + } - val result = - ExpressionEvaluator.evaluateQuestionnaireItemVariableExpression( - questionnaire.item[0].variableExpressions.last(), - questionnaire, - QuestionnaireResponse(), - mapOf(), - questionnaire.item[0] - ) + val result = + ExpressionEvaluator.evaluateQuestionnaireItemVariableExpression( + questionnaire.item[0].variableExpressions.last(), + questionnaire, + QuestionnaireResponse(), + mapOf(), + questionnaire.item[0] + ) - assertThat(result).isEqualTo(null) - } + assertThat(result).isEqualTo(null) + } @Test fun `should throw illegal argument exception with missing expression name for questionnaire variables`() { @@ -422,62 +422,63 @@ class ExpressionEvaluatorTest { @Test fun `should return not null value with expression dependent on answers of items for questionnaire item level`() = - runBlocking { - val questionnaire = - Questionnaire().apply { - id = "a-questionnaire" - addItem( - Questionnaire.QuestionnaireItemComponent().apply { - linkId = "a-group-item" - text = "a question" - type = Questionnaire.QuestionnaireItemType.GROUP - addExtension().apply { - url = EXTENSION_VARIABLE_URL - setValue( - Expression().apply { - name = "M" - language = "text/fhirpath" - expression = "%resource.repeat(item).where(linkId='an-item').answer.first().value" + runBlocking { + val questionnaire = + Questionnaire().apply { + id = "a-questionnaire" + addItem( + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "a-group-item" + text = "a question" + type = Questionnaire.QuestionnaireItemType.GROUP + addExtension().apply { + url = EXTENSION_VARIABLE_URL + setValue( + Expression().apply { + name = "M" + language = "text/fhirpath" + expression = + "%resource.repeat(item).where(linkId='an-item').answer.first().value" + } + ) + } + addItem( + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "an-item" + text = "a question" + type = Questionnaire.QuestionnaireItemType.TEXT } ) } - addItem( - Questionnaire.QuestionnaireItemComponent().apply { - linkId = "an-item" - text = "a question" - type = Questionnaire.QuestionnaireItemType.TEXT - } - ) - } - ) - } + ) + } - val questionnaireResponse = - QuestionnaireResponse().apply { - id = "a-questionnaire-response" - addItem( - QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { - linkId = "an-item" - addAnswer( - QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { - value = IntegerType(2) - } - ) - } - ) - } + val questionnaireResponse = + QuestionnaireResponse().apply { + id = "a-questionnaire-response" + addItem( + QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { + linkId = "an-item" + addAnswer( + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { + value = IntegerType(2) + } + ) + } + ) + } - val result = - ExpressionEvaluator.evaluateQuestionnaireItemVariableExpression( - questionnaire.item[0].variableExpressions.last(), - questionnaire, - questionnaireResponse, - mapOf(), - questionnaire.item[0] - ) + val result = + ExpressionEvaluator.evaluateQuestionnaireItemVariableExpression( + questionnaire.item[0].variableExpressions.last(), + questionnaire, + questionnaireResponse, + mapOf(), + questionnaire.item[0] + ) - assertThat((result as Type).asStringValue()).isEqualTo("2") - } + assertThat((result as Type).asStringValue()).isEqualTo("2") + } @Test fun `evaluateCalculatedExpressions should return list of calculated values`() = runBlocking { @@ -533,7 +534,6 @@ class ExpressionEvaluatorTest { questionnaire.item.elementAt(1), questionnaire, questionnaireResponse, - emptySet(), emptyMap() ) @@ -543,144 +543,142 @@ class ExpressionEvaluatorTest { @Test fun `evaluateCalculatedExpressions should return list of calculated values with variables`() = - runBlocking { - val questionnaire = - Questionnaire().apply { - id = "a-questionnaire" - addExtension().apply { - url = EXTENSION_VARIABLE_URL - setValue( - Expression().apply { - name = "AGE-YEARS" - language = "text/fhirpath" - expression = - "%resource.repeat(item).where(linkId='a-age-years' and answer.empty().not()).select(today() - answer.value)" + runBlocking { + val questionnaire = + Questionnaire().apply { + id = "a-questionnaire" + addExtension().apply { + url = EXTENSION_VARIABLE_URL + setValue( + Expression().apply { + name = "AGE-YEARS" + language = "text/fhirpath" + expression = + "%resource.repeat(item).where(linkId='a-age-years' and answer.empty().not()).select(today() - answer.value)" + } + ) + } + addItem( + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "a-birthdate" + type = Questionnaire.QuestionnaireItemType.DATE + addExtension().apply { + url = EXTENSION_CALCULATED_EXPRESSION_URL + setValue( + Expression().apply { + this.language = "text/fhirpath" + this.expression = "%AGE-YEARS" + } + ) + } + } + ) + addItem( + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "a-age-years" + type = Questionnaire.QuestionnaireItemType.QUANTITY } ) } - addItem( - Questionnaire.QuestionnaireItemComponent().apply { - linkId = "a-birthdate" - type = Questionnaire.QuestionnaireItemType.DATE - addExtension().apply { - url = EXTENSION_CALCULATED_EXPRESSION_URL - setValue( - Expression().apply { - this.language = "text/fhirpath" - this.expression = "%AGE-YEARS" - } - ) + + val questionnaireResponse = + QuestionnaireResponse().apply { + addItem( + QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { + linkId = "a-birthdate" } - } - ) - addItem( - Questionnaire.QuestionnaireItemComponent().apply { - linkId = "a-age-years" - type = Questionnaire.QuestionnaireItemType.QUANTITY - } - ) - } + ) + addItem( + QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { + linkId = "a-age-years" + answer = + listOf( + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { + this.value = Quantity(1).apply { unit = "year" } + } + ) + } + ) + } - val questionnaireResponse = - QuestionnaireResponse().apply { - addItem( - QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { - linkId = "a-birthdate" - } - ) - addItem( - QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { - linkId = "a-age-years" - answer = - listOf( - QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { - this.value = Quantity(1).apply { unit = "year" } - } - ) - } + val result = + evaluateCalculatedExpressions( + questionnaire.item.elementAt(1), + questionnaire, + questionnaireResponse, + emptyMap() ) - } - val result = - evaluateCalculatedExpressions( - questionnaire.item.elementAt(1), - questionnaire, - questionnaireResponse, - emptySet(), - emptyMap() - ) - - assertThat(result.first().second.first().asStringValue()) - .isEqualTo(DateType(Date()).apply { add(Calendar.YEAR, -1) }.asStringValue()) - } + assertThat(result.first().second.first().asStringValue()) + .isEqualTo(DateType(Date()).apply { add(Calendar.YEAR, -1) }.asStringValue()) + } @Test fun `evaluateCalculatedExpressions should not include item in list when item has been modified`() = - runBlocking { - val questionnaire = - Questionnaire().apply { - id = "a-questionnaire" - addItem( - Questionnaire.QuestionnaireItemComponent().apply { - linkId = "a-birthdate" - type = Questionnaire.QuestionnaireItemType.DATE - addExtension().apply { - url = EXTENSION_CALCULATED_EXPRESSION_URL - setValue( - Expression().apply { - this.language = "text/fhirpath" - this.expression = - "%resource.repeat(item).where(linkId='a-age-years' and answer.empty().not()).select(today() - answer.value)" - } - ) + runBlocking { + val questionnaire = + Questionnaire().apply { + id = "a-questionnaire" + addItem( + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "a-birthdate" + type = Questionnaire.QuestionnaireItemType.DATE + addExtension().apply { + url = EXTENSION_CALCULATED_EXPRESSION_URL + setValue( + Expression().apply { + this.language = "text/fhirpath" + this.expression = + "%resource.repeat(item).where(linkId='a-age-years' and answer.empty().not()).select(today() - answer.value)" + } + ) + } } - } - ) - addItem( - Questionnaire.QuestionnaireItemComponent().apply { - linkId = "a-age-years" - type = Questionnaire.QuestionnaireItemType.QUANTITY - } - ) - } + ) + addItem( + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "a-age-years" + type = Questionnaire.QuestionnaireItemType.QUANTITY + } + ) + } - val questionnaireResponse = - QuestionnaireResponse().apply { - addItem( - QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { - linkId = "a-birthdate" - answer = - listOf( - QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { - value = DateType(Date()) - } - ) - } - ) - addItem( - QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { - linkId = "a-age-years" - answer = - listOf( - QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { - this.value = Quantity(1).apply { unit = "year" } - } - ) - } - ) - } + val questionnaireResponse = + QuestionnaireResponse().apply { + addItem( + QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { + linkId = "a-birthdate" + answer = + listOf( + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { + value = DateType(Date()) + } + ) + } + ) + addItem( + QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { + linkId = "a-age-years" + answer = + listOf( + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { + this.value = Quantity(1).apply { unit = "year" } + } + ) + } + ) + } - val result = - evaluateCalculatedExpressions( - questionnaire.item.elementAt(1), - questionnaire, - questionnaireResponse, - setOf(questionnaireResponse.itemFirstRep), - emptyMap() - ) + val result = + evaluateCalculatedExpressions( + questionnaire.item.elementAt(1), + questionnaire, + questionnaireResponse, + emptyMap() + ) - assertThat(result).isEmpty() - } + assertThat(result).isEmpty() + } @Test fun `detectExpressionCyclicDependency() should throw illegal argument exception when item with calculated expression have cyclic dependency`() { diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/MaxValueConstraintValidatorTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/MaxValueConstraintValidatorTest.kt index 59b50f19dd..5b6d1a16cd 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/MaxValueConstraintValidatorTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/MaxValueConstraintValidatorTest.kt @@ -123,8 +123,8 @@ class MaxValueConstraintValidatorTest { } ) assertTrue( - (MinValueConstraintValidator.getMinValue(questionItem.first()) as? DateType)?.valueAsString - .equals(today) == true + (MinValueConstraintValidator.getMinValue(questionItem.first()) as? DateType) + ?.valueAsString.equals(today) == true ) } @@ -144,9 +144,8 @@ class MaxValueConstraintValidatorTest { ) assertTrue( - (MinValueConstraintValidator.getMinValue(questionItem.first()) as? DateType)?.value?.equals( - dateType.value - ) == true + (MinValueConstraintValidator.getMinValue(questionItem.first()) as? DateType) + ?.value?.equals(dateType.value) == true ) } @@ -166,9 +165,8 @@ class MaxValueConstraintValidatorTest { ) assertTrue( - (MaxValueConstraintValidator.getMaxValue(questionItem.first()) as? DateType)?.value?.equals( - dateType.value - ) == true + (MaxValueConstraintValidator.getMaxValue(questionItem.first()) as? DateType) + ?.value?.equals(dateType.value) == true ) } @@ -201,8 +199,8 @@ class MaxValueConstraintValidatorTest { ) assertTrue( - (MaxValueConstraintValidator.getMaxValue(questionItem.first()) as? DateType)?.valueAsString - ?.equals(today) == true + (MaxValueConstraintValidator.getMaxValue(questionItem.first()) as? DateType) + ?.valueAsString?.equals(today) == true ) } @@ -235,8 +233,8 @@ class MaxValueConstraintValidatorTest { ) assertTrue( - (MaxValueConstraintValidator.getMaxValue(questionItem.first()) as? DateType)?.valueAsString - ?.equals(fiveDaysAhead) == true + (MaxValueConstraintValidator.getMaxValue(questionItem.first()) as? DateType) + ?.valueAsString?.equals(fiveDaysAhead) == true ) } } From 89f1115bbba2a9dff72f2735e240f2fe36f9c870 Mon Sep 17 00:00:00 2001 From: maimoonak Date: Mon, 10 Oct 2022 22:42:18 +0500 Subject: [PATCH 34/34] Fix failing test --- .../fhirpath/ExpressionEvaluatorTest.kt | 67 ------------------- 1 file changed, 67 deletions(-) diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluatorTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluatorTest.kt index 92edf9efb3..6a8114af23 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluatorTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluatorTest.kt @@ -613,73 +613,6 @@ class ExpressionEvaluatorTest { .isEqualTo(DateType(Date()).apply { add(Calendar.YEAR, -1) }.asStringValue()) } - @Test - fun `evaluateCalculatedExpressions should not include item in list when item has been modified`() = - runBlocking { - val questionnaire = - Questionnaire().apply { - id = "a-questionnaire" - addItem( - Questionnaire.QuestionnaireItemComponent().apply { - linkId = "a-birthdate" - type = Questionnaire.QuestionnaireItemType.DATE - addExtension().apply { - url = EXTENSION_CALCULATED_EXPRESSION_URL - setValue( - Expression().apply { - this.language = "text/fhirpath" - this.expression = - "%resource.repeat(item).where(linkId='a-age-years' and answer.empty().not()).select(today() - answer.value)" - } - ) - } - } - ) - addItem( - Questionnaire.QuestionnaireItemComponent().apply { - linkId = "a-age-years" - type = Questionnaire.QuestionnaireItemType.QUANTITY - } - ) - } - - val questionnaireResponse = - QuestionnaireResponse().apply { - addItem( - QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { - linkId = "a-birthdate" - answer = - listOf( - QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { - value = DateType(Date()) - } - ) - } - ) - addItem( - QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { - linkId = "a-age-years" - answer = - listOf( - QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { - this.value = Quantity(1).apply { unit = "year" } - } - ) - } - ) - } - - val result = - evaluateCalculatedExpressions( - questionnaire.item.elementAt(1), - questionnaire, - questionnaireResponse, - emptyMap() - ) - - assertThat(result).isEmpty() - } - @Test fun `detectExpressionCyclicDependency() should throw illegal argument exception when item with calculated expression have cyclic dependency`() { val questionnaire =