Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/master' into 1764-scroll-to-bo…
Browse files Browse the repository at this point in the history
…ttom-to-enable-next
  • Loading branch information
LZRS committed Mar 21, 2024
2 parents b2db3d9 + bb19c19 commit e62105f
Show file tree
Hide file tree
Showing 40 changed files with 1,361 additions and 451 deletions.
15 changes: 12 additions & 3 deletions build-docs.bash
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,17 @@

set -euox pipefail

rm -rf site/
mkdir -p site/

# TODO Replace this with mkdocs generation
# TODO Fix https://github.com/google/android-fhir/issues/2237 and generate API JavaDoc
cp -R docs/* site/
# TODO https://github.com/google/android-fhir/issues/2232 Add mkdocs generation

./gradlew dokkaHtml
mkdir -p site/api/
mv docs/data-capture site/api/
mv docs/engine site/api/
mv docs/knowledge site/api/
mv docs/workflow site/api/

cp -R docs/index.html site/

4 changes: 2 additions & 2 deletions buildSrc/src/main/kotlin/Plugins.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2023 Google LLC
* Copyright 2023-2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -46,6 +46,6 @@ object Plugins {
object Versions {
const val androidGradlePlugin = "8.0.2"
const val benchmarkPlugin = "1.1.0"
const val dokka = "1.7.20"
const val dokka = "1.9.20"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -246,12 +246,14 @@ class QuestionnaireUiEspressoTest {
.perform(ViewActions.typeTextIntoFocusedView("01052005"))

onView(withId(R.id.time_input_layout)).perform(clickIcon(true))
clickOnText("AM")
clickOnText("6")
clickOnText("10")
clickOnText("OK")

runTest {
val answer = getQuestionnaireResponse().item.first().answer.first().valueDateTimeType
// check Locale
assertThat(answer.localDateTime).isEqualTo(LocalDateTime.of(2005, 1, 5, 6, 10))
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,14 @@ import com.google.android.fhir.datacapture.extensions.filterByCodeInNameExtensio
import com.google.android.fhir.datacapture.extensions.flattened
import com.google.android.fhir.datacapture.extensions.hasDifferentAnswerSet
import com.google.android.fhir.datacapture.extensions.isDisplayItem
import com.google.android.fhir.datacapture.extensions.isFhirPath
import com.google.android.fhir.datacapture.extensions.isHelpCode
import com.google.android.fhir.datacapture.extensions.isHidden
import com.google.android.fhir.datacapture.extensions.isPaginated
import com.google.android.fhir.datacapture.extensions.localizedTextSpanned
import com.google.android.fhir.datacapture.extensions.maxValue
import com.google.android.fhir.datacapture.extensions.maxValueCqfCalculatedValueExpression
import com.google.android.fhir.datacapture.extensions.minValue
import com.google.android.fhir.datacapture.extensions.minValueCqfCalculatedValueExpression
import com.google.android.fhir.datacapture.extensions.packRepeatedGroups
import com.google.android.fhir.datacapture.extensions.questionnaireLaunchContexts
import com.google.android.fhir.datacapture.extensions.shouldHaveNestedItemsUnderAnswers
Expand Down Expand Up @@ -69,8 +72,6 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.flow.withIndex
import kotlinx.coroutines.launch
import org.hl7.fhir.r4.model.Base
import org.hl7.fhir.r4.model.Element
import org.hl7.fhir.r4.model.Questionnaire
import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemComponent
import org.hl7.fhir.r4.model.QuestionnaireResponse
Expand Down Expand Up @@ -616,23 +617,6 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
}
}

private suspend fun resolveCqfExpression(
questionnaireItem: QuestionnaireItemComponent,
questionnaireResponseItem: QuestionnaireResponseItemComponent,
element: Element,
): List<Base> {
val cqfExpression = element.cqfExpression ?: return emptyList()

if (!cqfExpression.isFhirPath) {
throw UnsupportedOperationException("${cqfExpression.language} not supported yet")
}
return expressionEvaluator.evaluateExpression(
questionnaireItem,
questionnaireResponseItem,
cqfExpression,
)
}

private fun removeDisabledAnswers(
questionnaireItem: QuestionnaireItemComponent,
questionnaireResponseItem: QuestionnaireResponseItemComponent,
Expand Down Expand Up @@ -852,17 +836,25 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
questionnaireItem,
questionnaireResponseItem.answer,
this@QuestionnaireViewModel.getApplication(),
)
) {
expressionEvaluator.evaluateExpressionValue(
questionnaireItem,
questionnaireResponseItem,
it,
)
}
} else {
NotValidated
}

// Set question text dynamically from CQL expression
questionnaireResponseItem.apply {
resolveCqfExpression(questionnaireItem, this, questionnaireItem.textElement)
.firstOrNull()
?.let { text = it.primitiveValue() }
questionnaireItem.textElement.cqfExpression?.let { expression ->
expressionEvaluator
.evaluateExpressionValue(questionnaireItem, questionnaireResponseItem, expression)
?.primitiveValue()
?.let { questionnaireResponseItem.text = it }
}

val (enabledQuestionnaireAnswerOptions, disabledQuestionnaireResponseAnswers) =
answerOptionsEvaluator.evaluate(
questionnaireItem,
Expand All @@ -889,6 +881,24 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
validationResult = validationResult,
answersChangedCallback = answersChangedCallback,
enabledAnswerOptions = enabledQuestionnaireAnswerOptions,
minAnswerValue =
questionnaireItem.minValueCqfCalculatedValueExpression?.let {
expressionEvaluator.evaluateExpressionValue(
questionnaireItem,
questionnaireResponseItem,
it,
)
}
?: questionnaireItem.minValue,
maxAnswerValue =
questionnaireItem.maxValueCqfCalculatedValueExpression?.let {
expressionEvaluator.evaluateExpressionValue(
questionnaireItem,
questionnaireResponseItem,
it,
)
}
?: questionnaireItem.maxValue,
draftAnswer = draftAnswerMap[questionnaireResponseItem],
enabledDisplayItems =
questionnaireItem.item.filter {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import ca.uhn.fhir.util.UrlUtil
import com.google.android.fhir.datacapture.DataCapture
import com.google.android.fhir.datacapture.QuestionnaireViewHolderType
import com.google.android.fhir.datacapture.fhirpath.evaluateToDisplay
import com.google.android.fhir.datacapture.validation.MAX_VALUE_EXTENSION_URL
import com.google.android.fhir.datacapture.validation.MIN_VALUE_EXTENSION_URL
import com.google.android.fhir.getLocalizedText
import java.math.BigDecimal
import java.time.LocalDate
Expand Down Expand Up @@ -82,9 +84,6 @@ internal const val EXTENSION_CHOICE_ORIENTATION_URL =
internal const val EXTENSION_CHOICE_COLUMN_URL: String =
"http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-choiceColumn"

internal const val EXTENSION_CQF_CALCULATED_VALUE_URL: String =
"http://hl7.org/fhir/StructureDefinition/cqf-calculatedValue"

internal const val EXTENSION_DISPLAY_CATEGORY_URL =
"http://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory"

Expand Down Expand Up @@ -294,6 +293,18 @@ val Questionnaire.QuestionnaireItemComponent.sliderStepValue: Int?
return null
}

internal val Questionnaire.QuestionnaireItemComponent.minValue
get() = getExtensionByUrl(MIN_VALUE_EXTENSION_URL)?.value

internal val Questionnaire.QuestionnaireItemComponent.minValueCqfCalculatedValueExpression
get() = getExtensionByUrl(MIN_VALUE_EXTENSION_URL)?.value?.cqfCalculatedValueExpression

internal val Questionnaire.QuestionnaireItemComponent.maxValue
get() = getExtensionByUrl(MAX_VALUE_EXTENSION_URL)?.value

internal val Questionnaire.QuestionnaireItemComponent.maxValueCqfCalculatedValueExpression
get() = getExtensionByUrl(MAX_VALUE_EXTENSION_URL)?.value?.cqfCalculatedValueExpression

// ********************************************************************************************** //
// //
// Additional display utilities: display item control, localized text spanned, //
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2023 Google LLC
* Copyright 2023-2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -94,7 +94,7 @@ private fun validateLaunchContextExtension(launchExtension: Extension) {
false
}

if (nameCoding.system != EXTENSION_LAUNCH_CONTEXT || !isValidResourceType) {
if (nameCoding.system != CODE_SYSTEM_LAUNCH_CONTEXT || !isValidResourceType) {
error(
"The extension:name and/or extension:type do not follow the format specified in $EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT",
)
Expand Down Expand Up @@ -139,7 +139,8 @@ internal const val EXTENSION_ENTRY_MODE_URL: String =
internal const val EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT =
"http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-launchContext"

internal const val EXTENSION_LAUNCH_CONTEXT = "http://hl7.org/fhir/uv/sdc/CodeSystem/launchContext"
internal const val CODE_SYSTEM_LAUNCH_CONTEXT =
"http://hl7.org/fhir/uv/sdc/CodeSystem/launchContext"

val Questionnaire.entryMode: EntryMode?
get() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ package com.google.android.fhir.datacapture.extensions

import android.content.Context
import com.google.android.fhir.datacapture.R
import com.google.android.fhir.datacapture.fhirpath.evaluateToBase
import com.google.android.fhir.datacapture.views.factories.localDate
import com.google.android.fhir.datacapture.views.factories.localTime
import com.google.android.fhir.getLocalizedText
Expand Down Expand Up @@ -95,13 +94,13 @@ private fun getDisplayString(type: Type, context: Context): String? =
else -> (type as? PrimitiveType<*>)?.valueAsString
}

/**
* Returns the string representation for [PrimitiveType] or [Quantity], otherwise defaults to null
*/
private fun getValueString(type: Type): String? =
when (type) {
is DateType,
is DateTimeType,
is StringType, -> type.asStringValue()
is Quantity -> type.value.toString()
else -> (type as? PrimitiveType<*>)?.valueAsString
is Quantity -> type.value?.toString()
else -> (type as? PrimitiveType<*>)?.asStringValue()
}

