Skip to content

Commit

Permalink
work in progress
Browse files Browse the repository at this point in the history
  • Loading branch information
LZRS committed Nov 27, 2023
1 parent 20d6b14 commit e5fb8fe
Show file tree
Hide file tree
Showing 12 changed files with 323 additions and 55 deletions.
8 changes: 8 additions & 0 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ allprojects {
url = uri("/Users/ndegwamartin/.m2.dev/fhirsdk")
}
}

configurations {
configureEach {
resolutionStrategy {
force "ca.uhn.hapi.fhir:hapi-fhir-validation:6.0.1"
}
}
}
}

subprojects {
Expand Down
9 changes: 3 additions & 6 deletions android/engine/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -112,12 +112,6 @@ android {
}
}
}

configurations.all {
resolutionStrategy {
force "ca.uhn.hapi.fhir:org.hl7.fhir.utilities:5.5.7"
}
}
}

dependencies {
Expand Down Expand Up @@ -147,6 +141,9 @@ dependencies {
implementation(group: "com.github.java-json-tools", name: "msg-simple", version: "1.2");
implementation 'org.codehaus.woodstox:woodstox-core-asl:4.4.1'
implementation "ca.uhn.hapi.fhir:hapi-fhir-android:5.4.0"
implementation ("ca.uhn.hapi.fhir:hapi-fhir-validation:6.0.1"){
exclude module: "commons-logging"
}
implementation 'org.opencds.cqf.cql:engine:1.5.4'
implementation 'org.opencds.cqf.cql:engine.fhir:1.5.4'
implementation 'org.opencds.cqf.cql:evaluator:1.4.2'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright 2021 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.engine.di

import ca.uhn.fhir.context.FhirContext
import ca.uhn.fhir.validation.FhirValidator
import ca.uhn.fhir.validation.ResultSeverityEnum
import ca.uhn.fhir.validation.ValidationResult
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
import kotlin.coroutines.coroutineContext
import kotlinx.coroutines.withContext
import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator
import org.hl7.fhir.r4.model.Resource

@InstallIn(SingletonComponent::class)
@Module
class FhirValidatorModule {

@Singleton
@Provides
fun provideFhirValidator(): FhirValidator {
val fhirContext = FhirContext.forR4()
val validatorModule = FhirInstanceValidator(fhirContext)
return fhirContext.newValidator().apply { registerValidatorModule(validatorModule) }
}
}

val ValidationResult.errorMessages
get() = buildString {
for (validationMsg in messages.filter { it.severity == ResultSeverityEnum.ERROR }) {
appendLine("${validationMsg.message} - ${validationMsg.locationString}")
}
}

suspend fun FhirValidator.checkResourceValid(resource: Resource): ValidationResult {
return withContext(coroutineContext) { this@checkResourceValid.validateWithResult(resource) }
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,6 @@ constructor(
}
}
)

override fun getFhirEngine(): FhirEngine = engine
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,5 @@ import org.hl7.fhir.r4.model.Resource

sealed class ExtractionProgress {
class Success(val extras: List<Resource>? = null) : ExtractionProgress()
object Failed : ExtractionProgress()
class Failed(val errorMessages: String? = null) : ExtractionProgress()
}
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,13 @@ open class QuestionnaireActivity : BaseMultiLanguageActivity(), View.OnClickList
if (result is ExtractionProgress.Success) {
onPostSave(true, questionnaireResponse, result.extras)
} else {
result as ExtractionProgress.Failed
AlertDialogue.showErrorAlert(
this,
result.errorMessages ?: "",
getString(R.string.questionnaire_alert_extraction_fail)
)

onPostSave(false, questionnaireResponse)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,16 @@ package org.smartregister.fhircore.engine.ui.questionnaire

import android.content.Context
import android.content.Intent
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import ca.uhn.fhir.context.FhirContext
import ca.uhn.fhir.context.FhirVersionEnum
import ca.uhn.fhir.rest.gclient.TokenClientParam
import ca.uhn.fhir.rest.param.ParamPrefixEnum
import ca.uhn.fhir.validation.FhirValidator
import com.google.android.fhir.FhirEngine
import com.google.android.fhir.datacapture.mapping.ResourceMapper
import com.google.android.fhir.datacapture.mapping.StructureMapExtractionContext
Expand Down Expand Up @@ -71,6 +74,8 @@ import org.smartregister.fhircore.engine.configuration.view.FormConfiguration
import org.smartregister.fhircore.engine.cql.LibraryEvaluator
import org.smartregister.fhircore.engine.data.local.DefaultRepository
import org.smartregister.fhircore.engine.data.remote.model.response.UserInfo
import org.smartregister.fhircore.engine.di.checkResourceValid
import org.smartregister.fhircore.engine.di.errorMessages
import org.smartregister.fhircore.engine.task.FhirCarePlanGenerator
import org.smartregister.fhircore.engine.trace.PerformanceReporter
import org.smartregister.fhircore.engine.util.AssetUtil
Expand Down Expand Up @@ -110,14 +115,20 @@ constructor(
val dispatcherProvider: DispatcherProvider,
val sharedPreferencesHelper: SharedPreferencesHelper,
val libraryEvaluatorProvider: Provider<LibraryEvaluator>,
val fhirValidatorProvider: Provider<FhirValidator>,
var tracer: PerformanceReporter
) : ViewModel() {
@Inject lateinit var fhirCarePlanGenerator: FhirCarePlanGenerator

val extractionProgress = MutableLiveData<ExtractionProgress>()
private val _extractionProgress = MutableLiveData<ExtractionProgress>()
val extractionProgress: LiveData<ExtractionProgress> = _extractionProgress

val questionnaireResponseLiveData = MutableLiveData<QuestionnaireResponse?>(null)

val extractionProgressMessage = MutableLiveData<String>()
@Suppress("PropertyName")
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
val _extractionProgressMessage = MutableLiveData<String>()
val extractionProgressMessage: LiveData<String> = _extractionProgressMessage

var editQuestionnaireResponse: QuestionnaireResponse? = null

Expand Down Expand Up @@ -334,7 +345,23 @@ constructor(
Timber.w(
"${questionnaire.name}(${questionnaire.logicalId}) is experimental and not save any data"
)
} else saveBundleResources(bundle)
} else {
val unsuccessfulValidationResults =
bundle.entry
.map { fhirValidatorProvider.get().checkResourceValid(it.resource) }
.filter { !it.isSuccessful }


if (unsuccessfulValidationResults.isNotEmpty()) {
val mergedErrorMessages = buildString {
unsuccessfulValidationResults.forEach { appendLine(it.errorMessages) }
}
_extractionProgress.postValue(ExtractionProgress.Failed(mergedErrorMessages))
return@launch
}

saveBundleResources(bundle)
}

if (questionnaireType.isEditMode() && editQuestionnaireResponse != null) {
questionnaireResponse.retainMetadata(editQuestionnaireResponse!!)
Expand All @@ -357,7 +384,7 @@ constructor(
}
tracer.stopTrace(QUESTIONNAIRE_TRACE)
viewModelScope.launch(Dispatchers.Main) {
extractionProgress.postValue(ExtractionProgress.Success(extras))
_extractionProgress.postValue(ExtractionProgress.Success(extras))
}
}
}
Expand Down Expand Up @@ -398,7 +425,7 @@ constructor(
.runCatching { fhirCarePlanGenerator.generateCarePlan(planId, subject, data) }
.onFailure {
Timber.e(it)
extractionProgressMessage.postValue("Error extracting care plan. ${it.message}")
_extractionProgressMessage.postValue("Error extracting care plan. ${it.message}")
}
}
}
Expand All @@ -420,7 +447,7 @@ constructor(
libraryEvaluatorProvider.get().runCqlLibrary(it, patient, data, defaultRepository)
}
.forEach { output ->
if (output.isNotEmpty()) extractionProgressMessage.postValue(output.joinToString("\n"))
if (output.isNotEmpty()) _extractionProgressMessage.postValue(output.joinToString("\n"))
}
}
}
Expand Down Expand Up @@ -466,7 +493,6 @@ constructor(
questionnaire.useContext.filter { it.hasValueCodeableConcept() }.forEach {
it.valueCodeableConcept.coding.forEach { questionnaireResponse.meta.addTag(it) }
}

defaultRepository.addOrUpdate(true, questionnaireResponse)
}

