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 4 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_title.json
maimoonak marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
{
"resourceType": "Questionnaire",
"item": [
{
"text": "Choose vaccination administered",
"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": "bcg",
"display": "BCG - Bacillus Calmette Guerin Vaccine"
}
},
{
"valueCoding": {
"code": "opv 0",
"display": "OPV 0 - Oral Poliovirus Vaccine"
}
},
{
"valueCoding": {
"code": "penta 1",
"display": "Penta 1 - Pentavalent (DPT + Hep B + Hib) Vaccine"
}
},
{
"valueCoding": {
"code": "pcv 1",
"display": "PCV 1 - Pneumococcal Conjugate Vaccine"
}
}
]
},
{
"linkId": "2",
"required": true,
"text": "Vaccine Date",
"_text": {
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/cqf-expression",
"valueExpression": {
"language": "text/fhirpath",
"expression": "'Select '+ %resource.descendants().where(linkId = '1').answer.value.display + ' Date'"
}
}
]
},
"type": "date"
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ class BehaviorListViewModel(application: Application) : AndroidViewModel(applica
R.drawable.ic_skiplogic_behavior,
R.string.behavior_name_skip_logic,
"behavior_skip_logic.json"
),
DYNAMIC_QUESTION_TITLE(
R.drawable.ic_dynamic_title_behavior,
R.string.behavior_name_dynamic_question_title,
"behavior_dynamic_question_title.json"
)
}
}
30 changes: 30 additions & 0 deletions catalog/src/main/res/drawable/ic_dynamic_title_behavior.xml
maimoonak marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -43,6 +43,9 @@
<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>
<string
name="behavior_name_dynamic_question_title"
>Dynamic Question Title</string>
maimoonak marked this conversation as resolved.
Show resolved Hide resolved
<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
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 @@ -49,6 +50,7 @@ import com.google.android.fhir.datacapture.extensions.validateLaunchContext
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.evaluateToBase
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 @@ -64,7 +66,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 @@ -533,6 +537,20 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
return options
}

internal fun resolveCqfExpression(
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) {
maimoonak marked this conversation as resolved.
Show resolved Hide resolved
return evaluateToBase(
questionnaireResponse,
questionnaireResponseItem,
cqfExpression.expression
)
} else throw UnsupportedOperationException("${cqfExpression.language} not supported yet")
}

private suspend fun loadAnswerExpressionOptions(
item: QuestionnaireItemComponent,
expression: Expression,
Expand Down Expand Up @@ -685,6 +703,7 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
answersChangedCallback = answersChangedCallback,
resolveAnswerValueSet = { resolveAnswerValueSet(it) },
resolveAnswerExpression = { resolveAnswerExpression(it) },
resolveCqfExpression = { resolveCqfExpression(questionnaireResponseItem, it) },
draftAnswer = draftAnswerMap[questionnaireResponseItem],
enabledDisplayItems =
questionnaireItem.item.filter {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,18 @@

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)
}
maimoonak marked this conversation as resolved.
Show resolved Hide resolved

internal val Expression.isXFhirQuery: Boolean
get() = this.language == Expression.ExpressionLanguage.APPLICATION_XFHIRQUERY.toCode()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ package com.google.android.fhir.datacapture.fhirpath
import ca.uhn.fhir.context.FhirContext
import ca.uhn.fhir.context.FhirVersionEnum
import org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext
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.utils.FHIRPathEngine

Expand All @@ -34,3 +36,22 @@ internal val fhirPathEngine: FHIRPathEngine =
*/
internal fun evaluateToDisplay(expressions: List<String>, data: Resource) =
expressions.joinToString(" ") { fhirPathEngine.evaluateToString(data, it) }

