Skip to content

Commit 3da3bef

Browse files
FikriMilanodubdabasodubajingtang10
authored
enable when expression can access variable (#2132)
* 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 <dubdabasoduba@gmail.com> Co-authored-by: Jing Tang <jingtang@google.com>
1 parent bc5cf95 commit 3da3bef

File tree

14 files changed

+550
-334
lines changed

14 files changed

+550
-334
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
{
2+
"resourceType": "Questionnaire",
3+
"extension": [
4+
{
5+
"url": "http://hl7.org/fhir/StructureDefinition/variable",
6+
"valueExpression": {
7+
"name": "has-fever",
8+
"language": "text/fhirpath",
9+
"expression": "%resource.descendants().where(linkId='1').answer.value"
10+
}
11+
}
12+
],
13+
"item": [
14+
{
15+
"linkId": "1",
16+
"type": "boolean",
17+
"text": "Does patient has fever?",
18+
"item": [
19+
{
20+
"extension": [
21+
{
22+
"url": "http://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory",
23+
"valueCodeableConcept": {
24+
"coding": [
25+
{
26+
"system": "http://hl7.org/fhir/questionnaire-display-category",
27+
"code": "instructions"
28+
}
29+
]
30+
}
31+
}
32+
],
33+
"linkId": "1.1",
34+
"text": "Define the questionnaire variable 'has-fever' based on the answer to the question 'Does the patient have a fever?",
35+
"type": "display"
36+
}
37+
]
38+
},
39+
{
40+
"linkId": "2",
41+
"text": "Since when?",
42+
"type": "date",
43+
"extension": [
44+
{
45+
"url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-enableWhenExpression",
46+
"valueExpression": {
47+
"language": "text/fhirpath",
48+
"expression": "%has-fever"
49+
}
50+
}
51+
],
52+
"item": [
53+
{
54+
"extension": [
55+
{
56+
"url": "http://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory",
57+
"valueCodeableConcept": {
58+
"coding": [
59+
{
60+
"system": "http://hl7.org/fhir/questionnaire-display-category",
61+
"code": "instructions"
62+
}
63+
]
64+
}
65+
}
66+
],
67+
"linkId": "2.1",
68+
"text": "Enabled if variable 'has-fever' evaluates to true",
69+
"type": "display"
70+
}
71+
]
72+
}
73+
]
74+
}

catalog/src/main/java/com/google/android/fhir/catalog/BehaviorListViewModel.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2022 Google LLC
2+
* Copyright 2022-2023 Google LLC
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -48,6 +48,11 @@ class BehaviorListViewModel(application: Application) : AndroidViewModel(applica
4848
R.string.behavior_name_skip_logic,
4949
"behavior_skip_logic.json"
5050
),
51+
SKIP_LOGIC_WITH_EXPRESSION(
52+
R.drawable.ic_skiplogic_behavior,
53+
R.string.behavior_name_skip_logic_with_expression,
54+
"behavior_skip_logic_with_expression.json"
55+
),
5156
DYNAMIC_QUESTION_TEXT(
5257
R.drawable.ic_dynamic_text_behavior,
5358
R.string.behavior_name_dynamic_question_text,

catalog/src/main/res/values/strings.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@
4141
<string name="layout_name_review">Review</string>
4242
<string name="layout_name_read_only">Read only</string>
4343
<string name="behavior_name_skip_logic">Skip logic</string>
44+
<string
45+
name="behavior_name_skip_logic_with_expression"
46+
>Skip logic with expression</string>
4447
<string
4548
name="behavior_name_skip_logic_info"
4649
>If Yes is selected, a follow-up question is displayed. If No is selected, no follow-up questions are displayed.</string>

datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt

Lines changed: 43 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,7 @@ import com.google.android.fhir.datacapture.extensions.shouldHaveNestedItemsUnder
4646
import com.google.android.fhir.datacapture.extensions.unpackRepeatedGroups
4747
import com.google.android.fhir.datacapture.extensions.validateLaunchContextExtensions
4848
import com.google.android.fhir.datacapture.extensions.zipByLinkId
49-
import com.google.android.fhir.datacapture.fhirpath.ExpressionEvaluator.detectExpressionCyclicDependency
50-
import com.google.android.fhir.datacapture.fhirpath.ExpressionEvaluator.evaluateCalculatedExpressions
51-
import com.google.android.fhir.datacapture.fhirpath.ExpressionEvaluator.evaluateExpression
49+
import com.google.android.fhir.datacapture.fhirpath.ExpressionEvaluator
5250
import com.google.android.fhir.datacapture.validation.Invalid
5351
import com.google.android.fhir.datacapture.validation.NotValidated
5452
import com.google.android.fhir.datacapture.validation.QuestionnaireResponseItemValidator
@@ -335,12 +333,30 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
335333
modificationCount.update { it + 1 }
336334
}
337335

336+
private val expressionEvaluator: ExpressionEvaluator =
337+
ExpressionEvaluator(
338+
questionnaire,
339+
questionnaireResponse,
340+
questionnaireItemParentMap,
341+
questionnaireLaunchContextMap
342+
)
343+
344+
private val enablementEvaluator: EnablementEvaluator =
345+
EnablementEvaluator(
346+
questionnaire,
347+
questionnaireResponse,
348+
questionnaireItemParentMap,
349+
questionnaireLaunchContextMap
350+
)
351+
338352
private val answerOptionsEvaluator: EnabledAnswerOptionsEvaluator =
339353
EnabledAnswerOptionsEvaluator(
340354
questionnaire,
341-
questionnaireLaunchContextMap,
355+
questionnaireResponse,
342356
xFhirQueryResolver,
343-
externalValueSetResolver
357+
externalValueSetResolver,
358+
questionnaireItemParentMap,
359+
questionnaireLaunchContextMap
344360
)
345361

346362
/**
@@ -404,7 +420,9 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
404420
QuestionnaireResponseValidator.validateQuestionnaireResponse(
405421
questionnaire,
406422
questionnaireResponse,
407-
getApplication()
423+
getApplication(),
424+
questionnaireItemParentMap,
425+
questionnaireLaunchContextMap,
408426
)
409427
.also { result ->
410428
if (result.values.flatten().filterIsInstance<Invalid>().isNotEmpty()) {
@@ -480,13 +498,14 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
480498
.withIndex()
481499
.onEach {
482500
if (it.index == 0) {
483-
detectExpressionCyclicDependency(questionnaire.item)
501+
expressionEvaluator.detectExpressionCyclicDependency(questionnaire.item)
484502
questionnaire.item.flattened().forEach { qItem ->
485503
updateDependentQuestionnaireResponseItems(
486504
qItem,
487505
questionnaireResponse.allItems.find { qrItem -> qrItem.linkId == qItem.linkId }
488506
)
489507
}
508+
modificationCount.update { count -> count + 1 }
490509
}
491510
}
492511
.map { it.value }
@@ -497,15 +516,13 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
497516
)
498517

499518
private fun updateDependentQuestionnaireResponseItems(
500-
updatedQuestionnaireItem: QuestionnaireItemComponent,
519+
questionnaireItem: QuestionnaireItemComponent,
501520
updatedQuestionnaireResponseItem: QuestionnaireResponseItemComponent?,
502521
) {
503-
evaluateCalculatedExpressions(
504-
updatedQuestionnaireItem,
522+
expressionEvaluator
523+
.evaluateCalculatedExpressions(
524+
questionnaireItem,
505525
updatedQuestionnaireResponseItem,
506-
questionnaire,
507-
questionnaireResponse,
508-
questionnaireItemParentMap
509526
)
510527
.forEach { (questionnaireItem, calculatedAnswers) ->
511528
// update all response item with updated values
@@ -538,13 +555,10 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
538555
if (!cqfExpression.isFhirPath) {
539556
throw UnsupportedOperationException("${cqfExpression.language} not supported yet")
540557
}
541-
return evaluateExpression(
542-
questionnaire,
543-
questionnaireResponse,
558+
return expressionEvaluator.evaluateExpression(
544559
questionnaireItem,
545560
questionnaireResponseItem,
546561
cqfExpression,
547-
questionnaireItemParentMap
548562
)
549563
}
550564

@@ -653,8 +667,10 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
653667
// Hidden questions should not get QuestionnaireItemViewItem instances
654668
if (questionnaireItem.isHidden) return emptyList()
655669
val enabled =
656-
EnablementEvaluator(questionnaireResponse)
657-
.evaluate(questionnaireItem, questionnaireResponseItem)
670+
enablementEvaluator.evaluate(
671+
questionnaireItem,
672+
questionnaireResponseItem,
673+
)
658674
// Disabled questions should not get QuestionnaireItemViewItem instances
659675
if (!enabled) {
660676
cacheDisabledQuestionnaireItemAnswers(questionnaireResponseItem)
@@ -688,8 +704,6 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
688704
answerOptionsEvaluator.evaluate(
689705
questionnaireItem,
690706
questionnaireResponseItem,
691-
questionnaireResponse,
692-
questionnaireItemParentMap
693707
)
694708
if (disabledQuestionnaireResponseAnswers.isNotEmpty()) {
695709
removeDisabledAnswers(
@@ -713,7 +727,10 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
713727
enabledDisplayItems =
714728
questionnaireItem.item.filter {
715729
it.isDisplayItem &&
716-
EnablementEvaluator(questionnaireResponse).evaluate(it, questionnaireResponseItem)
730+
enablementEvaluator.evaluate(
731+
it,
732+
questionnaireResponseItem,
733+
)
717734
},
718735
questionViewTextConfiguration =
719736
QuestionTextConfiguration(
@@ -790,7 +807,6 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
790807
questionnaireItemList: List<QuestionnaireItemComponent>,
791808
questionnaireResponseItemList: List<QuestionnaireResponseItemComponent>,
792809
): List<QuestionnaireResponseItemComponent> {
793-
val enablementEvaluator = EnablementEvaluator(questionnaireResponse)
794810
val responseItemKeys = questionnaireResponseItemList.map { it.linkId }
795811
return questionnaireItemList
796812
.asSequence()
@@ -828,11 +844,10 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
828844
->
829845
QuestionnairePage(
830846
index,
831-
EnablementEvaluator(questionnaireResponse)
832-
.evaluate(
833-
questionnaireItem,
834-
questionnaireResponseItem,
835-
),
847+
enablementEvaluator.evaluate(
848+
questionnaireItem,
849+
questionnaireResponseItem,
850+
),
836851
questionnaireItem.isHidden
837852
)
838853
}

datacapture/src/main/java/com/google/android/fhir/datacapture/enablement/EnablementEvaluator.kt

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2022 Google LLC
2+
* Copyright 2022-2023 Google LLC
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,10 +19,12 @@ package com.google.android.fhir.datacapture.enablement
1919
import com.google.android.fhir.compareTo
2020
import com.google.android.fhir.datacapture.extensions.allItems
2121
import com.google.android.fhir.datacapture.extensions.enableWhenExpression
22+
import com.google.android.fhir.datacapture.fhirpath.ExpressionEvaluator
2223
import com.google.android.fhir.datacapture.fhirpath.evaluateToBoolean
2324
import com.google.android.fhir.equals
2425
import org.hl7.fhir.r4.model.Questionnaire
2526
import org.hl7.fhir.r4.model.QuestionnaireResponse
27+
import org.hl7.fhir.r4.model.Resource
2628

2729
/**
2830
* Evaluator for the enablement status of a [Questionnaire.QuestionnaireItemComponent].
@@ -50,16 +52,39 @@ import org.hl7.fhir.r4.model.QuestionnaireResponse
5052
* is shown or hidden. However, it is also possible that only user interaction is enabled or
5153
* disabled (e.g. grayed out) with the [Questionnaire.QuestionnaireItemComponent] always shown.
5254
*
53-
* The evaluator does not track the changes in the `questionnaire` and `questionnaireResponse`.
54-
* Therefore, a new evaluator should be created if they were modified.
55+
* The evaluator works in the context of a Questionnaire and the corresponding
56+
* QuestionnaireResponse. It is the caller's responsibility to make sure to call the evaluator with
57+
* QuestionnaireItems and QuestionnaireResponseItems that belong to the Questionnaire and the
58+
* QuestionnaireResponse.
5559
*
5660
* For more information see
5761
* [Questionnaire.item.enableWhen](https://www.hl7.org/fhir/questionnaire-definitions.html#Questionnaire.item.enableWhen)
5862
* and
5963
* [Questionnaire.item.enableBehavior](https://www.hl7.org/fhir/questionnaire-definitions.html#Questionnaire.item.enableBehavior)
6064
* .
65+
*
66+
* @param questionnaire the [Questionnaire] where the expression belong to
67+
* @param questionnaireResponse the [QuestionnaireResponse] related to the [Questionnaire]
68+
* @param questionnaireItemParentMap the [Map] of items parent
69+
* @param questionnaireLaunchContextMap the [Map] of launchContext names to their resource values
6170
*/
62-
internal class EnablementEvaluator(val questionnaireResponse: QuestionnaireResponse) {
71+
internal class EnablementEvaluator(
72+
private val questionnaire: Questionnaire,
73+
private val questionnaireResponse: QuestionnaireResponse,
74+
private val questionnaireItemParentMap:
75+
Map<Questionnaire.QuestionnaireItemComponent, Questionnaire.QuestionnaireItemComponent> =
76+
emptyMap(),
77+
private val questionnaireLaunchContextMap: Map<String, Resource>? = emptyMap(),
78+
) {
79+
80+
private val expressionEvaluator =
81+
ExpressionEvaluator(
82+
questionnaire,
83+
questionnaireResponse,
84+
questionnaireItemParentMap,
85+
questionnaireLaunchContextMap
86+
)
87+
6388
/**
6489
* The pre-order traversal trace of the items in the [QuestionnaireResponse]. This essentially
6590
* represents the order in which all items are displayed in the UI.
@@ -95,6 +120,7 @@ internal class EnablementEvaluator(val questionnaireResponse: QuestionnaireRespo
95120
/**
96121
* Returns whether [questionnaireItem] should be enabled.
97122
*
123+
* @param questionnaireItem the corresponding questionnaire item.
98124
* @param questionnaireResponseItem the corresponding questionnaire response item.
99125
*/
100126
fun evaluate(
@@ -110,10 +136,16 @@ internal class EnablementEvaluator(val questionnaireResponse: QuestionnaireRespo
110136

111137
// Evaluate `enableWhenExpression`.
112138
if (enableWhenExpression != null && enableWhenExpression.hasExpression()) {
139+
val contextMap =
140+
expressionEvaluator.extractDependentVariables(
141+
questionnaireItem.enableWhenExpression!!,
142+
questionnaireItem,
143+
)
113144
return evaluateToBoolean(
114145
questionnaireResponse,
115146
questionnaireResponseItem,
116-
enableWhenExpression.expression
147+
enableWhenExpression.expression,
148+
contextMap,
117149
)
118150
}
119151

0 commit comments

Comments
 (0)