Skip to content

Commit

Permalink
Prepopulate config computed values after sdk initial population (#3364)
Browse files Browse the repository at this point in the history
* 🐛 Fix NPE from empty valuesMap for filter criteria

* 🐛 Remove incomplete code committed erroneously

* Prepopulate questionnaire computed values after sdk population

* Refactor method prepopulateWithComputedConfigValues

* Unset prepopulate with computed value as extension function

for codcov

* 🐛 Initialize unique ID resource

This is to fix NPE in later updating of an ID as used

* Update jacoco task

* Move prepopulateWithComputedConfigValues to QuestionnaireExtension

* Revert jacoco related changes

* Fix missing argument error in QuestionnaireViewModelTest

---------

Co-authored-by: Allan O <[email protected]>
Co-authored-by: Allan Onchuru <[email protected]>
Co-authored-by: Elly Kitoto <[email protected]>
  • Loading branch information
4 people authored Aug 22, 2024
1 parent 115c4f4 commit b20c669
Show file tree
Hide file tree
Showing 7 changed files with 261 additions and 104 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1104,12 +1104,13 @@ constructor(

suspend fun retrieveUniqueIdAssignmentResource(
uniqueIdAssignmentConfig: UniqueIdAssignmentConfig?,
computedValuesMap: Map<String, Any>,
): Resource? {
if (uniqueIdAssignmentConfig != null) {
val search =
Search(uniqueIdAssignmentConfig.resource).apply {
uniqueIdAssignmentConfig.dataQueries.forEach {
filterBy(dataQuery = it, configComputedRuleValues = emptyMap())
filterBy(dataQuery = it, configComputedRuleValues = computedValuesMap)
}
if (uniqueIdAssignmentConfig.sortConfigs != null) {
sort(uniqueIdAssignmentConfig.sortConfigs)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,14 @@ 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
import org.smartregister.fhircore.engine.configuration.LinkIdType
import org.smartregister.fhircore.engine.configuration.QuestionnaireConfig
import org.smartregister.fhircore.engine.configuration.UniqueIdAssignmentConfig
import org.smartregister.fhircore.engine.domain.model.ActionParameter
import org.smartregister.fhircore.engine.domain.model.ActionParameterType
import org.smartregister.fhircore.engine.domain.model.RuleConfig
import org.smartregister.fhircore.engine.domain.model.isEditable
import org.smartregister.fhircore.engine.domain.model.isReadOnly

fun QuestionnaireResponse.QuestionnaireResponseItemComponent.asLabel() =
if (this.linkId != null) {
Expand Down Expand Up @@ -194,6 +201,105 @@ fun List<Questionnaire.QuestionnaireItemComponent>.prePopulateInitialValues(
}
}

/**
* Pre-populates questionnaire with computed values from the Rules engine as well as include initial
* values set on configured [QuestionnaireConfig.barcodeLinkId] or
* [QuestionnaireConfig.uniqueIdAssignment] properties.
*/
suspend fun Questionnaire.prepopulateWithComputedConfigValues(
questionnaireConfig: QuestionnaireConfig,
actionParameters: List<ActionParameter>?,
questionnaireConfigRulesComputeFunc: (List<RuleConfig>) -> Map<String, Any>,
extractUniqueIdAssignmentFunc: suspend (UniqueIdAssignmentConfig, Map<String, Any>) -> String,
): Map<String, Any> {
require(questionnaireConfig.id.isNotBlank())

// Compute questionnaire config rules and add extra questionnaire params to action parameters

val questionnaireComputedValues =
questionnaireConfig.configRules?.let { questionnaireConfigRulesComputeFunc.invoke(it) }
?: emptyMap()

val allActionParameters =
actionParameters?.plus(
questionnaireConfig.extraParams?.map { it.interpolate(questionnaireComputedValues) }
?: emptyList(),
)

if (questionnaireConfig.isReadOnly() || questionnaireConfig.isEditable()) {
item.prepareQuestionsForReadingOrEditing(
readOnly = questionnaireConfig.isReadOnly(),
readOnlyLinkIds =
questionnaireConfig.readOnlyLinkIds
?: questionnaireConfig.linkIds
?.filter { it.type == LinkIdType.READ_ONLY }
?.map { it.linkId },
)
}

if (questionnaireConfig.isEditable()) {
item.prepareQuestionsForEditing(
readOnlyLinkIds = questionnaireConfig.readOnlyLinkIds,
)
}

// Pre-populate questionnaire items with configured values
allActionParameters
?.filter { (it.paramType == ActionParameterType.PREPOPULATE && it.value.isNotEmpty()) }
?.let { actionParam -> item.prePopulateInitialValues(DEFAULT_PLACEHOLDER_PREFIX, actionParam) }

// Set barcode to the configured linkId default: "patient-barcode"
if (!questionnaireConfig.resourceIdentifier.isNullOrEmpty()) {
(questionnaireConfig.barcodeLinkId
?: questionnaireConfig.linkIds?.firstOrNull { it.type == LinkIdType.BARCODE }?.linkId)
?.let { barcodeLinkId ->
find(barcodeLinkId)?.apply {
initial =
mutableListOf(
Questionnaire.QuestionnaireItemInitialComponent()
.setValue(StringType(questionnaireConfig.resourceIdentifier)),
) // TODO should this be resource identifier or OpenSrp unique ID?
readOnly = true
}
}
}

prepopulateUniqueIdAssignment(
questionnaireConfig,
questionnaireComputedValues,
extractUniqueIdAssignmentFunc,
)

return questionnaireComputedValues
}

/** Pre-populates questionnaire with computed [QuestionnaireConfig.uniqueIdAssignment]. */
suspend fun Questionnaire.prepopulateUniqueIdAssignment(
questionnaireConfig: QuestionnaireConfig,
questionnaireComputedValues: Map<String, Any>,
extractUniqueIdAssignmentFunc: suspend (UniqueIdAssignmentConfig, Map<String, Any>) -> String,
) {
// Set configured OpenSRPId on Questionnaire
questionnaireConfig.uniqueIdAssignment?.let { uniqueIdAssignmentConfig ->
find(uniqueIdAssignmentConfig.linkId)?.apply {
if (initial.isNotEmpty() && !initial.first().isEmpty) return@apply

val extractedId =
extractUniqueIdAssignmentFunc(
questionnaireConfig.uniqueIdAssignment,
questionnaireComputedValues,
)
if (extractedId.isNotEmpty()) {
initial =
mutableListOf(
Questionnaire.QuestionnaireItemInitialComponent().setValue(StringType(extractedId)),
)
}
readOnly = extractedId.isNotEmpty() && uniqueIdAssignmentConfig.readOnly
}
}
}

/** Cast string value (including json string) to the FHIR {@link org.hl7.fhir.r4.model.Type} */
fun String.castToType(type: DataType): Type? {
return when (type) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1480,7 +1480,8 @@ class DefaultRepositoryTest : RobolectricTest() {
)

fhirEngine.create(group1, group2)
val resource = defaultRepository.retrieveUniqueIdAssignmentResource(uniqueIdAssignmentConfig)
val resource =
defaultRepository.retrieveUniqueIdAssignmentResource(uniqueIdAssignmentConfig, emptyMap())
Assert.assertNotNull(resource)
Assert.assertTrue(resource is Group)
Assert.assertEquals("1234", (resource as Group).characteristic[0].valueCodeableConcept.text)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,35 @@

package org.smartregister.fhircore.engine.util.extension

import java.util.UUID
import kotlinx.coroutines.test.runTest
import org.hl7.fhir.r4.model.BooleanType
import org.hl7.fhir.r4.model.Bundle
import org.hl7.fhir.r4.model.Coding
import org.hl7.fhir.r4.model.DateTimeType
import org.hl7.fhir.r4.model.DateType
import org.hl7.fhir.r4.model.DecimalType
import org.hl7.fhir.r4.model.Enumerations
import org.hl7.fhir.r4.model.Enumerations.DataType
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
import org.hl7.fhir.r4.model.QuestionnaireResponse
import org.hl7.fhir.r4.model.ResourceType
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
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.smartregister.fhircore.engine.app.fakes.Faker
import org.smartregister.fhircore.engine.configuration.QuestionnaireConfig
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.robolectric.RobolectricTest

class QuestionnaireExtensionTest : RobolectricTest() {
Expand Down Expand Up @@ -443,4 +451,74 @@ class QuestionnaireExtensionTest : RobolectricTest() {

// TODO: test valid JSON
}

@Test
fun testPrepopulateQuestionnaireWithComputedValues() = runTest {
val questionnaireConfig =
QuestionnaireConfig(
id = UUID.randomUUID().toString(),
resourceIdentifier = "patient.id",
resourceType = ResourceType.Patient,
barcodeLinkId = "patient-barcode",
type = QuestionnaireType.READ_ONLY.name,
configRules =
listOf(
RuleConfig(
name = "rule1",
actions = listOf("data.put('rule1', 'Sample Rule')"),
),
),
extraParams =
listOf(
ActionParameter(
key = "rule1",
value = "@{rule1}",
paramType = ActionParameterType.PARAMDATA,
),
),
)
val patientAgeLinkId = "patient-age"
val actionParameter =
listOf(
ActionParameter(
paramType = ActionParameterType.PREPOPULATE,
linkId = patientAgeLinkId,
dataType = Enumerations.DataType.INTEGER,
key = patientAgeLinkId,
value = "20",
),
)
val questionnaire =
Questionnaire().apply {
addItem(
Questionnaire.QuestionnaireItemComponent().apply {
linkId = patientAgeLinkId
type = Questionnaire.QuestionnaireItemType.INTEGER
readOnly = true
},
)
}

questionnaire.prepopulateWithComputedConfigValues(
questionnaireConfig,
actionParameter,
{ mapOf(patientAgeLinkId to "20") },
{ _, _ -> "" },
)

// Questionnaire.item pre-populated
val questionnairePatientAgeItem = questionnaire.find(patientAgeLinkId)
val itemValue: Type? = questionnairePatientAgeItem?.initial?.firstOrNull()?.value
Assert.assertTrue(itemValue is IntegerType)
Assert.assertEquals(20, itemValue?.primitiveValue()?.toInt())

// Barcode linkId updated
val questionnaireBarcodeItem = questionnaireConfig.barcodeLinkId?.let { questionnaire.find(it) }
val barCodeItemValue: Type? = questionnaireBarcodeItem?.initial?.firstOrNull()?.value
Assert.assertFalse(barCodeItemValue is StringType)
Assert.assertNull(
questionnaireConfig.resourceIdentifier,
barCodeItemValue?.primitiveValue(),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() {
}
}

questionnaire = viewModel.retrieveQuestionnaire(questionnaireConfig, actionParameters)
questionnaire = viewModel.retrieveQuestionnaire(questionnaireConfig)

try {
val questionnaireFragmentBuilder =
Expand Down
Loading

0 comments on commit b20c669

Please sign in to comment.