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

Cqf expression implementation for dynamic question title #1959

Merged
merged 30 commits into from
May 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
9ab6672
CQF Expression implementation for question title
maimoonak Apr 7, 2023
6a8478d
Merge branch 'master' into cqf-expression
maimoonak Apr 7, 2023
553afdd
Merge branch 'master' into cqf-expression
maimoonak Apr 7, 2023
c8b7694
Fix issue with failing tests
maimoonak Apr 7, 2023
19115e2
Merge branch 'master' into cqf-expression
dubdabasoduba Apr 18, 2023
eef0576
Add tests | Fix variables expression logic issue
maimoonak Apr 19, 2023
98b9af7
Remove extra javadoc
maimoonak Apr 19, 2023
b26e1e5
Merge branch 'master' into cqf-expression
maimoonak Apr 20, 2023
806d66e
Merge branch 'master' into cqf-expression
maimoonak Apr 26, 2023
ffa68c8
Remove function call from questionItem and use response text for dyna…
maimoonak Apr 26, 2023
0409406
Update datacapture/src/main/java/com/google/android/fhir/datacapture/…
maimoonak Apr 27, 2023
668b58f
Update datacapture/src/main/java/com/google/android/fhir/datacapture/…
maimoonak Apr 27, 2023
9e21fec
Update catalog/src/main/res/values/strings.xml
maimoonak Apr 27, 2023
101b672
Update datacapture/src/main/java/com/google/android/fhir/datacapture/…
maimoonak Apr 27, 2023
85406da
rename catalog files | add tests | add to group view item | modify sa…
maimoonak Apr 27, 2023
8bdeba1
Merge branch 'master' into cqf-expression
maimoonak May 3, 2023
38c0707
Merge branch 'master' into cqf-expression
dubdabasoduba May 4, 2023
d836c38
Merge branch 'master' into cqf-expression
dubdabasoduba May 5, 2023
291e0a1
Update catalog/src/main/res/values/strings.xml
maimoonak May 12, 2023
38c0f4e
Update datacapture/src/main/java/com/google/android/fhir/datacapture/…
maimoonak May 12, 2023
4d5f5f1
Update datacapture/src/main/java/com/google/android/fhir/datacapture/…
maimoonak May 12, 2023
57c27f2
Update datacapture/src/main/java/com/google/android/fhir/datacapture/…
maimoonak May 12, 2023
8dba1e0
Update datacapture/src/main/java/com/google/android/fhir/datacapture/…
maimoonak May 12, 2023
50af003
Merge branch 'master' into cqf-expression
maimoonak May 12, 2023
83a7e80
Move code to MoreElements
maimoonak May 12, 2023
994c22a
Merge branch 'cqf-expression' of https://github.com/opensrp/android-f…
maimoonak May 12, 2023
df852af
Make expression eval generic method
maimoonak May 12, 2023
7930f31
Add android test for cqf
maimoonak May 12, 2023
2cd53a6
Merge branch 'master' into cqf-expression
maimoonak May 15, 2023
ddc0beb
Merge branch 'master' into cqf-expression
jingtang10 May 17, 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
68 changes: 68 additions & 0 deletions catalog/src/main/assets/behavior_dynamic_question_text.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
{
"resourceType": "Questionnaire",
"item": [
{
"text": "Choose an option below",
"type": "choice",
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl",
"valueCodeableConcept": {
"coding": [
{
"system": "http://hl7.org/fhir/questionnaire-item-control",
"code": "radio-button",
"display": "Radio Button"
}
]
}
}
],
"linkId": "1",
"required": true,
"answerOption": [
{
"valueCoding": {
"code": "option1",
"display": "First Option"
}
},
{
"valueCoding": {
"code": "option2",
"display": "Second Option"
}
},
{
"valueCoding": {
"code": "option3",
"display": "Third Option"
}
},
{
"valueCoding": {
"code": "option4",
"display": "Fourth Option"
}
}
]
},
{
"linkId": "2",
"required": true,
"text": "Option Date",
"_text": {
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/cqf-expression",
"valueExpression": {
"language": "text/fhirpath",
"expression": "'Provide \"'+ %resource.descendants().where(linkId = '1').answer.value.display + '\" Date'"
}
}
]
},
"type": "date"
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ class BehaviorListViewModel(application: Application) : AndroidViewModel(applica
R.drawable.ic_skiplogic_behavior,
R.string.behavior_name_skip_logic,
"behavior_skip_logic.json"
),
DYNAMIC_QUESTION_TEXT(
R.drawable.ic_dynamic_text_behavior,
R.string.behavior_name_dynamic_question_text,
"behavior_dynamic_question_text.json"
)
}
}
30 changes: 30 additions & 0 deletions catalog/src/main/res/drawable/ic_dynamic_text_behavior.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="194dp"
android:height="128dp"
android:viewportWidth="194"
android:viewportHeight="128"
>
<path
android:strokeWidth="1"
android:pathData="M135.5,27V101C135.5,101.828 134.828,102.5 134,102.5H60C59.172,102.5 58.5,101.828 58.5,101V27C58.5,26.172 59.172,25.5 60,25.5H97H134C134.828,25.5 135.5,26.172 135.5,27Z"
android:fillColor="#ffffff"
android:strokeColor="#8AB4F8"
/>
<path
android:pathData="M64,49C64,43.477 68.477,39 74,39L135,39V59L74,59C68.477,59 64,54.523 64,49V49Z"
android:fillColor="#D2E3FC"
/>
<path
android:pathData="M74,49m-10,0a10,10 0,1 1,20 0a10,10 0,1 1,-20 0"
android:fillColor="#1A73E8"
/>
<path
android:pathData="M125,79C125,84.523 120.523,89 115,89L59,89L59,69L115,69C120.523,69 125,73.477 125,79V79Z"
android:fillColor="#D2E3FC"
/>
<path
android:pathData="M115,79m-10,0a10,10 0,1 1,20 0a10,10 0,1 1,-20 0"
android:fillColor="#1A73E8"
/>
</vector>
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 @@ -44,6 +44,9 @@
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>
<string name="behavior_name_context_variables">Context variable</string>
<string
name="behavior_name_dynamic_question_text"
>Dynamic question text</string>
<string
name="behavior_name_calculated_expression_info"
>Input age to automatically calculate birthdate until birthdate is updated manually.</string>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{
"resourceType": "Questionnaire",
"item": [
{
"text": "Choose an option below",
"type": "choice",
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl",
"valueCodeableConcept": {
"coding": [
{
"system": "http://hl7.org/fhir/questionnaire-item-control",
"code": "radio-button",
"display": "Radio Button"
}
]
}
}
],
"linkId": "1",
"required": true,
"answerOption": [
{
"valueCoding": {
"code": "option1",
"display": "First Option"
}
},
{
"valueCoding": {
"code": "option2",
"display": "Second Option"
}
}
]
},
{
"linkId": "2",
"required": true,
"text": "Option Date",
"_text": {
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/cqf-expression",
"valueExpression": {
"language": "text/fhirpath",
"expression": "'Provide \"'+ %resource.descendants().where(linkId = '1').answer.value.display + '\" Date'"
}
}
]
},
"type": "date"
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.matcher.RootMatchers
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
Expand Down Expand Up @@ -454,6 +455,29 @@ class QuestionnaireUiEspressoTest {
}
}