/**
* Evaluates the expressions over list of resources [QuestionnaireResponse] and
* [QuestionnaireResponseItemComponent] and returns the resulting elements FhirPath supplements
* https://build.fhir.org/ig/HL7/sdc/expressions.html#fhirpath-supplements %resource =
* [QuestionnaireResponse] %context = [QuestionnaireResponseItemComponent]
*/
internal fun evaluateToBase(
questionnaireResponse: QuestionnaireResponse,
questionnaireResponseItemComponent: QuestionnaireResponseItemComponent,
expression: String
) =
fhirPathEngine.evaluate(
null,
questionnaireResponse,
null,
questionnaireResponseItemComponent,
expression
)
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import com.google.android.fhir.datacapture.extensions.getHeaderViewVisibility
import com.google.android.fhir.datacapture.extensions.initHelpViews
import com.google.android.fhir.datacapture.extensions.localizedInstructionsSpanned
import com.google.android.fhir.datacapture.extensions.localizedPrefixSpanned
import com.google.android.fhir.datacapture.extensions.localizedTextSpanned
import com.google.android.fhir.datacapture.extensions.updateTextAndVisibility

/** View for the prefix, question, and hint of a questionnaire item. */
Expand All @@ -49,7 +48,7 @@ internal class HeaderView(context: Context, attrs: AttributeSet?) : LinearLayout
questionnaireItem = questionnaireViewItem.questionnaireItem
)
prefix.updateTextAndVisibility(questionnaireViewItem.questionnaireItem.localizedPrefixSpanned)
question.updateTextAndVisibility(questionnaireViewItem.questionnaireItem.localizedTextSpanned)
question.updateTextAndVisibility(questionnaireViewItem.questionTitle)
maimoonak marked this conversation as resolved.
Show resolved Hide resolved
hint.updateTextAndVisibility(
questionnaireViewItem.enabledDisplayItems.localizedInstructionsSpanned
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,21 @@
package com.google.android.fhir.datacapture.views

import android.content.Context
import android.text.Spanned
import androidx.core.text.toSpanned
import androidx.recyclerview.widget.RecyclerView
import com.google.android.fhir.datacapture.R
import com.google.android.fhir.datacapture.extensions.answerExpression
import com.google.android.fhir.datacapture.extensions.cqfExpression
import com.google.android.fhir.datacapture.extensions.displayString
import com.google.android.fhir.datacapture.extensions.localizedTextSpanned
import com.google.android.fhir.datacapture.validation.NotValidated
import com.google.android.fhir.datacapture.validation.Valid
import com.google.android.fhir.datacapture.validation.ValidationResult
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
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.QuestionnaireResponse

Expand Down Expand Up @@ -80,6 +86,7 @@ data class QuestionnaireViewItem(
{
emptyList()
},
private val resolveCqfExpression: (Element) -> List<Base> = { emptyList() },
maimoonak marked this conversation as resolved.
Show resolved Hide resolved
internal val draftAnswer: Any? = null,
internal val enabledDisplayItems: List<Questionnaire.QuestionnaireItemComponent> = emptyList()
) {
Expand Down Expand Up @@ -199,6 +206,18 @@ data class QuestionnaireViewItem(
}
}

/**
* Fetches the question title that should be displayed to user. The title is evaluated from
* cqf-expression on textElement if exists, otherwise it is derived from translatable textElement
* property
*/
internal val questionTitle: Spanned? by lazy {
maimoonak marked this conversation as resolved.
Show resolved Hide resolved
questionnaireItem.textElement
.takeIf { it.cqfExpression != null }
?.let { resolveCqfExpression(it).firstOrNull()?.primitiveValue()?.toSpanned() }
?: questionnaireItem.localizedTextSpanned
}

/**
* Returns whether this [QuestionnaireViewItem] and the `other` [QuestionnaireViewItem] have the
* same [Questionnaire.QuestionnaireItemComponent] and
Expand All @@ -210,7 +229,8 @@ data class QuestionnaireViewItem(
*/
internal fun hasTheSameItem(other: QuestionnaireViewItem) =
questionnaireItem === other.questionnaireItem &&
questionnaireResponseItem === other.questionnaireResponseItem
questionnaireResponseItem === other.questionnaireResponseItem &&
questionTitle == other.questionTitle
maimoonak marked this conversation as resolved.
Show resolved Hide resolved
maimoonak marked this conversation as resolved.
Show resolved Hide resolved

/**
* Returns whether this [QuestionnaireViewItem] and the `other` [QuestionnaireViewItem] have the
Expand Down