From 8179c81eae2b1b25a3221cca07f47cfc03b2780d Mon Sep 17 00:00:00 2001 From: Elly Kitoto Date: Thu, 19 Sep 2024 17:56:32 +0300 Subject: [PATCH] Exclude questionnaire items from population (#3460) * Exclude configured fields from form prepopulation Signed-off-by: Elly Kitoto * Document exclusion of Questionnaire fields via config Signed-off-by: Elly Kitoto * Handle exclusion of nested QuestionnaireResponse items Signed-off-by: Elly Kitoto --------- Signed-off-by: Elly Kitoto Co-authored-by: Allan Onchuru <16164649+allan-on@users.noreply.github.com> --- .../configuration/QuestionnaireConfig.kt | 2 +- .../questionnaire/QuestionnaireViewModel.kt | 38 +++++- .../QuestionnaireViewModelTest.kt | 127 +++++++++++++++++- .../app/configuring/forms/forms.mdx | 16 +++ 4 files changed, 180 insertions(+), 3 deletions(-) diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/QuestionnaireConfig.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/QuestionnaireConfig.kt index 60da3c1f83..0c5bc2f57c 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/QuestionnaireConfig.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/configuration/QuestionnaireConfig.kt @@ -169,5 +169,5 @@ enum class LinkIdType : Parcelable { READ_ONLY, BARCODE, LOCATION, - IDENTIFIER, + PREPOPULATION_EXCLUSION, } diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModel.kt index 472effbc78..90c3747bf8 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModel.kt @@ -65,6 +65,7 @@ import org.smartregister.fhircore.engine.BuildConfig import org.smartregister.fhircore.engine.configuration.ConfigType import org.smartregister.fhircore.engine.configuration.ConfigurationRegistry import org.smartregister.fhircore.engine.configuration.GroupResourceConfig +import org.smartregister.fhircore.engine.configuration.LinkIdType import org.smartregister.fhircore.engine.configuration.QuestionnaireConfig import org.smartregister.fhircore.engine.configuration.app.ApplicationConfiguration import org.smartregister.fhircore.engine.configuration.app.CodingSystemUsage @@ -1118,14 +1119,49 @@ constructor( null } + // Exclude the configured fields from QR + if (questionnaireResponse != null) { + val exclusionLinkIdsMap: Map = + questionnaireConfig.linkIds + ?.asSequence() + ?.filter { it.type == LinkIdType.PREPOPULATION_EXCLUSION } + ?.associateBy { it.linkId } + ?.mapValues { it.value.type == LinkIdType.PREPOPULATION_EXCLUSION } ?: emptyMap() + + questionnaireResponse.item = + excludePrepopulationFields(questionnaireResponse.item.toMutableList(), exclusionLinkIdsMap) + } return Pair(questionnaireResponse, launchContextResources) } + fun excludePrepopulationFields( + items: MutableList, + exclusionMap: Map, + ): MutableList { + val stack = LinkedList>() + stack.push(items) + while (stack.isNotEmpty()) { + val currentItems = stack.pop() + val iterator = currentItems.iterator() + while (iterator.hasNext()) { + val item = iterator.next() + if (exclusionMap.containsKey(item.linkId)) { + iterator.remove() + } else if (item.item.isNotEmpty()) { + stack.push(item.item) + } + } + } + return items + } + private fun List.removeUnAnsweredItems(): List { - return this.filter { it.hasAnswer() || it.item.isNotEmpty() } + return this.asSequence() + .filter { it.hasAnswer() || it.item.isNotEmpty() } .onEach { it.item = it.item.removeUnAnsweredItems() } .filter { it.hasAnswer() || it.item.isNotEmpty() } + .toList() } /** diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModelTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModelTest.kt index d0c93b6fdc..88f62221e5 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModelTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/questionnaire/QuestionnaireViewModelTest.kt @@ -163,7 +163,6 @@ class QuestionnaireViewModelTest : RobolectricTest() { @ExperimentalCoroutinesApi fun setUp() { hiltRule.inject() - // Write practitioner and organization to shared preferences sharedPreferencesHelper.write( SharedPreferenceKey.PRACTITIONER_ID.name, @@ -1840,6 +1839,92 @@ class QuestionnaireViewModelTest : RobolectricTest() { Assert.assertTrue(initialValueDate.isToday) } + @Test + fun testThatPopulateQuestionnaireSetInitialDefaultValueButExcludesFieldFromResponse() = + runTest(timeout = 90.seconds) { + val thisQuestionnaireConfig = + questionnaireConfig.copy( + resourceType = ResourceType.Patient, + resourceIdentifier = patient.logicalId, + type = QuestionnaireType.EDIT.name, + linkIds = + listOf( + LinkIdConfig("dateToday", LinkIdType.PREPOPULATION_EXCLUSION), + ), + ) + val questionnaireViewModelInstance = + QuestionnaireViewModel( + defaultRepository = defaultRepository, + dispatcherProvider = defaultRepository.dispatcherProvider, + fhirCarePlanGenerator = fhirCarePlanGenerator, + resourceDataRulesExecutor = resourceDataRulesExecutor, + transformSupportServices = mockk(), + sharedPreferencesHelper = sharedPreferencesHelper, + fhirOperator = fhirOperator, + fhirValidatorProvider = fhirValidatorProvider, + fhirPathDataExtractor = fhirPathDataExtractor, + configurationRegistry = configurationRegistry, + ) + val questionnaireWithDefaultDate = + Questionnaire().apply { + id = thisQuestionnaireConfig.id + addItem( + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "dateToday" + type = Questionnaire.QuestionnaireItemType.DATE + addExtension( + Extension( + "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression", + Expression().apply { + language = "text/fhirpath" + expression = "today()" + }, + ), + ) + }, + ) + } + + val questionnaireResponse = + QuestionnaireResponse().apply { + addItem( + QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { + linkId = "dateToday" + addAnswer( + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { + value = DateType(Date()) + }, + ) + }, + ) + setQuestionnaire( + thisQuestionnaireConfig.id.asReference(ResourceType.Questionnaire).reference, + ) + } + + coEvery { + fhirEngine.get( + thisQuestionnaireConfig.resourceType!!, + thisQuestionnaireConfig.resourceIdentifier!!, + ) + } returns patient + + coEvery { fhirEngine.search(any()) } returns + listOf( + SearchResult(questionnaireResponse, included = null, revIncluded = null), + ) + + val (result, _) = + questionnaireViewModelInstance.populateQuestionnaire( + questionnaire = questionnaireWithDefaultDate, + questionnaireConfig = thisQuestionnaireConfig, + actionParameters = emptyList(), + ) + + Assert.assertNotNull(result?.item) + Assert.assertTrue(result!!.item.isEmpty()) + } + @Test fun testThatPopulateQuestionnaireReturnsQuestionnaireResponseWithUnAnsweredRemoved() = runTest { val questionnaireViewModelInstance = @@ -1947,4 +2032,44 @@ class QuestionnaireViewModelTest : RobolectricTest() { Assert.assertNotNull(result.first) Assert.assertTrue(result.first!!.find("linkid-1") == null) } + + @Test + fun testExcludeNestedItemFromQuestionnairePrepopulation() { + val item1 = QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { linkId = "1" } + val item2 = QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { linkId = "2" } + val item3 = + QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { + linkId = "3" + item = + mutableListOf( + QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { linkId = "3.1" }, + QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { + linkId = "3.2" + item = + mutableListOf( + QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { + linkId = "3.2.1" + }, + QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { + linkId = "3.2.2" + }, + ) + }, + ) + } + + val items = mutableListOf(item1, item2, item3) + val exclusionMap = mapOf("2" to true, "3.1" to true, "3.2.2" to true) + val filteredItems = questionnaireViewModel.excludePrepopulationFields(items, exclusionMap) + Assert.assertEquals(2, filteredItems.size) + Assert.assertEquals("1", filteredItems.first().linkId) + val itemThree = filteredItems.last() + Assert.assertEquals("3", itemThree.linkId) + Assert.assertEquals(1, itemThree.item.size) + val itemThreePointTwo = itemThree.item.first() + Assert.assertEquals("3.2", itemThreePointTwo.linkId) + Assert.assertEquals(1, itemThreePointTwo.item.size) + val itemThreePointTwoOne = itemThreePointTwo.item.first() + Assert.assertEquals("3.2.1", itemThreePointTwoOne.linkId) + } } diff --git a/docs/engineering/app/configuring/forms/forms.mdx b/docs/engineering/app/configuring/forms/forms.mdx index 0b31ed5f3f..f5d4878e95 100644 --- a/docs/engineering/app/configuring/forms/forms.mdx +++ b/docs/engineering/app/configuring/forms/forms.mdx @@ -441,3 +441,19 @@ The QR code widget supports adding an arbitrary number of QR codes, implemented } ``` The extension's implementation can be found [here](https://github.com/opensrp/fhircore/blob/main/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/sdc/qrCode/EditTextQrCodeViewHolderFactory.kt) + +## Excluding questionnaire fields from prepopulation + +Use the `linkIds` property to provide linkIds for the Questionnaire fields that should not be pre-field with data during editing or when opening the questionnaire in a read only format. +The `LinkIdType` required for the exclusion to work is `PREPOPULATION_EXCLUSION`. Nested fields can also be excluded from pre-population of forms. + +Example: + +```json +"linkIds": [ + { + "linkId": "ad29c7bd-8041-427f-8e63-b066afe5b438-009", + "type": "PREPOPULATION_EXCLUSION" + } +] +```