@Test
fun cqfExpression_shouldSetText_withEvaluatedAnswer() {
buildFragmentFromQuestionnaire("/questionnaire_with_dynamic_question_text.json")

onView(CoreMatchers.allOf(withText("Option Date"))).check { view, _ ->
assertThat(view.id).isEqualTo(R.id.question)
}

onView(CoreMatchers.allOf(withText("Provide \"First Option\" Date"))).check { view, _ ->
assertThat(view).isNull()
}

onView(CoreMatchers.allOf(withText("First Option"))).perform(ViewActions.click())

onView(CoreMatchers.allOf(withText("Option Date"))).check { view, _ ->
assertThat(view).isNull()
}

onView(CoreMatchers.allOf(withText("Provide \"First Option\" Date"))).check { view, _ ->
assertThat(view.id).isEqualTo(R.id.question)
}
}

private fun buildFragmentFromQuestionnaire(fileName: String, isReviewMode: Boolean = false) {
val questionnaireJsonString = readFileFromAssets(fileName)
val questionnaireFragment =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ internal object DiffCallbacks {
newItem: QuestionnaireAdapterItem.Question,
): Boolean {
return oldItem.item.hasTheSameItem(newItem.item) &&
oldItem.item.hasTheSameAnswer(newItem.item) &&
oldItem.item.hasTheSameResponse(newItem.item) &&
oldItem.item.hasTheSameValidationResult(newItem.item)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import com.google.android.fhir.datacapture.extensions.EntryMode
import com.google.android.fhir.datacapture.extensions.addNestedItemsToAnswer
import com.google.android.fhir.datacapture.extensions.allItems
import com.google.android.fhir.datacapture.extensions.answerExpression
import com.google.android.fhir.datacapture.extensions.cqfExpression
import com.google.android.fhir.datacapture.extensions.createQuestionnaireResponseItem
import com.google.android.fhir.datacapture.extensions.entryMode
import com.google.android.fhir.datacapture.extensions.extractAnswerOptions
Expand All @@ -50,6 +51,7 @@ import com.google.android.fhir.datacapture.extensions.zipByLinkId
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.ExpressionEvaluator.evaluateExpression
import com.google.android.fhir.datacapture.fhirpath.fhirPathEngine
import com.google.android.fhir.datacapture.validation.Invalid
import com.google.android.fhir.datacapture.validation.NotValidated
Expand All @@ -65,7 +67,9 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import org.hl7.fhir.r4.model.Base
import org.hl7.fhir.r4.model.Coding
import org.hl7.fhir.r4.model.Element
import org.hl7.fhir.r4.model.Expression
import org.hl7.fhir.r4.model.Questionnaire
import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemComponent
Expand Down Expand Up @@ -317,7 +321,7 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
}
modifiedQuestionnaireResponseItemSet.add(questionnaireResponseItem)

updateDependentQuestionnaireResponseItems(questionnaireItem)
updateDependentQuestionnaireResponseItems(questionnaireItem, questionnaireResponseItem)

modificationCount.update { it + 1 }
}
Expand Down Expand Up @@ -439,17 +443,22 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
getQuestionnaireState()
.also { detectExpressionCyclicDependency(questionnaire.item) }
.also {
questionnaire.item.flattened().forEach {
updateDependentQuestionnaireResponseItems(it)
questionnaire.item.flattened().forEach { qItem ->
updateDependentQuestionnaireResponseItems(
qItem,
questionnaireResponse.allItems.find { it.linkId == qItem.linkId }
)
}
}
)

