Skip to content

Commit

Permalink
Merge branch 'master' of github.com:opensrp/android-fhir into master-…
Browse files Browse the repository at this point in the history
…release
  • Loading branch information
allan-on committed Apr 14, 2023
2 parents 8274e48 + 135feb2 commit 4a68b78
Show file tree
Hide file tree
Showing 135 changed files with 18,944 additions and 1,468 deletions.
1 change: 1 addition & 0 deletions benchmark/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ dependencies {
androidTestImplementation(Dependencies.truth)

androidTestImplementation(project(":engine"))
androidTestImplementation(project(":implementationguide"))
androidTestImplementation(project(":workflow"))
androidTestImplementation(project(":workflow-testing"))
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,22 @@

package com.google.android.fhir.benchmark

import android.content.Context
import androidx.benchmark.junit4.BenchmarkRule
import androidx.benchmark.junit4.measureRepeated
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import ca.uhn.fhir.context.FhirContext
import ca.uhn.fhir.context.FhirVersionEnum
import com.google.android.fhir.FhirEngineProvider
import com.google.android.fhir.workflow.FhirOperator
import com.google.android.fhir.implementationguide.IgManager
import com.google.android.fhir.workflow.FhirOperatorBuilder
import com.google.common.truth.Truth.assertThat
import java.io.File
import java.io.InputStream
import kotlinx.coroutines.runBlocking
import org.hl7.fhir.r4.model.Bundle
import org.hl7.fhir.r4.model.Library
import org.hl7.fhir.r4.model.Parameters
import org.junit.Rule
import org.junit.Test
Expand All @@ -48,20 +52,30 @@ class G_CqlEvaluatorBenchmark {
val fhirOperator = runWithTimingDisabled {
val fhirContext = FhirContext.forCached(FhirVersionEnum.R4)
val jsonParser = fhirContext.newJsonParser()
val context: Context = ApplicationProvider.getApplicationContext()

val patientImmunizationHistory =
jsonParser.parseResource(open("/immunity-check/ImmunizationHistory.json")) as Bundle
val fhirEngine = FhirEngineProvider.getInstance(ApplicationProvider.getApplicationContext())
val igManager = IgManager.createInMemory(context)
val lib = jsonParser.parseResource(open("/immunity-check/ImmunityCheck.json")) as Library

runBlocking {
for (entry in patientImmunizationHistory.entry) {
fhirEngine.create(entry.resource)
}
igManager.install(
File(context.filesDir, lib.name).apply {
writeText(jsonParser.encodeResourceToString(lib))
}
)
}

val lib = jsonParser.parseResource(open("/immunity-check/ImmunityCheck.json")) as Bundle

FhirOperator(fhirContext, fhirEngine).also { it.loadLibs(lib) }
FhirOperatorBuilder(context)
.withFhirContext(fhirContext)
.withFhirEngine(fhirEngine)
.withIgManager(igManager)
.build()
}

val results =
Expand Down
7 changes: 4 additions & 3 deletions buildSrc/src/main/kotlin/Dependencies.kt
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ object Dependencies {
const val compiler = "androidx.room:room-compiler:${Versions.Androidx.room}"
const val ktx = "androidx.room:room-ktx:${Versions.Androidx.room}"
const val runtime = "androidx.room:room-runtime:${Versions.Androidx.room}"
const val testing = "androidx.room:room-testing:${Versions.Androidx.room}"
}

object Mlkit {
Expand Down Expand Up @@ -280,7 +281,7 @@ object Dependencies {
const val slf4j = "1.7.36"
const val sqlcipher = "4.5.0"
const val timber = "5.0.1"
const val truth = "1.0.1"
const val truth = "1.1.3"
const val woodstox = "6.2.7"
const val xerces = "2.12.2"
const val xmlUnit = "2.9.0"
Expand All @@ -291,9 +292,9 @@ object Dependencies {
const val benchmarkJUnit = "1.1.0"
const val core = "1.4.0"
const val archCore = "2.1.0"
const val extJunit = "1.1.3"
const val extJunit = "1.1.5"
const val rules = "1.4.0"
const val runner = "1.4.0"
const val runner = "1.5.2"
const val fragmentVersion = "1.3.6"
}

Expand Down
1 change: 1 addition & 0 deletions buildSrc/src/main/kotlin/LicenseeConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -173,4 +173,5 @@ private val nonStandardLicenseUrls =
// BSD-3
"http://opensource.org/licenses/BSD-3-Clause",
"http://www.opensource.org/licenses/bsd-license.php",
"https://asm.ow2.io/license.html",
)
2 changes: 1 addition & 1 deletion buildSrc/src/main/kotlin/Releases.kt
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ object Releases {

object Engine : LibraryArtifact {
override val artifactId = "engine"
override val version = "0.1.0-beta02-preview14-SNAPSHOT"
override val version = "0.1.0-beta03"
override val name = "Android FHIR Engine Library"
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,16 @@
* limitations under the License.
*/

package com.google.android.fhir.datacapture.extensions
import java.io.File
import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.process.CommandLineArgumentProvider

import org.hl7.fhir.r4.model.QuestionnaireResponse
// https://developer.android.com/training/data-storage/room/migrating-db-versions#test
class RoomSchemaArgProvider(
@get:InputDirectory @get:PathSensitive(PathSensitivity.RELATIVE) val schemaDir: File
) : CommandLineArgumentProvider {

/** Pre-order list of all questionnaire response items in the questionnaire. */
val QuestionnaireResponse.allItems: List<QuestionnaireResponse.QuestionnaireResponseItemComponent>
get() = item.flatMap { it.descendant }
override fun asArguments() = listOf("-Aroom.schemaLocation=${schemaDir.path}")
}
11 changes: 11 additions & 0 deletions datacapture/sampledata/text_questionnaire_decimal.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"resourceType": "Questionnaire",
"status": "active",
"item": [
{
"linkId": "1",
"type": "decimal",
"text": "Enter a decimal"
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,10 @@ import com.google.android.fhir.datacapture.validation.QuestionnaireResponseValid
import com.google.android.fhir.datacapture.validation.Valid
import com.google.android.fhir.datacapture.views.factories.localDate
import com.google.android.fhir.datacapture.views.factories.localDateTime
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import com.google.common.truth.Truth.assertThat
import java.math.BigDecimal
import java.time.LocalDate
import java.time.LocalDateTime
import java.util.Calendar
Expand Down Expand Up @@ -112,6 +114,49 @@ class QuestionnaireUiEspressoTest {
}
}

@Test
fun integerTextEdit_typingZeroBeforeAnyIntegerShouldKeepZeroDisplayed() {
// Do not skip cursor when typing on the numeric field if the initial value is set to 0
// as from an integer comparison, leading zeros do not change how the answer is saved.
// e.g whether 000001 or 1 is input, the answer saved will be 1.
buildFragmentFromQuestionnaire("/text_questionnaire_integer.json")

onView(withId(R.id.text_input_edit_text)).perform(typeText("0"))
assertThat(getQuestionnaireResponse().item.first().answer.first().valueIntegerType.value)
.isEqualTo(0)

onView(withId(R.id.text_input_edit_text)).perform(typeText("01"))
assertThat(getQuestionnaireResponse().item.first().answer.first().valueIntegerType.value)
.isEqualTo(1)

onView(withId(R.id.text_input_edit_text)).check { view, _ ->
assertThat((view as TextInputEditText).text.toString()).isEqualTo("001")
}

assertThat(getQuestionnaireResponse().item.first().answer.first().valueIntegerType.value)
.isEqualTo(1)
}

@Test
fun decimalTextEdit_typingZeroBeforeAnyIntegerShouldKeepZeroDisplayed() {
buildFragmentFromQuestionnaire("/text_questionnaire_decimal.json")

onView(withId(R.id.text_input_edit_text)).perform(typeText("0."))
assertThat(getQuestionnaireResponse().item.first().answer.first().valueDecimalType.value)
.isEqualTo(BigDecimal.valueOf(0.0))

onView(withId(R.id.text_input_edit_text)).perform(typeText("01"))
assertThat(getQuestionnaireResponse().item.first().answer.first().valueDecimalType.value)
.isEqualTo(BigDecimal.valueOf(0.01))

onView(withId(R.id.text_input_edit_text)).check { view, _ ->
assertThat((view as TextInputEditText).text.toString()).isEqualTo("0.01")
}

assertThat(getQuestionnaireResponse().item.first().answer.first().valueDecimalType.value)
.isEqualTo(BigDecimal.valueOf(0.01))
}

@Test
fun dateTimePicker_shouldShowErrorForWrongDate() {
buildFragmentFromQuestionnaire("/component_date_time_picker.json")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,10 @@ class QuestionnaireFragment : Fragment() {
args.add(EXTRA_QUESTIONNAIRE_RESPONSE_JSON_URI to questionnaireResponseUri)
}

fun setQuestionnaireResourceContext(questionnaireResourceContext: String) = apply {
args.add(EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_JSON_STRING to questionnaireResourceContext)
}

/**
* An [Boolean] extra to control if the questionnaire is read-only. If review page and read-only
* are both enabled, read-only will take precedence.
Expand Down Expand Up @@ -385,6 +389,9 @@ class QuestionnaireFragment : Fragment() {
*/
internal const val EXTRA_QUESTIONNAIRE_RESPONSE_JSON_STRING = "questionnaire-response"

/** A JSON encoded string extra for questionnaire context. */
internal const val EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_JSON_STRING =
"questionnaire-launch-context"
/**
* A [URI][android.net.Uri] extra for streaming a JSON encoded questionnaire response.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import ca.uhn.fhir.context.FhirContext
import ca.uhn.fhir.context.FhirVersionEnum
import ca.uhn.fhir.parser.IParser
import com.google.android.fhir.datacapture.enablement.EnablementEvaluator
import com.google.android.fhir.datacapture.extensions.EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT
import com.google.android.fhir.datacapture.extensions.EntryMode
import com.google.android.fhir.datacapture.extensions.addNestedItemsToAnswer
import com.google.android.fhir.datacapture.extensions.allItems
Expand All @@ -41,7 +42,11 @@ import com.google.android.fhir.datacapture.extensions.isHidden
import com.google.android.fhir.datacapture.extensions.isPaginated
import com.google.android.fhir.datacapture.extensions.isXFhirQuery
import com.google.android.fhir.datacapture.extensions.localizedTextSpanned
import com.google.android.fhir.datacapture.extensions.packRepeatedGroups
import com.google.android.fhir.datacapture.extensions.shouldHaveNestedItemsUnderAnswers
import com.google.android.fhir.datacapture.extensions.unpackRepeatedGroups
import com.google.android.fhir.datacapture.extensions.validateLaunchContext
import com.google.android.fhir.datacapture.fhirpath.ExpressionEvaluator
import com.google.android.fhir.datacapture.fhirpath.ExpressionEvaluator.detectExpressionCyclicDependency
import com.google.android.fhir.datacapture.fhirpath.ExpressionEvaluator.evaluateCalculatedExpressions
import com.google.android.fhir.datacapture.fhirpath.fhirPathEngine
Expand All @@ -66,6 +71,7 @@ import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemComponent
import org.hl7.fhir.r4.model.QuestionnaireResponse
import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent
import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseItemComponent
import org.hl7.fhir.r4.model.Resource
import org.hl7.fhir.r4.model.ResourceType
import org.hl7.fhir.r4.model.ValueSet
import timber.log.Timber
Expand Down Expand Up @@ -143,6 +149,33 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
}
}
}
questionnaireResponse.packRepeatedGroups()
}

/**
* The launch context allows information to be passed into questionnaire based on the context in
* which he questionnaire is being evaluated. For example, what patient, what encounter, what
* user, etc. is "in context" at the time the questionnaire response is being completed.
* Currently, we support at most one launch context.The supported launch contexts are defined in:
* https://build.fhir.org/ig/HL7/sdc/StructureDefinition-sdc-questionnaire-launchContext.html
*/
private val questionnaireLaunchContext: Resource?

init {
questionnaireLaunchContext =
if (state.contains(QuestionnaireFragment.EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_JSON_STRING)) {
val questionnaireLaunchContextJson: String =
state[QuestionnaireFragment.EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_JSON_STRING]!!
questionnaire.extension
.firstOrNull { it.url == EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT }
?.let {
val resource = parser.parseResource(questionnaireLaunchContextJson) as Resource
validateLaunchContext(it, resource.resourceType.name)
resource
}
} else {
null
}
}

/** The map from each item in the [Questionnaire] to its parent. */
Expand Down Expand Up @@ -305,7 +338,15 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
*/
fun getQuestionnaireResponse(): QuestionnaireResponse {
return questionnaireResponse.copy().apply {
item = getEnabledResponseItems(this@QuestionnaireViewModel.questionnaire.item, item)
// Use the view model's questionnaire and questionnaire response for calculating enabled items
// because the calculation relies on references to the questionnaire response items.
item =
getEnabledResponseItems(
this@QuestionnaireViewModel.questionnaire.item,
questionnaireResponse.item
)
.map { it.copy() }
unpackRepeatedGroups(this@QuestionnaireViewModel.questionnaire)
}
}

Expand Down Expand Up @@ -501,7 +542,10 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
checkNotNull(xFhirQueryResolver) {
"XFhirQueryResolver cannot be null. Please provide the XFhirQueryResolver via DataCaptureConfig."
}
xFhirQueryResolver!!.resolve(expression.expression)

val xFhirExpressionString =
ExpressionEvaluator.createXFhirQueryFromExpression(expression, questionnaireLaunchContext)
xFhirQueryResolver!!.resolve(xFhirExpressionString)
} else if (expression.isFhirPath) {
fhirPathEngine.evaluate(questionnaireResponse, expression.expression)
} else {
Expand Down Expand Up @@ -728,60 +772,18 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
questionnaireResponseItem,
)
}
.flatMap { (questionnaireItem, questionnaireResponseItem) ->
val isRepeatedGroup =
questionnaireItem.type == Questionnaire.QuestionnaireItemType.GROUP &&
questionnaireItem.repeats
if (isRepeatedGroup) {
createRepeatedGroupResponse(questionnaireItem, questionnaireResponseItem)
} else {
listOf(
questionnaireResponseItem.apply {
text = questionnaireItem.localizedTextSpanned?.toString()
// Nested group items
item = getEnabledResponseItems(questionnaireItem.item, questionnaireResponseItem.item)
// Nested question items
answer.forEach { it.item = getEnabledResponseItems(questionnaireItem.item, it.item) }
}
)
.map { (questionnaireItem, questionnaireResponseItem) ->
questionnaireResponseItem.apply {
text = questionnaireItem.localizedTextSpanned?.toString()
// Nested group items
item = getEnabledResponseItems(questionnaireItem.item, questionnaireResponseItem.item)
// Nested question items
answer.forEach { it.item = getEnabledResponseItems(questionnaireItem.item, it.item) }
}
}
.toList()
}

/**
* Repeated groups need some massaging for their returned data-format; each instance of the group
* should be flattened out to be its own item in the parent, rather than an answer to the main
* item. See discussion:
* http://community.fhir.org/t/questionnaire-repeating-groups-what-is-the-correct-format/2276/3
*
* For example, if the group contains 2 questions, and the user answered the group 3 times, this
* function will return a list with 3 responses; each of those responses will have the linkId of
* the provided group, and each will contain an item array with 2 items (the answers to the
* individual questions within this particular group instance).
*/
private fun createRepeatedGroupResponse(
questionnaireItem: QuestionnaireItemComponent,
questionnaireResponseItem: QuestionnaireResponseItemComponent,
): List<QuestionnaireResponseItemComponent> {
val individualQuestions = questionnaireItem.item
return questionnaireResponseItem.answer.map { repeatedGroupInstance ->
val responsesToIndividualQuestions = repeatedGroupInstance.item
check(responsesToIndividualQuestions.size == individualQuestions.size) {
"Repeated groups responses must have the same # of responses as the group has questions"
}
QuestionnaireResponseItemComponent().apply {
linkId = questionnaireItem.linkId
text = questionnaireItem.localizedTextSpanned?.toString()
item =
getEnabledResponseItems(
questionnaireItemList = individualQuestions,
questionnaireResponseItemList = responsesToIndividualQuestions,
)
}
}
}

/**
* Gets a list of [QuestionnairePage]s for a paginated questionnaire, or `null` if the
* questionnaire is not paginated.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ internal class EnablementEvaluator(val questionnaireResponse: QuestionnaireRespo
questionnaireResponseItemParentMap[child] = item
buildParentList(child)
}
for (answer in item.answer) {
for (nestedItem in answer.item) {
buildParentList(nestedItem)
}
}
}

for (item in questionnaireResponse.item) {
Expand Down
Loading

0 comments on commit 4a68b78

Please sign in to comment.