diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/ResourceExtension.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/ResourceExtension.kt index f6f55fc3c5..6ddcb2752e 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/ResourceExtension.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/util/extension/ResourceExtension.kt @@ -224,6 +224,31 @@ fun List.prepareQuestionsForReadingOrE } } +/** + * Set all questions that are not of type [Questionnaire.QuestionnaireItemType.GROUP] to readOnly if + * [readOnlyLinkIds] item are there while editing the form. This also generates the correct FHIRPath + * population expression for each question when mapped to the corresponding [QuestionnaireResponse] + */ +fun List.prepareQuestionsForEditing( + path: String = "QuestionnaireResponse.item", + readOnlyLinkIds: List? = emptyList(), +) { + forEach { item -> + if (item.type != Questionnaire.QuestionnaireItemType.GROUP) { + item.readOnly = readOnlyLinkIds?.contains(item.linkId) == true + item.item.prepareQuestionsForEditing( + "$path.where(linkId = '${item.linkId}').answer.item", + readOnlyLinkIds, + ) + } else { + item.item.prepareQuestionsForEditing( + "$path.where(linkId = '${item.linkId}').item", + readOnlyLinkIds, + ) + } + } +} + /** Delete resources in [QuestionnaireResponse.contained] from the database */ suspend fun QuestionnaireResponse.deleteRelatedResources(defaultRepository: DefaultRepository) { contained.forEach { defaultRepository.delete(it) } diff --git a/android/engine/src/test/java/org/smartregister/fhircore/engine/robolectric/RobolectricTest.kt b/android/engine/src/test/java/org/smartregister/fhircore/engine/robolectric/RobolectricTest.kt index 366488e29a..a18b5ec614 100644 --- a/android/engine/src/test/java/org/smartregister/fhircore/engine/robolectric/RobolectricTest.kt +++ b/android/engine/src/test/java/org/smartregister/fhircore/engine/robolectric/RobolectricTest.kt @@ -45,8 +45,8 @@ abstract class RobolectricTest { val latch = CountDownLatch(1) val observer: Observer = object : Observer { - override fun onChanged(o: T) { - data[0] = o + override fun onChanged(value: T) { + data[0] = value latch.countDown() liveData.removeObserver(this) } diff --git a/android/engine/src/test/java/org/smartregister/fhircore/engine/util/extension/ResourceExtensionTest.kt b/android/engine/src/test/java/org/smartregister/fhircore/engine/util/extension/ResourceExtensionTest.kt index 3f5e0683c9..d5a1032d2f 100644 --- a/android/engine/src/test/java/org/smartregister/fhircore/engine/util/extension/ResourceExtensionTest.kt +++ b/android/engine/src/test/java/org/smartregister/fhircore/engine/util/extension/ResourceExtensionTest.kt @@ -895,4 +895,17 @@ class ResourceExtensionTest : RobolectricTest() { patient.appendOrganizationInfo(listOf("Organization/12345")) Assert.assertEquals("Organization/12345", patient.managingOrganization.reference) } + + @Test + fun `prepareQuestionsForEditing should set readOnly correctly when readOnlyLinkIds passed`() { + val questionnaire = Questionnaire() + questionnaire.item.add(Questionnaire.QuestionnaireItemComponent().apply { linkId = "1" }) + questionnaire.item.add(Questionnaire.QuestionnaireItemComponent().apply { linkId = "2" }) + questionnaire.item.add(Questionnaire.QuestionnaireItemComponent().apply { linkId = "3" }) + questionnaire.item.prepareQuestionsForEditing("", readOnlyLinkIds = listOf("1", "3")) + + Assert.assertTrue(questionnaire.item[0].readOnly) + Assert.assertFalse(questionnaire.item[1].readOnly) + Assert.assertTrue(questionnaire.item[2].readOnly) + } } 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 4a0605d86d..391edaa178 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 @@ -54,6 +54,7 @@ import org.hl7.fhir.r4.model.ListResource.ListEntryComponent import org.hl7.fhir.r4.model.Parameters import org.hl7.fhir.r4.model.Questionnaire import org.hl7.fhir.r4.model.QuestionnaireResponse +import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseItemComponent import org.hl7.fhir.r4.model.RelatedPerson import org.hl7.fhir.r4.model.Resource import org.hl7.fhir.r4.model.ResourceType @@ -90,6 +91,7 @@ import org.smartregister.fhircore.engine.util.extension.find import org.smartregister.fhircore.engine.util.extension.generateMissingId import org.smartregister.fhircore.engine.util.extension.isIn import org.smartregister.fhircore.engine.util.extension.prePopulateInitialValues +import org.smartregister.fhircore.engine.util.extension.prepareQuestionsForEditing import org.smartregister.fhircore.engine.util.extension.prepareQuestionsForReadingOrEditing import org.smartregister.fhircore.engine.util.extension.showToast import org.smartregister.fhircore.engine.util.extension.updateLastUpdated @@ -167,6 +169,10 @@ constructor( ) } + if (questionnaireConfig.isEditable()) { + item.prepareQuestionsForEditing(readOnlyLinkIds = questionnaireConfig.readOnlyLinkIds) + } + // Pre-populate questionnaire items with configured values allActionParameters ?.filter { (it.paramType == ActionParameterType.PREPOPULATE && it.value.isNotEmpty()) } @@ -974,7 +980,7 @@ constructor( ) ?.let { QuestionnaireResponse().apply { - item = it.item + item = it.item.removeUnAnsweredItems() // Clearing the text prompts the SDK to re-process the content, which includes HTML clearText() } @@ -986,6 +992,13 @@ constructor( return Pair(questionnaireResponse, launchContextResources) } + private fun List.removeUnAnsweredItems(): + List { + return this.filter { it.hasAnswer() || it.item.isNotEmpty() } + .onEach { it.item = it.item.removeUnAnsweredItems() } + .filter { it.hasAnswer() || it.item.isNotEmpty() } + } + /** * Return [Resource]s to be used in the launch context of the questionnaire. Launch context allows * information to be passed into questionnaire based on the context in which the questionnaire is 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 af950781a8..4f24ee3175 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 @@ -20,10 +20,12 @@ import android.app.Application import androidx.test.core.app.ApplicationProvider import ca.uhn.fhir.parser.IParser import com.google.android.fhir.FhirEngine +import com.google.android.fhir.SearchResult import com.google.android.fhir.datacapture.mapping.ResourceMapper import com.google.android.fhir.db.ResourceNotFoundException import com.google.android.fhir.get import com.google.android.fhir.logicalId +import com.google.android.fhir.search.Search import com.google.android.fhir.workflow.FhirOperator import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest @@ -83,6 +85,7 @@ import org.smartregister.fhircore.engine.configuration.app.ConfigService import org.smartregister.fhircore.engine.data.local.DefaultRepository import org.smartregister.fhircore.engine.domain.model.ActionParameter import org.smartregister.fhircore.engine.domain.model.ActionParameterType +import org.smartregister.fhircore.engine.domain.model.QuestionnaireType import org.smartregister.fhircore.engine.domain.model.RuleConfig import org.smartregister.fhircore.engine.rulesengine.ConfigRulesExecutor import org.smartregister.fhircore.engine.rulesengine.ResourceDataRulesExecutor @@ -1264,37 +1267,136 @@ class QuestionnaireViewModelTest : RobolectricTest() { } @Test - fun testThatPopulateQuestionnaireSetInitialDefaultValueForQuestionnaire() = runTest { - val questionnaireWithDefaultDate = + fun testThatPopulateQuestionnaireSetInitialDefaultValueForQuestionnaireInitialExpression() = + runTest { + val questionnaireWithDefaultDate = + Questionnaire().apply { + id = questionnaireConfig.id + addItem( + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "defaultedDate" + type = Questionnaire.QuestionnaireItemType.DATE + addExtension( + Extension( + "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression", + Expression().apply { + language = "text/fhirpath" + expression = "today()" + }, + ), + ) + }, + ) + } + coEvery { fhirEngine.get(ResourceType.Questionnaire, questionnaireConfig.id) } returns + questionnaireWithDefaultDate + + questionnaireViewModel.populateQuestionnaire( + questionnaireWithDefaultDate, + questionnaireConfig, + emptyList(), + ) + val initialValueDate = + questionnaireWithDefaultDate.item + .first { it.linkId == "defaultedDate" } + .initial + .first() + .value as DateType + Assert.assertTrue(initialValueDate.isToday) + } + + @Test + fun testThatPopulateQuestionnaireReturnsQuestionnaireResponseWithUnAnsweredRemoved() = runTest { + val questionnaireConfig1 = + questionnaireConfig.copy( + resourceType = ResourceType.Patient, + resourceIdentifier = patient.logicalId, + type = QuestionnaireType.EDIT.name, + ) + + val questionnaireWithInitialValue = Questionnaire().apply { - id = questionnaireConfig.id + id = questionnaireConfig1.id addItem( Questionnaire.QuestionnaireItemComponent().apply { - linkId = "defaultedDate" - type = Questionnaire.QuestionnaireItemType.DATE - addExtension( - Extension( - "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression", - Expression().apply { - language = "text/fhirpath" - expression = "today()" - }, - ), + linkId = "group-1" + type = Questionnaire.QuestionnaireItemType.GROUP + addItem( + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "linkid-1" + type = Questionnaire.QuestionnaireItemType.STRING + addInitial(Questionnaire.QuestionnaireItemInitialComponent(StringType("---"))) + }, + ) + + addItem( + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "linkid-1.1" + type = Questionnaire.QuestionnaireItemType.STRING + addInitial(Questionnaire.QuestionnaireItemInitialComponent(StringType("---"))) + }, ) }, ) + + addItem( + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "linkid-2" + type = Questionnaire.QuestionnaireItemType.STRING + }, + ) } - coEvery { fhirEngine.get(ResourceType.Questionnaire, questionnaireConfig.id) } returns - questionnaireWithDefaultDate + val questionnaireResponse = + QuestionnaireResponse().apply { + addItem( + QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { + linkId = "group-1" + addItem( + QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { + linkId = "linkid-1" + }, + ) + addItem( + QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { + linkId = "linkid-1.1" + addAnswer( + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { + value = StringType("World") + }, + ) + }, + ) + }, + ) - questionnaireViewModel.populateQuestionnaire( - questionnaireWithDefaultDate, - questionnaireConfig, - emptyList(), - ) - val initialValueDate = - questionnaireWithDefaultDate.item.first { it.linkId == "defaultedDate" }.initial.first().value - as DateType - Assert.assertTrue(initialValueDate.isToday) + addItem( + QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { + linkId = "linkid-2" + addAnswer( + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { + value = StringType("45678") + }, + ) + }, + ) + } + coEvery { + fhirEngine.get(questionnaireConfig1.resourceType!!, questionnaireConfig1.resourceIdentifier!!) + } returns patient + + coEvery { fhirEngine.search(any()) } returns + listOf( + SearchResult(questionnaireResponse, included = null, revIncluded = null), + ) + + Assert.assertNotNull(questionnaireResponse.find("linkid-1")) + val result = + questionnaireViewModel.populateQuestionnaire( + questionnaireWithInitialValue, + questionnaireConfig1, + emptyList(), + ) + Assert.assertNotNull(result.first) + Assert.assertTrue(result.first!!.find("linkid-1") == null) } }