Skip to content

Commit 1bf012d

Browse files
committed
Set questionnaire saving/extraction as request
that can be debounced
1 parent 2ede518 commit 1bf012d

File tree

3 files changed

+165
-121
lines changed

3 files changed

+165
-121
lines changed

android/engine/src/main/java/org/smartregister/fhircore/engine/ui/questionnaire/ExtractionProgress.kt

+8-2
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,16 @@
1616

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

19+
import java.lang.Exception
20+
import org.hl7.fhir.r4.model.QuestionnaireResponse
1921
import org.hl7.fhir.r4.model.Resource
2022

2123
sealed class ExtractionProgress {
22-
class Success(val extras: List<Resource>? = null) : ExtractionProgress()
24+
class Success(
25+
val questionnaireResponse: QuestionnaireResponse,
26+
val extras: List<Resource>? = null,
27+
) : ExtractionProgress()
2328

24-
object Failed : ExtractionProgress()
29+
class Failed(val questionnaireResponse: QuestionnaireResponse, val exception: Exception) :
30+
ExtractionProgress()
2531
}

android/engine/src/main/java/org/smartregister/fhircore/engine/ui/questionnaire/QuestionnaireActivity.kt

+36-39
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,14 @@ open class QuestionnaireActivity : BaseMultiLanguageActivity(), View.OnClickList
137137
fragment.whenResumed { loadProgress.dismiss() }
138138
}
139139
}
140+
141+
questionnaireViewModel.extractionProgress.observe(this) { result ->
142+
if (result is ExtractionProgress.Success) {
143+
onPostSave(true, result.questionnaireResponse, result.extras)
144+
} else {
145+
onPostSave(false, (result as ExtractionProgress.Failed).questionnaireResponse)
146+
}
147+
}
140148
}
141149

