Skip to content

Commit

Permalink
enable when expression can access variable (google#2132)
Browse files Browse the repository at this point in the history
* Provide proper contextMap when evaluating the following:

- enableWhenExpression can access variablesMap and launchContextMap
- variableExpression can access launchContextMap

* FOR PR TESTING ONLY

* Fix failing test

* Rename questionnaireResource to questionnaire

* Revert component_dropdown.json

* Add skip logic w expression to catalog

* Add trailing comas

* Add default parameter value for maps and Questionnaire

* spotlessApply

* Change method name to avoid conflict with questionnaireJson variable

* Refactor evaluators

- ExpressionEvaluator, EnablementEvaluator, EnabledAnswerOptionsEvaluator.

- Moving the params from method to class constructor for easier use of methods by having less params.

* Also tie enablementEvaluator lifecycle to viewmodel

* get latest questionnaire state to see calculated expression result in UI

* Remove unused log

* Fix quantity initial value not showing in catalog app

Out of topic, my hands can't resist fixing this issue.

* Update kdoc

* Remove old evaluateToBoolean

* Address review

* Revert behavior_calculated_expression.json

Should be fixed in other PR, there is more issue w Date picker widget format that doesn't work properly because declared in the bind() function.

* Add named parameter comment

* Update datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/FhirPathUtil.kt

* Update datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluator.kt

* Update datacapture/src/test/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluatorTest.kt

* Update datacapture/src/test/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluatorTest.kt

* Update datacapture/src/test/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluatorTest.kt

* Update datacapture/src/test/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluatorTest.kt

* Update datacapture/src/test/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluatorTest.kt

* Update datacapture/src/test/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluatorTest.kt

* Update datacapture/src/test/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluatorTest.kt

* Update datacapture/src/test/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluatorTest.kt

* Update datacapture/src/test/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluatorTest.kt

* Update datacapture/src/test/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluatorTest.kt

* Update datacapture/src/test/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluatorTest.kt

* Update datacapture/src/test/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluatorTest.kt

* Update datacapture/src/test/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluatorTest.kt

* Update datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluator.kt

* Update datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluator.kt

* Update datacapture/src/test/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluatorTest.kt

* Update datacapture/src/test/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluatorTest.kt

* Spotless

---------

Co-authored-by: Benjamin Mwalimu <[email protected]>
Co-authored-by: Jing Tang <[email protected]>
  • Loading branch information
3 people committed Sep 11, 2023
1 parent bc5cf95 commit 3da3bef
Show file tree
Hide file tree
Showing 14 changed files with 550 additions and 334 deletions.
74 changes: 74 additions & 0 deletions catalog/src/main/assets/behavior_skip_logic_with_expression.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
{
"resourceType": "Questionnaire",
"extension": [
{
"url": "http:https://hl7.org/fhir/StructureDefinition/variable",
"valueExpression": {
"name": "has-fever",
"language": "text/fhirpath",
"expression": "%resource.descendants().where(linkId='1').answer.value"
}
}
],
"item": [
{
"linkId": "1",
"type": "boolean",
"text": "Does patient has fever?",
"item": [
{
"extension": [
{
"url": "http:https://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory",
"valueCodeableConcept": {
"coding": [
{
"system": "http:https://hl7.org/fhir/questionnaire-display-category",
"code": "instructions"
}
]
}
}
],
"linkId": "1.1",
"text": "Define the questionnaire variable 'has-fever' based on the answer to the question 'Does the patient have a fever?",
"type": "display"
}
]
},
{
"linkId": "2",
"text": "Since when?",
"type": "date",
"extension": [
{
"url": "http:https://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-enableWhenExpression",
"valueExpression": {
"language": "text/fhirpath",
"expression": "%has-fever"
}
}
],
"item": [
{
"extension": [
{
"url": "http:https://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory",
"valueCodeableConcept": {
"coding": [
{
"system": "http:https://hl7.org/fhir/questionnaire-display-category",
"code": "instructions"
}
]
}
}
],
"linkId": "2.1",
"text": "Enabled if variable 'has-fever' evaluates to true",
"type": "display"
}
]
}
]
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2022 Google LLC
* Copyright 2022-2023 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 @@ -48,6 +48,11 @@ class BehaviorListViewModel(application: Application) : AndroidViewModel(applica
R.string.behavior_name_skip_logic,
"behavior_skip_logic.json"
),
SKIP_LOGIC_WITH_EXPRESSION(
R.drawable.ic_skiplogic_behavior,
R.string.behavior_name_skip_logic_with_expression,
"behavior_skip_logic_with_expression.json"
),
DYNAMIC_QUESTION_TEXT(
R.drawable.ic_dynamic_text_behavior,
R.string.behavior_name_dynamic_question_text,
Expand Down
3 changes: 3 additions & 0 deletions catalog/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@
<string name="layout_name_review">Review</string>
<string name="layout_name_read_only">Read only</string>
<string name="behavior_name_skip_logic">Skip logic</string>
<string
name="behavior_name_skip_logic_with_expression"
>Skip logic with expression</string>
<string
name="behavior_name_skip_logic_info"
>If Yes is selected, a follow-up question is displayed. If No is selected, no follow-up questions are displayed.</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,7 @@ import com.google.android.fhir.datacapture.extensions.shouldHaveNestedItemsUnder
import com.google.android.fhir.datacapture.extensions.unpackRepeatedGroups
import com.google.android.fhir.datacapture.extensions.validateLaunchContextExtensions
import com.google.android.fhir.datacapture.extensions.zipByLinkId
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.ExpressionEvaluator.evaluateExpression
import com.google.android.fhir.datacapture.fhirpath.ExpressionEvaluator
import com.google.android.fhir.datacapture.validation.Invalid
import com.google.android.fhir.datacapture.validation.NotValidated
import com.google.android.fhir.datacapture.validation.QuestionnaireResponseItemValidator
Expand Down Expand Up @@ -335,12 +333,30 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
modificationCount.update { it + 1 }
}

private val expressionEvaluator: ExpressionEvaluator =
ExpressionEvaluator(
questionnaire,
questionnaireResponse,
questionnaireItemParentMap,
questionnaireLaunchContextMap
)

private val enablementEvaluator: EnablementEvaluator =
EnablementEvaluator(
questionnaire,
questionnaireResponse,
questionnaireItemParentMap,
questionnaireLaunchContextMap
)

private val answerOptionsEvaluator: EnabledAnswerOptionsEvaluator =
EnabledAnswerOptionsEvaluator(
questionnaire,
questionnaireLaunchContextMap,
questionnaireResponse,
xFhirQueryResolver,
externalValueSetResolver
externalValueSetResolver,
questionnaireItemParentMap,
questionnaireLaunchContextMap
)

/**
Expand Down Expand Up @@ -404,7 +420,9 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
QuestionnaireResponseValidator.validateQuestionnaireResponse(
questionnaire,
questionnaireResponse,
getApplication()
getApplication(),
questionnaireItemParentMap,
questionnaireLaunchContextMap,
)
.also { result ->
if (result.values.flatten().filterIsInstance<Invalid>().isNotEmpty()) {
Expand Down Expand Up @@ -480,13 +498,14 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
.withIndex()
.onEach {
if (it.index == 0) {
detectExpressionCyclicDependency(questionnaire.item)
expressionEvaluator.detectExpressionCyclicDependency(questionnaire.item)
questionnaire.item.flattened().forEach { qItem ->
updateDependentQuestionnaireResponseItems(
qItem,
questionnaireResponse.allItems.find { qrItem -> qrItem.linkId == qItem.linkId }
)
}
modificationCount.update { count -> count + 1 }
}
}
.map { it.value }
Expand All @@ -497,15 +516,13 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
)

private fun updateDependentQuestionnaireResponseItems(
updatedQuestionnaireItem: QuestionnaireItemComponent,
questionnaireItem: QuestionnaireItemComponent,
updatedQuestionnaireResponseItem: QuestionnaireResponseItemComponent?,
) {
evaluateCalculatedExpressions(
updatedQuestionnaireItem,
expressionEvaluator
.evaluateCalculatedExpressions(
questionnaireItem,
updatedQuestionnaireResponseItem,
questionnaire,
questionnaireResponse,
questionnaireItemParentMap
)
.forEach { (questionnaireItem, calculatedAnswers) ->
// update all response item with updated values
Expand Down Expand Up @@ -538,13 +555,10 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
if (!cqfExpression.isFhirPath) {
throw UnsupportedOperationException("${cqfExpression.language} not supported yet")
}
return evaluateExpression(
questionnaire,
questionnaireResponse,
return expressionEvaluator.evaluateExpression(
questionnaireItem,
questionnaireResponseItem,
cqfExpression,
questionnaireItemParentMap
)
}

Expand Down Expand Up @@ -653,8 +667,10 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
// Hidden questions should not get QuestionnaireItemViewItem instances
if (questionnaireItem.isHidden) return emptyList()
val enabled =
EnablementEvaluator(questionnaireResponse)
.evaluate(questionnaireItem, questionnaireResponseItem)
enablementEvaluator.evaluate(
questionnaireItem,
questionnaireResponseItem,
)
// Disabled questions should not get QuestionnaireItemViewItem instances
if (!enabled) {
cacheDisabledQuestionnaireItemAnswers(questionnaireResponseItem)
Expand Down Expand Up @@ -688,8 +704,6 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
answerOptionsEvaluator.evaluate(
questionnaireItem,
questionnaireResponseItem,
questionnaireResponse,
questionnaireItemParentMap
)
if (disabledQuestionnaireResponseAnswers.isNotEmpty()) {
removeDisabledAnswers(
Expand All @@ -713,7 +727,10 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
enabledDisplayItems =
questionnaireItem.item.filter {
it.isDisplayItem &&
EnablementEvaluator(questionnaireResponse).evaluate(it, questionnaireResponseItem)
enablementEvaluator.evaluate(
it,
questionnaireResponseItem,
)
},
questionViewTextConfiguration =
QuestionTextConfiguration(
Expand Down Expand Up @@ -790,7 +807,6 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
questionnaireItemList: List<QuestionnaireItemComponent>,
questionnaireResponseItemList: List<QuestionnaireResponseItemComponent>,
): List<QuestionnaireResponseItemComponent> {
val enablementEvaluator = EnablementEvaluator(questionnaireResponse)
val responseItemKeys = questionnaireResponseItemList.map { it.linkId }
return questionnaireItemList
.asSequence()
Expand Down Expand Up @@ -828,11 +844,10 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
->
QuestionnairePage(
index,
EnablementEvaluator(questionnaireResponse)
.evaluate(
questionnaireItem,
questionnaireResponseItem,
),
enablementEvaluator.evaluate(
questionnaireItem,
questionnaireResponseItem,
),
questionnaireItem.isHidden
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2022 Google LLC
* Copyright 2022-2023 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 @@ -19,10 +19,12 @@ package com.google.android.fhir.datacapture.enablement
import com.google.android.fhir.compareTo
import com.google.android.fhir.datacapture.extensions.allItems
import com.google.android.fhir.datacapture.extensions.enableWhenExpression
import com.google.android.fhir.datacapture.fhirpath.ExpressionEvaluator
import com.google.android.fhir.datacapture.fhirpath.evaluateToBoolean
import com.google.android.fhir.equals
import org.hl7.fhir.r4.model.Questionnaire
import org.hl7.fhir.r4.model.QuestionnaireResponse
import org.hl7.fhir.r4.model.Resource

/**
* Evaluator for the enablement status of a [Questionnaire.QuestionnaireItemComponent].
Expand Down Expand Up @@ -50,16 +52,39 @@ import org.hl7.fhir.r4.model.QuestionnaireResponse
* is shown or hidden. However, it is also possible that only user interaction is enabled or
* disabled (e.g. grayed out) with the [Questionnaire.QuestionnaireItemComponent] always shown.
*
* The evaluator does not track the changes in the `questionnaire` and `questionnaireResponse`.
* Therefore, a new evaluator should be created if they were modified.
* The evaluator works in the context of a Questionnaire and the corresponding
* QuestionnaireResponse. It is the caller's responsibility to make sure to call the evaluator with
* QuestionnaireItems and QuestionnaireResponseItems that belong to the Questionnaire and the
* QuestionnaireResponse.
*
* For more information see
* [Questionnaire.item.enableWhen](https://www.hl7.org/fhir/questionnaire-definitions.html#Questionnaire.item.enableWhen)
* and
* [Questionnaire.item.enableBehavior](https://www.hl7.org/fhir/questionnaire-definitions.html#Questionnaire.item.enableBehavior)
* .
*
* @param questionnaire the [Questionnaire] where the expression belong to
* @param questionnaireResponse the [QuestionnaireResponse] related to the [Questionnaire]
* @param questionnaireItemParentMap the [Map] of items parent
* @param questionnaireLaunchContextMap the [Map] of launchContext names to their resource values
*/
internal class EnablementEvaluator(val questionnaireResponse: QuestionnaireResponse) {
internal class EnablementEvaluator(
private val questionnaire: Questionnaire,
private val questionnaireResponse: QuestionnaireResponse,
private val questionnaireItemParentMap:
Map<Questionnaire.QuestionnaireItemComponent, Questionnaire.QuestionnaireItemComponent> =
emptyMap(),
private val questionnaireLaunchContextMap: Map<String, Resource>? = emptyMap(),
) {

private val expressionEvaluator =
ExpressionEvaluator(
questionnaire,
questionnaireResponse,
questionnaireItemParentMap,
questionnaireLaunchContextMap
)

/**
* The pre-order traversal trace of the items in the [QuestionnaireResponse]. This essentially
* represents the order in which all items are displayed in the UI.
Expand Down Expand Up @@ -95,6 +120,7 @@ internal class EnablementEvaluator(val questionnaireResponse: QuestionnaireRespo
/**
* Returns whether [questionnaireItem] should be enabled.
*
* @param questionnaireItem the corresponding questionnaire item.
* @param questionnaireResponseItem the corresponding questionnaire response item.
*/
fun evaluate(
Expand All @@ -110,10 +136,16 @@ internal class EnablementEvaluator(val questionnaireResponse: QuestionnaireRespo

// Evaluate `enableWhenExpression`.
if (enableWhenExpression != null && enableWhenExpression.hasExpression()) {
val contextMap =
expressionEvaluator.extractDependentVariables(
questionnaireItem.enableWhenExpression!!,
questionnaireItem,
)
return evaluateToBoolean(
questionnaireResponse,
questionnaireResponseItem,
enableWhenExpression.expression
enableWhenExpression.expression,
contextMap,
)
}

Expand Down
Loading

0 comments on commit 3da3bef

Please sign in to comment.