Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prevent possible duplication avenues on extract/save questionnaire #58

Merged
merged 2 commits into from
Jun 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion android/engine/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
<activity
android:name=".ui.questionnaire.QuestionnaireActivity"
android:exported="false"
android:theme="@style/AppTheme" />
android:theme="@style/AppTheme"
android:launchMode="singleTop"/>
<activity
android:name=".HiltActivityForTest"
android:exported="false"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,16 @@

package org.smartregister.fhircore.engine.ui.questionnaire

import java.lang.Exception
import org.hl7.fhir.r4.model.QuestionnaireResponse
import org.hl7.fhir.r4.model.Resource

sealed class ExtractionProgress {
class Success(val extras: List<Resource>? = null) : ExtractionProgress()
class Success(
val questionnaireResponse: QuestionnaireResponse,
val extras: List<Resource>? = null,
) : ExtractionProgress()

object Failed : ExtractionProgress()
class Failed(val questionnaireResponse: QuestionnaireResponse, val exception: Exception) :
ExtractionProgress()
}
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,14 @@ open class QuestionnaireActivity : BaseMultiLanguageActivity(), View.OnClickList
fragment.whenResumed { loadProgress.dismiss() }
}
}

questionnaireViewModel.extractionProgress.observe(this) { result ->
if (result is ExtractionProgress.Success) {
onPostSave(true, result.questionnaireResponse, result.extras)
} else {
onPostSave(false, (result as ExtractionProgress.Failed).questionnaireResponse)
}
}
}