/** Converts StringType to toUriType. */
Expand Down Expand Up @@ -132,16 +131,10 @@ internal fun Quantity.toCoding(): Coding {
return Coding(this.system, this.code, this.unit)
}

fun Type.valueOrCalculateValue(): Type {
return if (this.hasExtension()) {
this.extension
.firstOrNull { it.url == EXTENSION_CQF_CALCULATED_VALUE_URL }
?.let { extension ->
val expression = (extension.value as Expression).expression
evaluateToBase(this, expression).singleOrNull()?.let { it as Type }
}
?: this
} else {
this
}
}
internal fun Type.hasValue(): Boolean = !getValueString(this).isNullOrBlank()

internal val Type.cqfCalculatedValueExpression
get() = this.getExtensionByUrl(EXTENSION_CQF_CALCULATED_VALUE_URL)?.value as? Expression

internal const val EXTENSION_CQF_CALCULATED_VALUE_URL: String =
"http://hl7.org/fhir/StructureDefinition/cqf-calculatedValue"
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,27 @@ internal class ExpressionEvaluator(
)
}

/**
* Returns single [Type] evaluation value result of an expression, including cqf-expression and
* cqf-calculatedValue expressions
*/
suspend fun evaluateExpressionValue(
questionnaireItem: QuestionnaireItemComponent,
questionnaireResponseItem: QuestionnaireResponseItemComponent?,
expression: Expression,
): Type? {
if (!expression.isFhirPath) {
throw UnsupportedOperationException("${expression.language} not supported yet")
}
return try {
evaluateExpression(questionnaireItem, questionnaireResponseItem, expression).singleOrNull()
as? Type
} catch (e: Exception) {
Timber.w("Could not evaluate expression ${expression.expression} with FHIRPathEngine", e)
null
}
}

