Skip to content

Commit

Permalink
Exclude questionnaire items from population (#3460)
Browse files Browse the repository at this point in the history
* Exclude configured fields from form prepopulation

Signed-off-by: Elly Kitoto <junkmailstoelly@gmail.com>

* Document exclusion of Questionnaire fields via config

Signed-off-by: Elly Kitoto <junkmailstoelly@gmail.com>

* Handle exclusion of nested QuestionnaireResponse items

Signed-off-by: Elly Kitoto <junkmailstoelly@gmail.com>

---------

Signed-off-by: Elly Kitoto <junkmailstoelly@gmail.com>
Co-authored-by: Allan Onchuru <16164649+allan-on@users.noreply.github.com>
  • Loading branch information
ellykits and allan-on committed Sep 19, 2024
1 parent 97a75a0 commit 8179c81
Show file tree
Hide file tree
Showing 4 changed files with 180 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -169,5 +169,5 @@ enum class LinkIdType : Parcelable {
READ_ONLY,
BARCODE,
LOCATION,
IDENTIFIER,
PREPOPULATION_EXCLUSION,
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1118,14 +1119,49 @@ constructor(
null
}

// Exclude the configured fields from QR
if (questionnaireResponse != null) {
val exclusionLinkIdsMap: Map<String, Boolean> =
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<QuestionnaireResponseItemComponent>,
exclusionMap: Map<String, Boolean>,
): MutableList<QuestionnaireResponseItemComponent> {
val stack = LinkedList<MutableList<QuestionnaireResponseItemComponent>>()
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<QuestionnaireResponseItemComponent>.removeUnAnsweredItems():
List<QuestionnaireResponseItemComponent> {
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()
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<QuestionnaireResponse>(any<Search>()) } 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 =
Expand Down Expand Up @@ -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)
}
}
16 changes: 16 additions & 0 deletions docs/engineering/app/configuring/forms/forms.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
```

0 comments on commit 8179c81

Please sign in to comment.