Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

enable when expression can access variable #2132

Merged
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
d73edd2
Provide proper contextMap when evaluating the following:
FikriMilano Aug 14, 2023
4d0d9e9
FOR PR TESTING ONLY
FikriMilano Aug 14, 2023
669aeac
Fix failing test
FikriMilano Aug 15, 2023
3ec78bd
Merge branch 'master' into 2096-variable-for-enable-when-expression
dubdabasoduba Aug 22, 2023
7e1952b
Rename questionnaireResource to questionnaire
FikriMilano Sep 4, 2023
0446d43
Revert component_dropdown.json
FikriMilano Sep 4, 2023
8429ae6
Add skip logic w expression to catalog
FikriMilano Sep 4, 2023
6545f72
Add trailing comas
FikriMilano Sep 4, 2023
cf06b67
Add default parameter value for maps and Questionnaire
FikriMilano Sep 4, 2023
cd974a9
spotlessApply
FikriMilano Sep 4, 2023
f49eb8e
Change method name to avoid conflict with questionnaireJson variable
FikriMilano Sep 4, 2023
c198e67
Refactor evaluators
FikriMilano Sep 6, 2023
035d173
Also tie enablementEvaluator lifecycle to viewmodel
FikriMilano Sep 6, 2023
0f321d6
get latest questionnaire state to see calculated expression result in UI
FikriMilano Sep 6, 2023
518c279
Remove unused log
FikriMilano Sep 6, 2023
17370e3
Fix quantity initial value not showing in catalog app
FikriMilano Sep 6, 2023
e1cfd3f
Update kdoc
FikriMilano Sep 6, 2023
cc95c5d
Remove old evaluateToBoolean
FikriMilano Sep 6, 2023
c44e49b
Address review
FikriMilano Sep 7, 2023
c437af3
Merge branch 'master' of github.com:google/android-fhir into 2096-var…
FikriMilano Sep 7, 2023
fb04421
Revert behavior_calculated_expression.json
FikriMilano Sep 8, 2023
bd0a687
Add named parameter comment
FikriMilano Sep 11, 2023
dbd232f
Merge branch 'master' of github.com:google/android-fhir into 2096-var…
FikriMilano Sep 11, 2023
b5a69ef
Update datacapture/src/main/java/com/google/android/fhir/datacapture/…
jingtang10 Sep 11, 2023
1b52e69
Update datacapture/src/main/java/com/google/android/fhir/datacapture/…
jingtang10 Sep 11, 2023
81de124
Update datacapture/src/test/java/com/google/android/fhir/datacapture/…
jingtang10 Sep 11, 2023
bd67a3c
Update datacapture/src/test/java/com/google/android/fhir/datacapture/…
jingtang10 Sep 11, 2023
e53ee15
Update datacapture/src/test/java/com/google/android/fhir/datacapture/…
jingtang10 Sep 11, 2023
59f2434
Update datacapture/src/test/java/com/google/android/fhir/datacapture/…
jingtang10 Sep 11, 2023
5e39c56
Update datacapture/src/test/java/com/google/android/fhir/datacapture/…
jingtang10 Sep 11, 2023
c572eed
Update datacapture/src/test/java/com/google/android/fhir/datacapture/…
jingtang10 Sep 11, 2023
3b24375
Update datacapture/src/test/java/com/google/android/fhir/datacapture/…
jingtang10 Sep 11, 2023
1ed2ba0
Update datacapture/src/test/java/com/google/android/fhir/datacapture/…
jingtang10 Sep 11, 2023
af9a2f6
Update datacapture/src/test/java/com/google/android/fhir/datacapture/…
jingtang10 Sep 11, 2023
66fa9fe
Update datacapture/src/test/java/com/google/android/fhir/datacapture/…
jingtang10 Sep 11, 2023
36ea59e
Update datacapture/src/test/java/com/google/android/fhir/datacapture/…
jingtang10 Sep 11, 2023
cba10f7
Update datacapture/src/test/java/com/google/android/fhir/datacapture/…
jingtang10 Sep 11, 2023
183e8ed
Update datacapture/src/test/java/com/google/android/fhir/datacapture/…
jingtang10 Sep 11, 2023
6051a58
Update datacapture/src/main/java/com/google/android/fhir/datacapture/…
jingtang10 Sep 11, 2023
92ffe98
Update datacapture/src/main/java/com/google/android/fhir/datacapture/…
jingtang10 Sep 11, 2023
7015953
Update datacapture/src/test/java/com/google/android/fhir/datacapture/…
jingtang10 Sep 11, 2023
bc8b853
Update datacapture/src/test/java/com/google/android/fhir/datacapture/…
jingtang10 Sep 11, 2023
70d2112
Spotless
jingtang10 Sep 11, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
FikriMilano marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
{
"resourceType": "Questionnaire",
"extension": [
{
"url": "http://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://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory",
"valueCodeableConcept": {
"coding": [
{
"system": "http://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://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-enableWhenExpression",
"valueExpression": {
"language": "text/fhirpath",
"expression": "%has-fever"
}
}
],
"item": [
{
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory",
"valueCodeableConcept": {
"coding": [
{
"system": "http://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(
FikriMilano marked this conversation as resolved.
Show resolved Hide resolved
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(
FikriMilano marked this conversation as resolved.
Show resolved Hide resolved
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