Expand Down Expand Up @@ -615,10 +641,6 @@ constructor(
return tasks.filter { it.status in arrayOf(TaskStatus.READY, TaskStatus.INPROGRESS) }
}

fun saveResource(resource: Resource) {
viewModelScope.launch { defaultRepository.save(resource = resource) }
}

fun extractRelevantObservation(
resource: Bundle,
questionnaireLogicalId: String,
Expand Down
1 change: 1 addition & 0 deletions android/engine/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
<string name="questionnaire_alert_confirm_button_title">Yes</string>
<string name="questionnaire_alert_invalid_message">Given details have validation errors. Resolve errors and submit again</string>
<string name="questionnaire_alert_invalid_title">Validation Failed</string>
<string name="questionnaire_alert_extraction_fail">Extraction did not succeed</string>
<string name="questionnaire_alert_ack_button_title">Ok</string>
<string name="username">Username</string>
<string name="password">Password</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,24 +31,31 @@ import androidx.fragment.app.commitNow
import androidx.test.core.app.ApplicationProvider
import ca.uhn.fhir.context.FhirContext
import ca.uhn.fhir.context.FhirVersionEnum
import ca.uhn.fhir.validation.FhirValidator
import com.google.android.fhir.datacapture.QuestionnaireFragment
import com.google.android.fhir.datacapture.validation.QuestionnaireResponseValidator.checkQuestionnaireResponse
import dagger.hilt.android.testing.BindValue
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import io.mockk.spyk
import io.mockk.unmockkObject
import io.mockk.verify
import javax.inject.Inject
import javax.inject.Provider
import kotlin.test.assertFailsWith
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runBlockingTest
import kotlinx.coroutines.test.runTest
import org.hl7.fhir.r4.model.CanonicalType
import org.hl7.fhir.r4.model.CarePlan
import org.hl7.fhir.r4.model.CodeableConcept
import org.hl7.fhir.r4.model.Coding
import org.hl7.fhir.r4.model.Encounter
Expand Down Expand Up @@ -110,25 +117,29 @@ class QuestionnaireActivityTest : ActivityRobolectricTest() {

@BindValue val syncBroadcaster = mockk<SyncBroadcaster>()

@BindValue
val questionnaireViewModel: QuestionnaireViewModel =
spyk(
QuestionnaireViewModel(
fhirEngine = mockk(),
defaultRepository = mockk { coEvery { addOrUpdate(true, any()) } just runs },
configurationRegistry = mockk(),
transformSupportServices = mockk(),
dispatcherProvider = dispatcherProvider,
sharedPreferencesHelper = mockk(),
libraryEvaluatorProvider = { mockk<LibraryEvaluator>() },
tracer = FakePerformanceReporter()
)
)
@Inject lateinit var fhirValidatorProvider: Provider<FhirValidator>

@BindValue lateinit var questionnaireViewModel: QuestionnaireViewModel

@Before
fun setUp() {
// TODO Proper set up
hiltRule.inject()
questionnaireViewModel =
spyk(
QuestionnaireViewModel(
fhirEngine = mockk(),
defaultRepository = mockk { coEvery { addOrUpdate(true, any()) } just runs },
configurationRegistry = mockk(),
transformSupportServices = mockk(),
dispatcherProvider = dispatcherProvider,
sharedPreferencesHelper = mockk(),
libraryEvaluatorProvider = { mockk<LibraryEvaluator>() },
fhirValidatorProvider = fhirValidatorProvider,
tracer = FakePerformanceReporter()
)
)

ApplicationProvider.getApplicationContext<Context>().apply { setTheme(R.style.AppTheme) }
intent =
Intent().apply {
Expand Down Expand Up @@ -508,6 +519,46 @@ class QuestionnaireActivityTest : ActivityRobolectricTest() {
}
}

@Test
fun testHandleQuestionnaireSubmitWithExtractionProgressFailureShowsErrorDialog() = runTest {
val sampleExtractedCarePlan = CarePlan()
coEvery { questionnaireViewModel.performExtraction(any(), any(), any()) } returns
org.hl7.fhir.r4.model.Bundle().apply {
addEntry().apply { resource = sampleExtractedCarePlan }
}
val questionnaire =
Questionnaire().apply {
addExtension(
"http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-targetStructureMap",
CanonicalType("1234")
)
}

ReflectionHelpers.setField(
questionnaireViewModel,
"questionnaireConfig",
QuestionnaireConfig("form", "title", "form-id", setPractitionerDetails = false)
)
ReflectionHelpers.setField(questionnaireActivity, "questionnaire", questionnaire)
questionnaireActivity.handleQuestionnaireSubmit()
advanceUntilIdle()

val dialog = shadowOf(ShadowAlertDialog.getLatestDialog())
val alertDialog = ReflectionHelpers.getField<AlertDialog>(dialog, "realDialog")

Assert.assertNotNull(questionnaireViewModel.extractionProgress.value)
Assert.assertTrue(questionnaireViewModel.extractionProgress.value is ExtractionProgress.Failed)

Assert.assertEquals(
"CarePlan.status: minimum required = 1, but only found 0 (from http://hl7.org/fhir/StructureDefinition/CarePlan) - CarePlan\n" +
"CarePlan.intent: minimum required = 1, but only found 0 (from http://hl7.org/fhir/StructureDefinition/CarePlan) - CarePlan\n" +
"CarePlan.subject: minimum required = 1, but only found 0 (from http://hl7.org/fhir/StructureDefinition/CarePlan) - CarePlan\n",
alertDialog.findViewById<TextView>(R.id.tv_alert_message)!!.text
)

coVerify(exactly = 0) { questionnaireViewModel.defaultRepository.addOrUpdate(resource = any()) }
}

@Test
fun testOnClickSaveButtonShouldShowSubmitConfirmationAlert() {
ReflectionHelpers.setField(
Expand Down Expand Up @@ -599,7 +650,7 @@ class QuestionnaireActivityTest : ActivityRobolectricTest() {

@Test
fun testPostSaveSuccessfulWithExtractionMessageShouldShowAlert() = runTest {
questionnaireActivity.questionnaireViewModel.extractionProgressMessage.postValue("ABC")
questionnaireActivity.questionnaireViewModel._extractionProgressMessage.postValue("ABC")
questionnaireActivity.postSaveSuccessful(QuestionnaireResponse())

val dialog = shadowOf(ShadowAlertDialog.getLatestDialog())
Expand Down
Loading

0 comments on commit e5fb8fe

Please sign in to comment.