Skip to content

Commit

Permalink
pull out qr validation
Browse files Browse the repository at this point in the history
  • Loading branch information
pld committed Jan 9, 2025
1 parent 27d8c85 commit 982399f
Show file tree
Hide file tree
Showing 7 changed files with 147 additions and 59 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import org.smartregister.fhircore.quest.R
import org.smartregister.fhircore.quest.databinding.QuestionnaireActivityBinding
import org.smartregister.fhircore.quest.ui.shared.ActivityOnResultType
import org.smartregister.fhircore.quest.ui.shared.ON_RESULT_TYPE
import org.smartregister.fhircore.quest.util.QuestionnaireResponseValidator
import org.smartregister.fhircore.quest.util.ResourceUtils
import timber.log.Timber

Expand Down Expand Up @@ -285,7 +286,12 @@ class QuestionnaireActivity : BaseMultiLanguageActivity() {
if (questionnaireResponse != null) {
questionnaireResponse
.takeIf {
viewModel.validateQuestionnaireResponse(questionnaire, it, this@QuestionnaireActivity)
QuestionnaireResponseValidator.validateQuestionnaireResponse(
questionnaire,
it,
this@QuestionnaireActivity,
viewModel.dispatcherProvider,
)
}
?.let { setQuestionnaireResponse(it.json()) }
?: showToast(getString(R.string.error_populating_questionnaire))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,6 @@ import ca.uhn.fhir.context.FhirContext
import com.google.android.fhir.datacapture.extensions.logicalId
import com.google.android.fhir.datacapture.mapping.ResourceMapper
import com.google.android.fhir.datacapture.mapping.StructureMapExtractionContext
import com.google.android.fhir.datacapture.validation.NotValidated
import com.google.android.fhir.datacapture.validation.QuestionnaireResponseValidator
import com.google.android.fhir.datacapture.validation.Valid
import com.google.android.fhir.db.ResourceNotFoundException
import com.google.android.fhir.search.Search
import com.google.android.fhir.search.filter.TokenParamFilterCriterion
Expand Down Expand Up @@ -92,7 +89,6 @@ import org.smartregister.fhircore.engine.util.extension.extractLogicalIdUuid
import org.smartregister.fhircore.engine.util.extension.find
import org.smartregister.fhircore.engine.util.extension.generateMissingId
import org.smartregister.fhircore.engine.util.extension.isIn
import org.smartregister.fhircore.engine.util.extension.packRepeatedGroups
import org.smartregister.fhircore.engine.util.extension.prepopulateWithComputedConfigValues
import org.smartregister.fhircore.engine.util.extension.questionnaireResponseStatus
import org.smartregister.fhircore.engine.util.extension.showToast
Expand All @@ -102,6 +98,7 @@ import org.smartregister.fhircore.engine.util.helper.TransformSupportServices
import org.smartregister.fhircore.engine.util.validation.ResourceValidationRequest
import org.smartregister.fhircore.engine.util.validation.ResourceValidationRequestHandler
import org.smartregister.fhircore.quest.R
import org.smartregister.fhircore.quest.util.QuestionnaireResponseValidator
import timber.log.Timber

@HiltViewModel
Expand Down Expand Up @@ -169,10 +166,11 @@ constructor(
) {
viewModelScope.launch(SupervisorJob()) {
val questionnaireResponseValid =
validateQuestionnaireResponse(
QuestionnaireResponseValidator.validateQuestionnaireResponse(
questionnaire = questionnaire,
questionnaireResponse = currentQuestionnaireResponse,
context = context,
dispatcherProvider = dispatcherProvider,
)

if (questionnaireConfig.saveQuestionnaireResponse && !questionnaireResponseValid) {
Expand Down Expand Up @@ -788,45 +786,6 @@ constructor(
}
}

/**
* This function validates all [QuestionnaireResponse] and returns true if all the validation
* result of [QuestionnaireResponseValidator] are [Valid] or [NotValidated] (validation is
* optional on [Questionnaire] fields)
*/
suspend fun validateQuestionnaireResponse(
questionnaire: Questionnaire,
questionnaireResponse: QuestionnaireResponse,
context: Context,
): Boolean {
val validQuestionnaireResponseItems = mutableListOf<QuestionnaireResponseItemComponent>()
val validQuestionnaireItems = mutableListOf<Questionnaire.QuestionnaireItemComponent>()
val questionnaireItemsMap = questionnaire.item.associateBy { it.linkId }

// Only validate items that are present on both Questionnaire and the QuestionnaireResponse
questionnaireResponse.copy().item.forEach {
if (questionnaireItemsMap.containsKey(it.linkId)) {
val questionnaireItem = questionnaireItemsMap.getValue(it.linkId)
validQuestionnaireResponseItems.add(it)
validQuestionnaireItems.add(questionnaireItem)
}
}

return withContext(dispatcherProvider.default()) {
QuestionnaireResponseValidator.validateQuestionnaireResponse(
questionnaire = Questionnaire().apply { item = validQuestionnaireItems },
questionnaireResponse =
QuestionnaireResponse().apply {
item = validQuestionnaireResponseItems
packRepeatedGroups()
},
context = context,
)
.values
.flatten()
.all { it is Valid || it is NotValidated }
}
}

suspend fun executeCql(
subject: Resource,
bundle: Bundle,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class SpeechToForm(
private val speechToText: SpeechToText,
geminiModel: GeminiModel,
) {
private val textToForm: TextToForm = TextToForm(geminiModel.getGeminiModel())
private val textToForm: TextToForm = TextToForm(geminiModel.getGeminiModel(), null)
private val logger = Logger.getLogger(SpeechToForm::class.java.name)

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,16 @@ import java.util.logging.Logger
import org.hl7.fhir.r4.model.Questionnaire
import org.hl7.fhir.r4.model.QuestionnaireResponse
import org.json.JSONObject
import org.smartregister.fhircore.engine.util.DefaultDispatcherProvider
import org.smartregister.fhircore.engine.util.DispatcherProvider
import org.smartregister.fhircore.quest.ui.questionnaire.QuestionnaireActivity
import org.smartregister.fhircore.quest.util.QuestionnaireResponseValidator

class TextToForm(private val generativeModel: GenerativeModel) {
class TextToForm(
private val generativeModel: GenerativeModel,
private val dispatcherProvider: DispatcherProvider?,
private val questionnaireActivity: QuestionnaireActivity?,
) {

private val logger = Logger.getLogger(TextToForm::class.java.name)

Expand All @@ -49,7 +57,7 @@ class TextToForm(private val generativeModel: GenerativeModel) {

return try {
val questionnaireResponse = parseQuestionnaireResponse(questionnaireResponseJson)
if (validateQuestionnaireResponse(questionnaireResponse)) {
if (validateQuestionnaireResponse(questionnaire, questionnaireResponse)) {
logger.info("QuestionnaireResponse validated successfully.")
questionnaireResponse
} else {
Expand Down Expand Up @@ -113,9 +121,29 @@ class TextToForm(private val generativeModel: GenerativeModel) {
* @param questionnaireResponse The QuestionnaireResponse object to validate.
* @return True if the QuestionnaireResponse is valid, false otherwise.
*/
private fun validateQuestionnaireResponse(questionnaireResponse: QuestionnaireResponse): Boolean {
// todo use SDC validation
private suspend fun validateQuestionnaireResponse(
questionnaire: Questionnaire,
questionnaireResponse: QuestionnaireResponse,
): Boolean {
// validate using existing code, not sure about how to pass/instantiate in the
// dispatcherProvider and questionnaireActivity
val errors =
QuestionnaireResponseValidator.getQuestionnaireResponseErrors(
questionnaire,
questionnaireResponse,
QuestionnaireActivity(),
DefaultDispatcherProvider(),
)

// no errors, no need to retry
if (errors.isEmpty()) {
return true
}

// TODO build new prompt from errors

// TODO retry with new prompt, and go to top

return true
return false
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* Copyright 2021-2024 Ona Systems, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.smartregister.fhircore.quest.util

import android.content.Context
import com.google.android.fhir.datacapture.validation.NotValidated
import com.google.android.fhir.datacapture.validation.QuestionnaireResponseValidator
import com.google.android.fhir.datacapture.validation.Valid
import com.google.android.fhir.datacapture.validation.ValidationResult
import kotlinx.coroutines.withContext
import org.hl7.fhir.r4.model.Questionnaire
import org.hl7.fhir.r4.model.QuestionnaireResponse
import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseItemComponent
import org.smartregister.fhircore.engine.util.DispatcherProvider
import org.smartregister.fhircore.engine.util.extension.packRepeatedGroups

class QuestionnaireResponseValidator {
/**
* This function validates all [QuestionnaireResponse] and returns true if all the validation
* result of [QuestionnaireResponseValidator] are [Valid] or [NotValidated] (validation is
* optional on [Questionnaire] fields)
*/
companion object {
suspend fun validateQuestionnaireResponse(
questionnaire: Questionnaire,
questionnaireResponse: QuestionnaireResponse,
context: Context,
dispatcherProvider: DispatcherProvider,
): Boolean {
return getQuestionnaireResponseErrors(
questionnaire = questionnaire,
questionnaireResponse = questionnaireResponse,
context = context,
dispatcherProvider = dispatcherProvider,
)
.isEmpty()
}

suspend fun getQuestionnaireResponseErrors(
questionnaire: Questionnaire,
questionnaireResponse: QuestionnaireResponse,
context: Context,
dispatcherProvider: DispatcherProvider,
): List<ValidationResult> {
val validQuestionnaireResponseItems = mutableListOf<QuestionnaireResponseItemComponent>()
val validQuestionnaireItems = mutableListOf<Questionnaire.QuestionnaireItemComponent>()
val questionnaireItemsMap = questionnaire.item.associateBy { it.linkId }

// Only validate items that are present on both Questionnaire and the QuestionnaireResponse
questionnaireResponse.copy().item.forEach {
if (questionnaireItemsMap.containsKey(it.linkId)) {
val questionnaireItem = questionnaireItemsMap.getValue(it.linkId)
validQuestionnaireResponseItems.add(it)
validQuestionnaireItems.add(questionnaireItem)
}
}

return withContext(dispatcherProvider.default()) {
QuestionnaireResponseValidator.validateQuestionnaireResponse(
questionnaire = Questionnaire().apply { item = validQuestionnaireItems },
questionnaireResponse =
QuestionnaireResponse().apply {
item = validQuestionnaireResponseItems
packRepeatedGroups()
},
context = context,
)
.values
.flatten()
.filter { it !is Valid }
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ import org.smartregister.fhircore.quest.app.fakes.Faker
import org.smartregister.fhircore.quest.assertResourceEquals
import org.smartregister.fhircore.quest.robolectric.RobolectricTest
import org.smartregister.fhircore.quest.ui.questionnaire.QuestionnaireViewModel.Companion.CONTAINED_LIST_TITLE
import org.smartregister.fhircore.quest.util.QuestionnaireResponseValidator
import org.smartregister.model.practitioner.FhirPractitionerDetails
import org.smartregister.model.practitioner.PractitionerDetails
import timber.log.Timber
Expand Down Expand Up @@ -292,10 +293,11 @@ class QuestionnaireViewModelTest : RobolectricTest() {

// Verify QuestionnaireResponse was validated
coVerify {
questionnaireViewModel.validateQuestionnaireResponse(
QuestionnaireResponseValidator.validateQuestionnaireResponse(
questionnaire,
questionnaireResponse,
context,
questionnaireViewModel.dispatcherProvider,
)
}
// Verify perform extraction was invoked
Expand Down Expand Up @@ -395,10 +397,11 @@ class QuestionnaireViewModelTest : RobolectricTest() {

// Verify QuestionnaireResponse was validated
coVerify {
questionnaireViewModel.validateQuestionnaireResponse(
QuestionnaireResponseValidator.validateQuestionnaireResponse(
questionnaire = questionnaire,
questionnaireResponse = questionnaireResponse,
context = context,
dispatcherProvider = questionnaireViewModel.dispatcherProvider,
)
}

Expand Down Expand Up @@ -924,16 +927,17 @@ class QuestionnaireViewModelTest : RobolectricTest() {
runBlocking {
// No answer provided
Assert.assertFalse(
questionnaireViewModel.validateQuestionnaireResponse(
QuestionnaireResponseValidator.validateQuestionnaireResponse(
questionnaire,
questionnaireResponse,
context,
questionnaireViewModel.dispatcherProvider,
),
)

// With an answer provided
Assert.assertTrue(
questionnaireViewModel.validateQuestionnaireResponse(
QuestionnaireResponseValidator.validateQuestionnaireResponse(
questionnaire,
questionnaireResponse.apply {
itemFirstRep.answer =
Expand All @@ -943,6 +947,7 @@ class QuestionnaireViewModelTest : RobolectricTest() {
)
},
context,
questionnaireViewModel.dispatcherProvider,
),
)
}
Expand Down Expand Up @@ -1037,10 +1042,11 @@ class QuestionnaireViewModelTest : RobolectricTest() {
val questionnaireResponse =
parser.parseResource(questionnaireResponseString) as QuestionnaireResponse
val result =
questionnaireViewModel.validateQuestionnaireResponse(
QuestionnaireResponseValidator.validateQuestionnaireResponse(
questionnaire,
questionnaireResponse,
context,
questionnaireViewModel.dispatcherProvider,
)
Assert.assertTrue(result)
}
Expand Down Expand Up @@ -1161,10 +1167,11 @@ class QuestionnaireViewModelTest : RobolectricTest() {
val actualQuestionnaireResponse =
parser.parseResource(questionnaireResponseString) as QuestionnaireResponse
val result =
questionnaireViewModel.validateQuestionnaireResponse(
QuestionnaireResponseValidator.validateQuestionnaireResponse(
questionnaire,
actualQuestionnaireResponse,
context,
questionnaireViewModel.dispatcherProvider,
)
val expectedQuestionnaireResponse =
parser.parseResource(questionnaireResponseString) as QuestionnaireResponse
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@ class TextToFormTest {
fun setUp() {
if (!useRealApi) {
mockGenerativeModel = mockk(relaxed = true)
textToForm = TextToForm(mockGenerativeModel)
textToForm = TextToForm(mockGenerativeModel, null)
} else {
val geminiModel = GeminiModel()
textToForm = TextToForm(geminiModel.getGeminiModel())
textToForm = TextToForm(geminiModel.getGeminiModel(), null)
}
}

Expand Down

0 comments on commit 982399f

Please sign in to comment.