private fun updateDependentQuestionnaireResponseItems(
updatedQuestionnaireItem: QuestionnaireItemComponent,
updatedQuestionnaireResponseItem: QuestionnaireResponseItemComponent?,
) {
evaluateCalculatedExpressions(
updatedQuestionnaireItem,
updatedQuestionnaireResponseItem,
questionnaire,
questionnaireResponse,
questionnaireItemParentMap
Expand Down Expand Up @@ -534,6 +543,26 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
return options
}

private fun resolveCqfExpression(
questionnaireItem: QuestionnaireItemComponent,
questionnaireResponseItem: QuestionnaireResponseItemComponent,
element: Element,
maimoonak marked this conversation as resolved.
Show resolved Hide resolved
): List<Base> {
val cqfExpression = element.cqfExpression ?: return emptyList()

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

private suspend fun loadAnswerExpressionOptions(
item: QuestionnaireItemComponent,
expression: Expression,
Expand Down Expand Up @@ -675,6 +704,14 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
} else {
NotValidated
}

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

val items = buildList {
// Add an item for the question itself
add(
Expand Down Expand Up @@ -775,7 +812,9 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
}
.map { (questionnaireItem, questionnaireResponseItem) ->
questionnaireResponseItem.apply {
text = questionnaireItem.localizedTextSpanned?.toString()
if (text.isNullOrBlank()) {
text = questionnaireItem.localizedTextSpanned?.toString()
}
// Nested group items
item = getEnabledResponseItems(questionnaireItem.item, questionnaireResponseItem.item)
// Nested question items
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.android.fhir.datacapture.extensions

import org.hl7.fhir.r4.model.Element
import org.hl7.fhir.r4.model.Expression
import org.hl7.fhir.r4.utils.ToolingExtensions

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

internal val Element.cqfExpression: Expression?
get() =
ToolingExtensions.getExtension(this, EXTENSION_CQF_EXPRESSION_URL)?.value?.let {
it.castToExpression(it)
}
Loading