142150
fun updateViews() {
@@ -180,7 +188,7 @@ open class QuestionnaireActivity : BaseMultiLanguageActivity(), View.OnClickList
180188
.setShowSubmitButton(true)
181189
.setCustomQuestionnaireItemViewHolderFactoryMatchersProvider(DEFAULT_PROVIDER)
182190
.setIsReadOnly(questionnaireType.isReadOnly())
183-
questionnaireResponse?.let {
191+
questionnaireResponse.let {
184192
it.distinctifyLinkId()
185193
// Timber.e(it.encodeResourceToString())
186194
questionnaireFragmentBuilder.setQuestionnaireResponse(it.encodeResourceToString())
@@ -310,23 +318,22 @@ open class QuestionnaireActivity : BaseMultiLanguageActivity(), View.OnClickList
310318
}
311319

312320
open fun showFormSubmissionConfirmAlert() {
313-
if (questionnaire.experimental) {
314-
showConfirmAlert(
315-
context = this,
316-
message = R.string.questionnaire_alert_test_only_message,
317-
title = R.string.questionnaire_alert_test_only_title,
318-
confirmButtonListener = { handleQuestionnaireSubmit() },
319-
confirmButtonText = R.string.questionnaire_alert_test_only_button_title,
320-
)
321-
} else {
322-
showConfirmAlert(
323-
context = this,
324-
message = R.string.questionnaire_alert_submit_message,
325-
title = R.string.questionnaire_alert_submit_title,
326-
confirmButtonListener = { handleQuestionnaireSubmit() },
327-
confirmButtonText = R.string.questionnaire_alert_submit_button_title,
328-
)
329-
}
321+
showConfirmAlert(
322+
context = this,
323+
message =
324+
if (questionnaire.experimental) {
325+
R.string.questionnaire_alert_test_only_message
326+
} else R.string.questionnaire_alert_submit_message,
327+
title =
328+
if (questionnaire.experimental) {
329+
R.string.questionnaire_alert_test_only_title
330+
} else R.string.questionnaire_alert_submit_title,
331+
confirmButtonListener = { handleQuestionnaireSubmit() },
332+
confirmButtonText =
333+
if (questionnaire.experimental) {
334+
R.string.questionnaire_alert_test_only_button_title
335+
} else R.string.questionnaire_alert_submit_button_title,
336+
)
330337
}
331338

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

344351
open fun handleQuestionnaireSubmit() {
345352
saveProcessingAlertDialog = showProgressAlert(this, R.string.form_progress_message)
346-
347-
val questionnaireResponse = getQuestionnaireResponse()
348-
if (!validQuestionnaireResponse(questionnaireResponse)) {
349-
saveProcessingAlertDialog.dismiss()
350-
351-
AlertDialogue.showErrorAlert(
352-
this,
353-
R.string.questionnaire_alert_invalid_message,
354-
R.string.questionnaire_alert_invalid_title,
355-
)
356-
return
357-
}
358-
359-
handleQuestionnaireResponse(questionnaireResponse)
360-
361-
questionnaireViewModel.extractionProgress.observe(this) { result ->
362-
if (result is ExtractionProgress.Success) {
363-
onPostSave(true, questionnaireResponse, result.extras)
364-
} else {
365-
onPostSave(false, questionnaireResponse)
353+
getQuestionnaireResponse()
354+
.takeIf { validQuestionnaireResponse(it) }
355+
?.let { handleQuestionnaireResponse(it) }
356+
?: {
357+
saveProcessingAlertDialog.dismiss().also {
358+
AlertDialogue.showErrorAlert(
359+
this,
360+
R.string.questionnaire_alert_invalid_message,
361+
R.string.questionnaire_alert_invalid_title,
362+
)
363+
}
366364
}
367-
}
368365
}
369366

370367
fun onPostSave(

android/engine/src/main/java/org/smartregister/fhircore/engine/ui/questionnaire/QuestionnaireViewModel.kt

+121-80
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,12 @@ import java.util.Date
4040
import java.util.UUID
4141
import javax.inject.Inject
4242
import javax.inject.Provider
43+
import kotlin.time.Duration.Companion.milliseconds
4344
import kotlinx.coroutines.Dispatchers
45+
import kotlinx.coroutines.FlowPreview
46+
import kotlinx.coroutines.flow.MutableStateFlow
47+
import kotlinx.coroutines.flow.debounce
48+
import kotlinx.coroutines.flow.filter
4449
import kotlinx.coroutines.launch
4550
import kotlinx.coroutines.withContext
4651
import org.hl7.fhir.r4.context.IWorkerContext
@@ -100,6 +105,7 @@ import org.smartregister.fhircore.engine.util.helper.TransformSupportServices
100105
import timber.log.Timber
101106

102107
@HiltViewModel
108+
@OptIn(FlowPreview::class)
103109
open class QuestionnaireViewModel
104110
@Inject
105111
constructor(
@@ -137,6 +143,17 @@ constructor(
137143
?.extractLogicalIdUuid()
138144
}
139145

146+
private val extractAndSaveRequestStateFlow: MutableStateFlow<suspend () -> Unit> =
147+
MutableStateFlow {}
148+
149+
init {
150+
viewModelScope.launch(dispatcherProvider.io()) {
151+
extractAndSaveRequestStateFlow.debounce(800.milliseconds).collect {
152+
it.invoke() // invoke request
153+
}
154+
}
155+
}
156+
140157
suspend fun loadQuestionnaire(id: String, type: QuestionnaireType): Questionnaire? =
141158
defaultRepository.loadResource<Questionnaire>(id)?.apply {
142159
if (type.isReadOnly() || type.isEditMode()) {
@@ -272,102 +289,126 @@ constructor(
272289
questionnaireType: QuestionnaireType = QuestionnaireType.DEFAULT,
273290
questionnaire: Questionnaire,
274291
) {
275-
viewModelScope.launch(dispatcherProvider.io()) {
276-
tracer.startTrace(QUESTIONNAIRE_TRACE)
277-
// important to set response subject so that structure map can handle subject for all entities
278-
handleQuestionnaireResponseSubject(resourceId, questionnaire, questionnaireResponse)
279-
val extras = mutableListOf<Resource>()
280-
if (questionnaire.isExtractionCandidate()) {
281-
val bundle = performExtraction(context, questionnaire, questionnaireResponse)
282-
questionnaireResponse.contained = mutableListOf()
283-
bundle.entry.forEach { bundleEntry ->
284-
// add organization to entities representing individuals in registration questionnaire
285-
if (bundleEntry.resource.resourceType.isIn(ResourceType.Patient, ResourceType.Group)) {
286-
if (questionnaireConfig.setOrganizationDetails) {
287-
appendOrganizationInfo(bundleEntry.resource)
288-
}
289-
// if it is new registration set response subject
290-
if (resourceId == null) {
291-
questionnaireResponse.subject = bundleEntry.resource.asReference()
292-
}
293-
}
294-
if (questionnaireConfig.setPractitionerDetails) {
295-
appendPractitionerInfo(bundleEntry.resource)
296-
}
297-
298-
if (
299-
questionnaireType != QuestionnaireType.EDIT &&
300-
bundleEntry.resource.resourceType.isIn(
301-
ResourceType.Patient,
302-
ResourceType.RelatedPerson,
303-
)
304-
) {
305-
groupResourceId?.let {
306-
appendPatientsAndRelatedPersonsToGroups(
307-
resource = bundleEntry.resource,
308-
groupResourceId = it,
309-
)
310-
}
311-
}
292+
val request = suspend {
293+
try {
294+
val extras =
295+
doExtractAndSaveResources(
296+
context,
297+
resourceId,
298+
groupResourceId,
299+
questionnaireResponse,
300+
questionnaireType,
301+
questionnaire,
302+
)
303+
extractionProgress.postValue(ExtractionProgress.Success(questionnaireResponse, extras))
304+
} catch (e: Exception) {
305+
extractionProgress.postValue(ExtractionProgress.Failed(questionnaireResponse, e))
306+
}
307+
}
312308

313-
// response MUST have subject by far otherwise flow has issues
314-
if (!questionnaire.experimental) questionnaireResponse.assertSubject()
309+
extractAndSaveRequestStateFlow.value = request
310+
}
315311

316-
// TODO https://github.com/opensrp/fhircore/issues/900
317-
// for edit mode replace client and resource subject ids.
318-
// Ideally ResourceMapper should allow this internally via structure-map
319-
if (questionnaireType.isEditMode()) {
320-
if (bundleEntry.resource.resourceType.isIn(ResourceType.Patient, ResourceType.Group)) {
321-
bundleEntry.resource.id = questionnaireResponse.subject.extractId()
322-
} else {
323-
bundleEntry.resource.setPropertySafely("subject", questionnaireResponse.subject)
324-
bundleEntry.resource.setPropertySafely("patient", questionnaireResponse.subject)
325-
}
312+
private suspend fun doExtractAndSaveResources(
313+
context: Context,
314+
resourceId: String?,
315+
groupResourceId: String? = null,
316+
questionnaireResponse: QuestionnaireResponse,
317+
questionnaireType: QuestionnaireType = QuestionnaireType.DEFAULT,
318+
questionnaire: Questionnaire,
319+
): List<Resource> {
320+
tracer.startTrace(QUESTIONNAIRE_TRACE)
321+
// important to set response subject so that structure map can handle subject for all entities
322+
handleQuestionnaireResponseSubject(resourceId, questionnaire, questionnaireResponse)
323+
val extras = mutableListOf<Resource>()
324+
if (questionnaire.isExtractionCandidate()) {
325+
val bundle = performExtraction(context, questionnaire, questionnaireResponse)
326+
questionnaireResponse.contained = mutableListOf()
327+
bundle.entry.forEach { bundleEntry ->
328+
// add organization to entities representing individuals in registration questionnaire
329+
if (bundleEntry.resource.resourceType.isIn(ResourceType.Patient, ResourceType.Group)) {
330+
if (questionnaireConfig.setOrganizationDetails) {
331+
appendOrganizationInfo(bundleEntry.resource)
326332
}
327-
questionnaireResponse.contained.add(bundleEntry.resource)
328-
329-
if (bundleEntry.resource is Encounter) extras.add(bundleEntry.resource)
330-
331-
if (
332-
(bundleEntry.resource is CarePlan || bundleEntry.resource is Patient) &&
333-
bundleEntry.resource.meta.tag.isNotEmpty()
334-
) {
335-
carePlanAndPatientMetaExtraction(bundleEntry.resource)
333+
// if it is new registration set response subject
334+
if (resourceId == null) {
335+
questionnaireResponse.subject = bundleEntry.resource.asReference()
336336
}
337337
}
338-
339-
if (questionnaire.experimental) {
340-
Timber.w(
341-
"${questionnaire.name}(${questionnaire.logicalId}) is experimental and not save any data",
342-
)
343-
} else {
344-
saveBundleResources(bundle)
338+
if (questionnaireConfig.setPractitionerDetails) {
339+
appendPractitionerInfo(bundleEntry.resource)
345340
}
346341

347-
if (questionnaireType.isEditMode() && editQuestionnaireResponse != null) {
348-
questionnaireResponse.retainMetadata(editQuestionnaireResponse!!)
342+
if (
343+
questionnaireType != QuestionnaireType.EDIT &&
344+
bundleEntry.resource.resourceType.isIn(
345+
ResourceType.Patient,
346+
ResourceType.RelatedPerson,
347+
)
348+
) {
349+
groupResourceId?.let {
350+
appendPatientsAndRelatedPersonsToGroups(
351+
resource = bundleEntry.resource,
352+
groupResourceId = it,
353+
)
354+
}
349355
}
350356

351-
saveQuestionnaireResponse(questionnaire, questionnaireResponse)
352-
questionnaireResponseLiveData.postValue(questionnaireResponse)
357+
// response MUST have subject by far otherwise flow has issues
358+
if (!questionnaire.experimental) questionnaireResponse.assertSubject()
359+
353360
// TODO https://github.com/opensrp/fhircore/issues/900
354-
// reassess following i.e. deleting/updating older resources because one resource
355-
// might have generated other flow in subsequent followups
356-
if (questionnaireType.isEditMode() && editQuestionnaireResponse != null) {
357-
editQuestionnaireResponse!!.deleteRelatedResources(defaultRepository)
361+
// for edit mode replace client and resource subject ids.
362+
// Ideally ResourceMapper should allow this internally via structure-map
363+
if (questionnaireType.isEditMode()) {
364+
if (bundleEntry.resource.resourceType.isIn(ResourceType.Patient, ResourceType.Group)) {
365+
bundleEntry.resource.id = questionnaireResponse.subject.extractId()
366+
} else {
367+
bundleEntry.resource.setPropertySafely("subject", questionnaireResponse.subject)
368+
bundleEntry.resource.setPropertySafely("patient", questionnaireResponse.subject)
369+
}
370+
}
371+
questionnaireResponse.contained.add(bundleEntry.resource)
372+
373+
if (bundleEntry.resource is Encounter) extras.add(bundleEntry.resource)
374+
375+
if (
376+
(bundleEntry.resource is CarePlan || bundleEntry.resource is Patient) &&
377+
bundleEntry.resource.meta.tag.isNotEmpty()
378+
) {
379+
carePlanAndPatientMetaExtraction(bundleEntry.resource)
358380
}
381+
}
359382

360-
extractCqlOutput(questionnaire, questionnaireResponse, bundle)
361-
extractCarePlan(questionnaireResponse, bundle)
383+
if (questionnaire.experimental) {
384+
Timber.w(
385+
"${questionnaire.name}(${questionnaire.logicalId}) is experimental and not save any data",
386+
)
362387
} else {
363-
saveQuestionnaireResponse(questionnaire, questionnaireResponse)
364-
extractCqlOutput(questionnaire, questionnaireResponse, null)
388+
saveBundleResources(bundle)
389+
}
390+
391+
if (questionnaireType.isEditMode() && editQuestionnaireResponse != null) {
392+
questionnaireResponse.retainMetadata(editQuestionnaireResponse!!)
365393
}
366-
tracer.stopTrace(QUESTIONNAIRE_TRACE)
367-
viewModelScope.launch(Dispatchers.Main) {
368-
extractionProgress.postValue(ExtractionProgress.Success(extras))
394+
395+
saveQuestionnaireResponse(questionnaire, questionnaireResponse)
396+
questionnaireResponseLiveData.postValue(questionnaireResponse)
397+
// TODO https://github.com/opensrp/fhircore/issues/900
398+
// reassess following i.e. deleting/updating older resources because one resource
399+
// might have generated other flow in subsequent followups
400+
if (questionnaireType.isEditMode() && editQuestionnaireResponse != null) {
401+
editQuestionnaireResponse!!.deleteRelatedResources(defaultRepository)
369402
}
403+
404+
extractCqlOutput(questionnaire, questionnaireResponse, bundle)
405+
extractCarePlan(questionnaireResponse, bundle)
406+
} else {
407+
saveQuestionnaireResponse(questionnaire, questionnaireResponse)
408+
extractCqlOutput(questionnaire, questionnaireResponse, null)
370409
}
410+
tracer.stopTrace(QUESTIONNAIRE_TRACE)
411+
return extras
371412
}
372413

373414
suspend fun carePlanAndPatientMetaExtraction(source: Resource) {

0 commit comments

Comments
 (0)