fun updateViews() {
Expand Down Expand Up @@ -180,7 +188,7 @@ open class QuestionnaireActivity : BaseMultiLanguageActivity(), View.OnClickList
.setShowSubmitButton(true)
.setCustomQuestionnaireItemViewHolderFactoryMatchersProvider(DEFAULT_PROVIDER)
.setIsReadOnly(questionnaireType.isReadOnly())
questionnaireResponse?.let {
questionnaireResponse.let {
it.distinctifyLinkId()
// Timber.e(it.encodeResourceToString())
questionnaireFragmentBuilder.setQuestionnaireResponse(it.encodeResourceToString())
Expand Down Expand Up @@ -310,23 +318,22 @@ open class QuestionnaireActivity : BaseMultiLanguageActivity(), View.OnClickList
}

open fun showFormSubmissionConfirmAlert() {
if (questionnaire.experimental) {
showConfirmAlert(
context = this,
message = R.string.questionnaire_alert_test_only_message,
title = R.string.questionnaire_alert_test_only_title,
confirmButtonListener = { handleQuestionnaireSubmit() },
confirmButtonText = R.string.questionnaire_alert_test_only_button_title,
)
} else {
showConfirmAlert(
context = this,
message = R.string.questionnaire_alert_submit_message,
title = R.string.questionnaire_alert_submit_title,
confirmButtonListener = { handleQuestionnaireSubmit() },
confirmButtonText = R.string.questionnaire_alert_submit_button_title,
)
}
showConfirmAlert(
context = this,
message =
if (questionnaire.experimental) {
R.string.questionnaire_alert_test_only_message
} else R.string.questionnaire_alert_submit_message,
title =
if (questionnaire.experimental) {
R.string.questionnaire_alert_test_only_title
} else R.string.questionnaire_alert_submit_title,
confirmButtonListener = { handleQuestionnaireSubmit() },
confirmButtonText =
if (questionnaire.experimental) {
R.string.questionnaire_alert_test_only_button_title
} else R.string.questionnaire_alert_submit_button_title,
)
}

fun getQuestionnaireResponse(): QuestionnaireResponse {
Expand All @@ -343,28 +350,18 @@ open class QuestionnaireActivity : BaseMultiLanguageActivity(), View.OnClickList

open fun handleQuestionnaireSubmit() {
saveProcessingAlertDialog = showProgressAlert(this, R.string.form_progress_message)

val questionnaireResponse = getQuestionnaireResponse()
if (!validQuestionnaireResponse(questionnaireResponse)) {
saveProcessingAlertDialog.dismiss()

AlertDialogue.showErrorAlert(
this,
R.string.questionnaire_alert_invalid_message,
R.string.questionnaire_alert_invalid_title,
)
return
}

handleQuestionnaireResponse(questionnaireResponse)

questionnaireViewModel.extractionProgress.observe(this) { result ->
if (result is ExtractionProgress.Success) {
onPostSave(true, questionnaireResponse, result.extras)
} else {
onPostSave(false, questionnaireResponse)
getQuestionnaireResponse()
.takeIf { validQuestionnaireResponse(it) }
?.let { handleQuestionnaireResponse(it) }
?: {
saveProcessingAlertDialog.dismiss().also {
AlertDialogue.showErrorAlert(
this,
R.string.questionnaire_alert_invalid_message,
R.string.questionnaire_alert_invalid_title,
)
}
}
}
}

fun onPostSave(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,12 @@ import java.util.Date
import java.util.UUID
import javax.inject.Inject
import javax.inject.Provider
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.hl7.fhir.r4.context.IWorkerContext
Expand Down Expand Up @@ -100,6 +105,7 @@ import org.smartregister.fhircore.engine.util.helper.TransformSupportServices
import timber.log.Timber

@HiltViewModel
@OptIn(FlowPreview::class)
open class QuestionnaireViewModel
@Inject
constructor(
Expand Down Expand Up @@ -137,6 +143,17 @@ constructor(
?.extractLogicalIdUuid()
}

private val extractAndSaveRequestStateFlow: MutableStateFlow<suspend () -> Unit> =
MutableStateFlow {}

init {
viewModelScope.launch(dispatcherProvider.io()) {
extractAndSaveRequestStateFlow.debounce(800.milliseconds).collect {
it.invoke() // invoke request
}
}
}

suspend fun loadQuestionnaire(id: String, type: QuestionnaireType): Questionnaire? =
defaultRepository.loadResource<Questionnaire>(id)?.apply {
if (type.isReadOnly() || type.isEditMode()) {
Expand Down Expand Up @@ -272,102 +289,126 @@ constructor(
questionnaireType: QuestionnaireType = QuestionnaireType.DEFAULT,
questionnaire: Questionnaire,
) {
viewModelScope.launch(dispatcherProvider.io()) {
tracer.startTrace(QUESTIONNAIRE_TRACE)
// important to set response subject so that structure map can handle subject for all entities
handleQuestionnaireResponseSubject(resourceId, questionnaire, questionnaireResponse)
val extras = mutableListOf<Resource>()
if (questionnaire.isExtractionCandidate()) {
val bundle = performExtraction(context, questionnaire, questionnaireResponse)
questionnaireResponse.contained = mutableListOf()
bundle.entry.forEach { bundleEntry ->
// add organization to entities representing individuals in registration questionnaire
if (bundleEntry.resource.resourceType.isIn(ResourceType.Patient, ResourceType.Group)) {
if (questionnaireConfig.setOrganizationDetails) {
appendOrganizationInfo(bundleEntry.resource)
}
// if it is new registration set response subject
if (resourceId == null) {
questionnaireResponse.subject = bundleEntry.resource.asReference()
}
}
if (questionnaireConfig.setPractitionerDetails) {
appendPractitionerInfo(bundleEntry.resource)
}

if (
questionnaireType != QuestionnaireType.EDIT &&
bundleEntry.resource.resourceType.isIn(
ResourceType.Patient,
ResourceType.RelatedPerson,
)
) {
groupResourceId?.let {
appendPatientsAndRelatedPersonsToGroups(
resource = bundleEntry.resource,
groupResourceId = it,
)
}
}
val request = suspend {
try {
val extras =
doExtractAndSaveResources(
context,
resourceId,
groupResourceId,
questionnaireResponse,
questionnaireType,
questionnaire,
)
extractionProgress.postValue(ExtractionProgress.Success(questionnaireResponse, extras))
} catch (e: Exception) {
extractionProgress.postValue(ExtractionProgress.Failed(questionnaireResponse, e))
}
}

// response MUST have subject by far otherwise flow has issues
if (!questionnaire.experimental) questionnaireResponse.assertSubject()
extractAndSaveRequestStateFlow.value = request
}

// TODO https://github.com/opensrp/fhircore/issues/900
// for edit mode replace client and resource subject ids.
// Ideally ResourceMapper should allow this internally via structure-map
if (questionnaireType.isEditMode()) {
if (bundleEntry.resource.resourceType.isIn(ResourceType.Patient, ResourceType.Group)) {
bundleEntry.resource.id = questionnaireResponse.subject.extractId()
} else {
bundleEntry.resource.setPropertySafely("subject", questionnaireResponse.subject)
bundleEntry.resource.setPropertySafely("patient", questionnaireResponse.subject)
}
private suspend fun doExtractAndSaveResources(
context: Context,
resourceId: String?,
groupResourceId: String? = null,
questionnaireResponse: QuestionnaireResponse,
questionnaireType: QuestionnaireType = QuestionnaireType.DEFAULT,
questionnaire: Questionnaire,
): List<Resource> {
tracer.startTrace(QUESTIONNAIRE_TRACE)
// important to set response subject so that structure map can handle subject for all entities
handleQuestionnaireResponseSubject(resourceId, questionnaire, questionnaireResponse)
val extras = mutableListOf<Resource>()
if (questionnaire.isExtractionCandidate()) {
val bundle = performExtraction(context, questionnaire, questionnaireResponse)
questionnaireResponse.contained = mutableListOf()
bundle.entry.forEach { bundleEntry ->
// add organization to entities representing individuals in registration questionnaire
if (bundleEntry.resource.resourceType.isIn(ResourceType.Patient, ResourceType.Group)) {
if (questionnaireConfig.setOrganizationDetails) {
appendOrganizationInfo(bundleEntry.resource)
}
questionnaireResponse.contained.add(bundleEntry.resource)

if (bundleEntry.resource is Encounter) extras.add(bundleEntry.resource)

if (
(bundleEntry.resource is CarePlan || bundleEntry.resource is Patient) &&
bundleEntry.resource.meta.tag.isNotEmpty()
) {
carePlanAndPatientMetaExtraction(bundleEntry.resource)
// if it is new registration set response subject
if (resourceId == null) {
questionnaireResponse.subject = bundleEntry.resource.asReference()
}
}

if (questionnaire.experimental) {
Timber.w(
"${questionnaire.name}(${questionnaire.logicalId}) is experimental and not save any data",
)
} else {
saveBundleResources(bundle)
if (questionnaireConfig.setPractitionerDetails) {
appendPractitionerInfo(bundleEntry.resource)
}

if (questionnaireType.isEditMode() && editQuestionnaireResponse != null) {
questionnaireResponse.retainMetadata(editQuestionnaireResponse!!)
if (
questionnaireType != QuestionnaireType.EDIT &&
bundleEntry.resource.resourceType.isIn(
ResourceType.Patient,
ResourceType.RelatedPerson,
)
) {
groupResourceId?.let {
appendPatientsAndRelatedPersonsToGroups(
resource = bundleEntry.resource,
groupResourceId = it,
)
}
}

saveQuestionnaireResponse(questionnaire, questionnaireResponse)
questionnaireResponseLiveData.postValue(questionnaireResponse)
// response MUST have subject by far otherwise flow has issues
if (!questionnaire.experimental) questionnaireResponse.assertSubject()

// TODO https://github.com/opensrp/fhircore/issues/900
// reassess following i.e. deleting/updating older resources because one resource
// might have generated other flow in subsequent followups
if (questionnaireType.isEditMode() && editQuestionnaireResponse != null) {
editQuestionnaireResponse!!.deleteRelatedResources(defaultRepository)
// for edit mode replace client and resource subject ids.
// Ideally ResourceMapper should allow this internally via structure-map
if (questionnaireType.isEditMode()) {
if (bundleEntry.resource.resourceType.isIn(ResourceType.Patient, ResourceType.Group)) {
bundleEntry.resource.id = questionnaireResponse.subject.extractId()
} else {
bundleEntry.resource.setPropertySafely("subject", questionnaireResponse.subject)
bundleEntry.resource.setPropertySafely("patient", questionnaireResponse.subject)
}
}
questionnaireResponse.contained.add(bundleEntry.resource)

if (bundleEntry.resource is Encounter) extras.add(bundleEntry.resource)

if (
(bundleEntry.resource is CarePlan || bundleEntry.resource is Patient) &&
bundleEntry.resource.meta.tag.isNotEmpty()
) {
carePlanAndPatientMetaExtraction(bundleEntry.resource)
}
}

extractCqlOutput(questionnaire, questionnaireResponse, bundle)
extractCarePlan(questionnaireResponse, bundle)
if (questionnaire.experimental) {
Timber.w(
"${questionnaire.name}(${questionnaire.logicalId}) is experimental and not save any data",
)
} else {
saveQuestionnaireResponse(questionnaire, questionnaireResponse)
extractCqlOutput(questionnaire, questionnaireResponse, null)
saveBundleResources(bundle)
}

if (questionnaireType.isEditMode() && editQuestionnaireResponse != null) {
questionnaireResponse.retainMetadata(editQuestionnaireResponse!!)
}
tracer.stopTrace(QUESTIONNAIRE_TRACE)
viewModelScope.launch(Dispatchers.Main) {
extractionProgress.postValue(ExtractionProgress.Success(extras))

saveQuestionnaireResponse(questionnaire, questionnaireResponse)
questionnaireResponseLiveData.postValue(questionnaireResponse)
// TODO https://github.com/opensrp/fhircore/issues/900
// reassess following i.e. deleting/updating older resources because one resource
// might have generated other flow in subsequent followups
if (questionnaireType.isEditMode() && editQuestionnaireResponse != null) {
editQuestionnaireResponse!!.deleteRelatedResources(defaultRepository)
}

extractCqlOutput(questionnaire, questionnaireResponse, bundle)
extractCarePlan(questionnaireResponse, bundle)
} else {
saveQuestionnaireResponse(questionnaire, questionnaireResponse)
extractCqlOutput(questionnaire, questionnaireResponse, null)
}
tracer.stopTrace(QUESTIONNAIRE_TRACE)
return extras
}

suspend fun carePlanAndPatientMetaExtraction(source: Resource) {
Expand Down
Loading