Skip to content

Commit

Permalink
Fix questionnaire prepopulation using initialExpression (#3240)
Browse files Browse the repository at this point in the history
  • Loading branch information
LZRS authored May 8, 2024
1 parent 99f974e commit 5c2da08
Show file tree
Hide file tree
Showing 3 changed files with 131 additions and 69 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,28 +33,22 @@ import androidx.core.os.bundleOf
import androidx.fragment.app.commit
import androidx.lifecycle.lifecycleScope
import com.google.android.fhir.datacapture.QuestionnaireFragment
import com.google.android.fhir.logicalId
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationServices
import dagger.hilt.android.AndroidEntryPoint
import java.io.Serializable
import java.util.LinkedList
import javax.inject.Inject
import kotlinx.coroutines.launch
import org.hl7.fhir.r4.model.Questionnaire
import org.hl7.fhir.r4.model.QuestionnaireResponse
import org.hl7.fhir.r4.model.Resource
import org.hl7.fhir.r4.model.ResourceType
import org.smartregister.fhircore.engine.configuration.QuestionnaireConfig
import org.smartregister.fhircore.engine.configuration.app.LocationLogOptions
import org.smartregister.fhircore.engine.domain.model.ActionParameter
import org.smartregister.fhircore.engine.domain.model.ActionParameterType
import org.smartregister.fhircore.engine.domain.model.isEditable
import org.smartregister.fhircore.engine.domain.model.isReadOnly
import org.smartregister.fhircore.engine.ui.base.AlertDialogue
import org.smartregister.fhircore.engine.ui.base.BaseMultiLanguageActivity
import org.smartregister.fhircore.engine.util.DispatcherProvider
import org.smartregister.fhircore.engine.util.extension.clearText
import org.smartregister.fhircore.engine.util.extension.encodeResourceToString
import org.smartregister.fhircore.engine.util.extension.parcelable
import org.smartregister.fhircore.engine.util.extension.parcelableArrayList
Expand Down Expand Up @@ -272,75 +266,37 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() {
questionnaire: Questionnaire,
): QuestionnaireFragment.Builder {
if (questionnaire.subjectType.isNullOrEmpty()) {
showToast(getString(R.string.missing_subject_type))
Timber.e(
"Missing subject type on questionnaire. Provide Questionnaire.subjectType to resolve.",
)
val subjectRequiredMessage = getString(R.string.missing_subject_type)
showToast(subjectRequiredMessage)
Timber.e(subjectRequiredMessage)
finish()
}
val questionnaireFragmentBuilder =
QuestionnaireFragment.builder()
.setQuestionnaire(questionnaire.json())
.setCustomQuestionnaireItemViewHolderFactoryMatchersProvider(
OPENSRP_ITEM_VIEWHOLDER_FACTORY_MATCHERS_PROVIDER,
)
.showAsterisk(questionnaireConfig.showRequiredTextAsterisk)
.showRequiredText(questionnaireConfig.showRequiredText)

val questionnaireSubjectType = questionnaire.subjectType.firstOrNull()?.code
val resourceType =
questionnaireConfig.resourceType ?: questionnaireSubjectType?.let { ResourceType.valueOf(it) }
val resourceIdentifier = questionnaireConfig.resourceIdentifier

if (resourceType != null && !resourceIdentifier.isNullOrEmpty()) {
// Add subject and other configured resource to launchContext
val launchContextResources =
LinkedList<Resource>().apply {
viewModel.loadResource(resourceType, resourceIdentifier)?.let { add(it) }
addAll(
// Exclude the subject resource its already added
viewModel.retrievePopulationResources(
actionParameters.filterNot {
it.paramType == ActionParameterType.QUESTIONNAIRE_RESPONSE_POPULATION_RESOURCE &&
resourceType == it.resourceType &&
resourceIdentifier.equals(it.value, ignoreCase = true)
},
),
)
}

if (launchContextResources.isNotEmpty()) {
questionnaireFragmentBuilder.setQuestionnaireLaunchContextMap(
launchContextResources.associate {
Pair(it.resourceType.name.lowercase(), it.encodeResourceToString())
},
)
}

// Populate questionnaire with latest QuestionnaireResponse
if (questionnaireConfig.isEditable()) {
val latestQuestionnaireResponse =
viewModel.searchLatestQuestionnaireResponse(
resourceId = resourceIdentifier,
resourceType = resourceType,
questionnaireId = questionnaire.logicalId,
)

val questionnaireResponse =
QuestionnaireResponse().apply {
item = latestQuestionnaireResponse?.item
// Clearing the text prompts the SDK to re-process the content, which includes HTML
clearText()
}
val (questionnaireResponse, launchContextResources) =
viewModel.populateQuestionnaire(questionnaire, questionnaireConfig, actionParameters)

if (viewModel.validateQuestionnaireResponse(questionnaire, questionnaireResponse, this)) {
questionnaireFragmentBuilder.setQuestionnaireResponse(questionnaireResponse.json())
} else {
showToast(getString(R.string.error_populating_questionnaire))
return QuestionnaireFragment.builder()
.setQuestionnaire(questionnaire.json())
.setCustomQuestionnaireItemViewHolderFactoryMatchersProvider(
OPENSRP_ITEM_VIEWHOLDER_FACTORY_MATCHERS_PROVIDER,
)
.showAsterisk(questionnaireConfig.showRequiredTextAsterisk)
.showRequiredText(questionnaireConfig.showRequiredText)
.apply {
if (questionnaireResponse != null) {
questionnaireResponse
.takeIf {
viewModel.validateQuestionnaireResponse(questionnaire, it, this@QuestionnaireActivity)
}
?.let { setQuestionnaireResponse(it.json()) }
?: showToast(getString(R.string.error_populating_questionnaire))
}

launchContextResources
.associate { Pair(it.resourceType.name.lowercase(), it.encodeResourceToString()) }
.takeIf { it.isNotEmpty() }
?.let { setQuestionnaireLaunchContextMap(it) }
}
}
return questionnaireFragmentBuilder
}

private fun Resource.json(): String = this.encodeResourceToString()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import com.google.android.fhir.search.search
import com.google.android.fhir.workflow.FhirOperator
import dagger.hilt.android.lifecycle.HiltViewModel
import java.util.Date
import java.util.LinkedList
import java.util.UUID
import javax.inject.Inject
import kotlinx.coroutines.SupervisorJob
Expand Down Expand Up @@ -79,7 +80,9 @@ import org.smartregister.fhircore.engine.util.extension.appendOrganizationInfo
import org.smartregister.fhircore.engine.util.extension.appendPractitionerInfo
import org.smartregister.fhircore.engine.util.extension.appendRelatedEntityLocation
import org.smartregister.fhircore.engine.util.extension.asReference
import org.smartregister.fhircore.engine.util.extension.clearText
import org.smartregister.fhircore.engine.util.extension.cqfLibraryUrls
import org.smartregister.fhircore.engine.util.extension.encodeResourceToString
import org.smartregister.fhircore.engine.util.extension.extractByStructureMap
import org.smartregister.fhircore.engine.util.extension.extractId
import org.smartregister.fhircore.engine.util.extension.extractLogicalIdUuid
Expand Down Expand Up @@ -917,6 +920,72 @@ constructor(
return questionnaireResponses.maxByOrNull { it.meta.lastUpdated }
}

suspend fun launchContextResources(
subjectResourceType: ResourceType?,
subjectResourceIdentifier: String?,
actionParameters: List<ActionParameter>,
): List<Resource> {
return when {
subjectResourceType != null && subjectResourceIdentifier != null ->
LinkedList<Resource>().apply {
loadResource(subjectResourceType, subjectResourceIdentifier)?.let { add(it) }
val actionParametersExcludingSubject =
actionParameters.filterNot {
it.paramType == ActionParameterType.QUESTIONNAIRE_RESPONSE_POPULATION_RESOURCE &&
subjectResourceType == it.resourceType &&
subjectResourceIdentifier.equals(it.value, ignoreCase = true)
}
addAll(retrievePopulationResources(actionParametersExcludingSubject))
}
else -> LinkedList(retrievePopulationResources(actionParameters))
}
}

suspend fun populateQuestionnaire(
questionnaire: Questionnaire,
questionnaireConfig: QuestionnaireConfig,
actionParameters: List<ActionParameter>,
): Pair<QuestionnaireResponse?, List<Resource>> {
val questionnaireSubjectType = questionnaire.subjectType.firstOrNull()?.code
val resourceType =
questionnaireConfig.resourceType ?: questionnaireSubjectType?.let { ResourceType.valueOf(it) }
val resourceIdentifier = questionnaireConfig.resourceIdentifier

val launchContextResources =
launchContextResources(resourceType, resourceIdentifier, actionParameters)

// Populate questionnaire with initial default values
ResourceMapper.populate(
questionnaire,
launchContexts = launchContextResources.associateBy { it.resourceType.name.lowercase() },
)

// Populate questionnaire with latest QuestionnaireResponse
val questionnaireResponse =
if (
resourceType != null &&
!resourceIdentifier.isNullOrEmpty() &&
questionnaireConfig.isEditable()
) {
searchLatestQuestionnaireResponse(
resourceId = resourceIdentifier,
resourceType = resourceType,
questionnaireId = questionnaire.logicalId,
)
?.let {
QuestionnaireResponse().apply {
item = it.item
// Clearing the text prompts the SDK to re-process the content, which includes HTML
clearText()
}
}
} else {
null
}

return Pair(questionnaireResponse, launchContextResources)
}

/**
* 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,10 @@ import org.hl7.fhir.r4.model.Basic
import org.hl7.fhir.r4.model.Bundle
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.Encounter
import org.hl7.fhir.r4.model.Enumerations
import org.hl7.fhir.r4.model.Expression
import org.hl7.fhir.r4.model.Extension
import org.hl7.fhir.r4.model.Flag
import org.hl7.fhir.r4.model.Group
Expand Down Expand Up @@ -1260,4 +1262,39 @@ class QuestionnaireViewModelTest : RobolectricTest() {
// Assert that the listResource id matches the linkId
assertEquals(linkId, listResource.id)
}

@Test
fun testThatPopulateQuestionnaireSetInitialDefaultValueForQuestionnaire() = 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)
}
}

0 comments on commit 5c2da08

Please sign in to comment.