/**
* Returns a list of pair of item and the calculated and evaluated value for all items with
* calculated expression extension, which is dependent on value of updated response
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2022-2023 Google LLC
* Copyright 2022-2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -24,7 +24,6 @@ import org.hl7.fhir.r4.model.ExpressionNode
import org.hl7.fhir.r4.model.QuestionnaireResponse
import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseItemComponent
import org.hl7.fhir.r4.model.Resource
import org.hl7.fhir.r4.model.Type
import org.hl7.fhir.r4.utils.FHIRPathEngine

private val fhirPathEngine: FHIRPathEngine =
Expand Down Expand Up @@ -101,9 +100,9 @@ internal fun evaluateToBase(
}

/** Evaluates the given expression and returns list of [Base] */
internal fun evaluateToBase(type: Type, expression: String): List<Base> {
internal fun evaluateToBase(base: Base, expression: String): List<Base> {
return fhirPathEngine.evaluate(
/* base = */ type,
/* base = */ base,
/* path = */ expression,
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2022-2023 Google LLC
* Copyright 2022-2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -17,8 +17,10 @@
package com.google.android.fhir.datacapture.validation

import android.content.Context
import org.hl7.fhir.r4.model.Expression
import org.hl7.fhir.r4.model.Questionnaire
import org.hl7.fhir.r4.model.QuestionnaireResponse
import org.hl7.fhir.r4.model.Type

/**
* Validates [QuestionnaireResponse.QuestionnaireResponseItemComponent] against a particular
Expand All @@ -35,10 +37,11 @@ internal interface AnswerConstraintValidator {
*
* [Learn more](https://www.hl7.org/fhir/questionnaireresponse.html#link).
*/
fun validate(
suspend fun validate(
questionnaireItem: Questionnaire.QuestionnaireItemComponent,
answer: QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent,
context: Context,
expressionEvaluator: suspend (Expression) -> Type?,
): Result

/**
Expand Down
Loading

0 comments on commit e62105f

Please sign in to comment.