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

971 | calculated expression #5

Open
wants to merge 92 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
92 commits
Select commit Hold shift + click to select a range
7bf05ce
Implement calculated-expression extension
maimoonak May 11, 2022
b569b81
Fix form value update bug
maimoonak May 12, 2022
27aef40
Merge branch 'master' into 971_calc_exp
maimoonak May 12, 2022
d472190
Detect cyclic dependency | fix on init value loading
maimoonak May 12, 2022
c44e595
Merge branch '971_calc_exp' of https://github.com/opensrp/android-fhi…
maimoonak May 12, 2022
5536e96
Fix merge conflict
maimoonak May 12, 2022
bce9fd9
Make birthdate age dependent | Handle and fix quantity values
maimoonak May 12, 2022
777489b
Fix failing test
maimoonak May 12, 2022
1eafc31
Merge branch 'master' into 971_calc_exp
maimoonak May 12, 2022
0ee4c27
quantity viewholder delegate test covergae
maimoonak May 12, 2022
1f43305
Merge branch '971_calc_exp' of https://github.com/opensrp/android-fhi…
maimoonak May 12, 2022
3fe03fe
Test coverage for update flow
maimoonak May 13, 2022
8ae3161
spotless fix
maimoonak May 13, 2022
e56bd98
spotless fix | re-run ci
maimoonak May 13, 2022
d4d8e1f
Merge branch '971_calc_exp' of https://github.com/opensrp/android-fhi…
maimoonak May 13, 2022
1acf759
Merge branch 'master' into 971_calc_exp
maimoonak May 16, 2022
219dd9e
Test covergae for questionnaire fragment
maimoonak May 19, 2022
0fdb56b
Merge branch '971_calc_exp' of https://github.com/opensrp/android-fhi…
maimoonak May 19, 2022
b613865
Merge branch 'master' into 971_calc_exp
maimoonak May 19, 2022
5fff50d
test coverage for quantity types
maimoonak May 20, 2022
23ed2e3
questionnaire fragment test with launchInFragmentContainer
maimoonak May 20, 2022
5e41ae4
Merge branch 'master' into 971_calc_exp
maimoonak May 30, 2022
1f97b8d
Update datacapture/src/main/java/com/google/android/fhir/datacapture/…
maimoonak May 30, 2022
9e31819
Move widget to LayoutList | Run Calculation after state-change
maimoonak May 31, 2022
501f4ef
Revert the run-expression after state-flow
maimoonak May 31, 2022
a7b8709
Merge branch 'master' into 971_calc_exp
maimoonak Jun 1, 2022
39cebfa
Merge branch 'master' into 971_calc_exp
maimoonak Jun 1, 2022
8ed91d7
Merge branch 'master' into 971_calc_exp
maimoonak Jun 2, 2022
37fd56d
Merge branch 'master' into 971_calc_exp
maimoonak Jun 3, 2022
dddfd20
Merge branch 'master' into 971_calc_exp
maimoonak Jun 6, 2022
cda1f03
Merge branch 'master' into 971_calc_exp
maimoonak Jun 6, 2022
a5993ff
Merge branch 'master' into 971_calc_exp
dubdabasoduba Jun 7, 2022
b5e8352
Merge branch 'master' into 971_calc_exp
maimoonak Jun 8, 2022
58fb48e
Merge branch 'master' into 971_calc_exp
dubdabasoduba Jun 10, 2022
b566b28
Merge branch 'master' into 971_calc_exp
dubdabasoduba Jun 15, 2022
28f0c65
Merge branch 'master' into 971_calc_exp
dubdabasoduba Jun 16, 2022
284524b
Merge branch 'master' into 971_calc_exp
maimoonak Jun 20, 2022
2ce0055
Merge branch 'master' into 971_calc_exp
maimoonak Jun 30, 2022
bf956c5
Merge branch 'master' into 971_calc_exp
maimoonak Jul 4, 2022
3c227f2
Merge branch 'master' into 971_calc_exp
maimoonak Jul 4, 2022
0414961
remove empty line changes
maimoonak Jul 4, 2022
2406e26
Merge branch '971_calc_exp' of https://github.com/opensrp/android-fhi…
maimoonak Jul 4, 2022
bbda724
spotless fix
maimoonak Jul 4, 2022
1fd726c
Esperesso test | Fix failing test
maimoonak Jul 4, 2022
4c4f1ef
Merge branch 'master' into 971_calc_exp
maimoonak Jul 5, 2022
4793baf
Merge main and resolve conflicts
maimoonak Jul 20, 2022
d402a3f
Merge branch 'master' into 971_calc_exp
maimoonak Jul 20, 2022
e738864
Remove unnessary changes
maimoonak Jul 20, 2022
4b4aaaf
Fix espresso tests
maimoonak Jul 20, 2022
f1cb2bf
Merge branch '971_calc_exp' of https://github.com/opensrp/android-fhi…
maimoonak Jul 20, 2022
be654e1
Merge branch 'master' into 971_calc_exp
maimoonak Jul 20, 2022
c6032aa
Ignore Failing tests
maimoonak Jul 20, 2022
7b2b4bd
Merge branch 'master' into 971_calc_exp
maimoonak Jul 21, 2022
2c6a86f
Merge branch 'master' into 971_calc_exp
maimoonak Jul 21, 2022
26fb54a
Merge branch 'master' into 971_calc_exp
maimoonak Jul 21, 2022
cefbb96
Merge branch 'master' into 971_calc_exp
maimoonak Jul 25, 2022
f51eb9e
Merge branch 'master' into 971_calc_exp
maimoonak Jul 26, 2022
1d2ae03
Merge branch 'master' into 971_calc_exp
maimoonak Jul 26, 2022
e12f3bc
Merge branch 'master' into 971_calc_exp
maimoonak Jul 27, 2022
2f868ce
Merge branch 'master' into 971_calc_exp
maimoonak Jul 29, 2022
f2a1aec
Merge branch 'master' into 971_calc_exp
maimoonak Aug 5, 2022
9302aa0
Revert ignore test | merge main | refactor
maimoonak Aug 5, 2022
01bc73d
Merge branch 'master' into 971_calc_exp
maimoonak Aug 12, 2022
6651038
Merge master and resolve conflicts
maimoonak Sep 2, 2022
cb2a027
Merge branch '971_calc_exp' of https://github.com/opensrp/android-fhi…
maimoonak Sep 2, 2022
77b163e
spotless fix
maimoonak Sep 2, 2022
b9f40b6
Rename tests
maimoonak Sep 2, 2022
54a8f11
Add tests and docs
maimoonak Sep 2, 2022
9089ceb
Merge branch 'master' into 971_calc_exp
maimoonak Sep 14, 2022
76c934f
Merge branch 'master' into 971_calc_exp
maimoonak Sep 15, 2022
dc93bdb
Merge branch 'master' into 971_calc_exp
maimoonak Sep 19, 2022
e80c3f4
Move catalog calculation to behavior tab
maimoonak Sep 23, 2022
a041d1e
Merge master and resolve conflicts
maimoonak Sep 23, 2022
e3f303a
spotless fix
maimoonak Sep 23, 2022
6d06948
Merge branch 'master' into 971_calc_exp
maimoonak Sep 26, 2022
f494aff
Merge branch 'master' into 971_calc_exp
maimoonak Sep 27, 2022
7459ea4
Merge branch 'master' into 971_calc_exp
maimoonak Sep 27, 2022
02f926e
Merge branch 'master' into 971_calc_exp
maimoonak Sep 30, 2022
4c16c3a
Update datacapture/src/main/java/com/google/android/fhir/datacapture/…
maimoonak Sep 30, 2022
0a3c836
Resolve feedback for naming
maimoonak Oct 3, 2022
e20451d
Resolve feedback for doc and naming
maimoonak Oct 4, 2022
cfe4c4a
Refactor the update answer handling logic
maimoonak Oct 4, 2022
27acf14
Merge branch '971_calc_exp' of https://github.com/opensrp/android-fhi…
maimoonak Oct 4, 2022
8fe48fe
Merge branch 'master' into 971_calc_exp
maimoonak Oct 6, 2022
9879702
Merge branch 'master' into 971_calc_exp
maimoonak Oct 6, 2022
58184f1
Merge branch 'master' into 971_calc_exp
maimoonak Oct 10, 2022
800bba1
Resolve feedback and merge master
maimoonak Oct 10, 2022
4e90294
Merge branch 'master' into 971_calc_exp
maimoonak Oct 10, 2022
89f1115
Fix failing test
maimoonak Oct 10, 2022
1a25f28
Merge branch '971_calc_exp' of https://github.com/opensrp/android-fhi…
maimoonak Oct 10, 2022
6c2e0e4
Merge branch 'master' into 971_calc_exp
maimoonak Oct 11, 2022
86e7a2b
Merge branch 'master' into 971_calc_exp
maimoonak Oct 12, 2022
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
36 changes: 36 additions & 0 deletions catalog/src/main/assets/calculated_expression_questionnaire.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"resourceType": "Questionnaire",
"item": [
{
"linkId": "a-birthdate",
"text": "Birth Date",
"type": "date",
"extension": [
{
"url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-calculatedExpression",
"valueExpression": {
"language": "text/fhirpath",
"expression": "%resource.repeat(item).where(linkId='a-age-years' and answer.empty().not()).select(today() - answer.value)"
}
}
]
},
{
"linkId": "a-age-years",
"text": "Age years",
"type": "quantity",
"initial": [{
"valueQuantity": {
"unit": "years",
"system": "http://unitsofmeasure.org",
"code": "years"
}
}]
},
{
"linkId": "a-age-acknowledge",
"text": "Input age to automatically calculate birthdate until birthdate is updated manually",
"type": "display"
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@ class BehaviorListViewModel(application: Application) : AndroidViewModel(applica
val questionnaireFileName: String,
val workFlow: WorkflowType = WorkflowType.DEFAULT
) {
CALCULATIONS(R.drawable.ic_calculations_behavior, R.string.behavior_name_calculation, ""),
CALCULATED_EXPRESSION(
R.drawable.ic_calculations_behavior,
R.string.behavior_name_calculated_expression,
"calculated_expression_questionnaire.json"
),
SKIP_LOGIC(R.drawable.ic_skiplogic_behavior, R.string.behavior_name_skip_logic, "")
}
}
4 changes: 3 additions & 1 deletion catalog/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@
<string name="layout_name_paginated">Paginated</string>
<string name="layout_name_review">Review</string>
<string name="layout_name_read_only">Read only</string>
<string name="behavior_name_calculation">Calculation</string>
<string name="behavior_name_skip_logic">Skip logic</string>
<string
name="behavior_name_calculated_expression"
>Calculated Expression</string>
<string name="toolbar_text">Structured data capture \n Catalog</string>
<string
name="questionnaire_response_title"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* 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.views

import android.view.View
import android.widget.FrameLayout
import android.widget.TextView
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.typeText
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.platform.app.InstrumentationRegistry
import com.google.android.fhir.datacapture.R
import com.google.android.fhir.datacapture.TestActivity
import com.google.android.fhir.datacapture.validation.NotValidated
import com.google.common.truth.Truth.assertThat
import java.math.BigDecimal
import org.hl7.fhir.r4.model.Quantity
import org.hl7.fhir.r4.model.Questionnaire
import org.hl7.fhir.r4.model.QuestionnaireResponse
import org.junit.Before
import org.junit.Rule
import org.junit.Test

class QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest {
@Rule
@JvmField
var activityScenarioRule: ActivityScenarioRule<TestActivity> =
ActivityScenarioRule(TestActivity::class.java)

private lateinit var parent: FrameLayout
private lateinit var viewHolder: QuestionnaireItemViewHolder

@Before
fun setup() {
activityScenarioRule.scenario.onActivity { activity -> parent = FrameLayout(activity) }
viewHolder = QuestionnaireItemEditTextQuantityViewHolderFactory.create(parent)
setTestLayout(viewHolder.itemView)
}

@Test
fun getValue_WithInitial_shouldReturn_Quantity_With_UnitAndSystem() {
val questionnaireItemViewItem =
QuestionnaireItemViewItem(
Questionnaire.QuestionnaireItemComponent().apply {
required = true
addInitial(
Questionnaire.QuestionnaireItemInitialComponent(
Quantity().apply {
code = "months"
system = "http://unitofmeasure.com"
}
)
)
},
QuestionnaireResponse.QuestionnaireResponseItemComponent(),
validationResult = NotValidated,
answersChangedCallback = { _, _, _ -> },
)
runOnUI { viewHolder.bind(questionnaireItemViewItem) }

onView(withId(R.id.text_input_edit_text)).perform(click())
onView(withId(R.id.text_input_edit_text)).perform(typeText("22"))
assertThat(
viewHolder.itemView.findViewById<TextView>(R.id.text_input_edit_text).text.toString()
)
.isEqualTo("22")

val responseValue = questionnaireItemViewItem.answers.first().valueQuantity
assertThat(responseValue.code).isEqualTo("months")
assertThat(responseValue.system).isEqualTo("http://unitofmeasure.com")
assertThat(responseValue.value).isEqualTo(BigDecimal(22))
}

@Test
fun getValue_WithoutInitial_shouldReturn_Quantity_Without_UnitAndSystem() {
val questionnaireItemViewItem =
QuestionnaireItemViewItem(
Questionnaire.QuestionnaireItemComponent().apply { required = true },
QuestionnaireResponse.QuestionnaireResponseItemComponent(),
validationResult = NotValidated,
answersChangedCallback = { _, _, _ -> },
)
runOnUI { viewHolder.bind(questionnaireItemViewItem) }

onView(withId(R.id.text_input_edit_text)).perform(click())
onView(withId(R.id.text_input_edit_text)).perform(typeText("22"))
assertThat(
viewHolder.itemView.findViewById<TextView>(R.id.text_input_edit_text).text.toString()
)
.isEqualTo("22")

val responseValue = questionnaireItemViewItem.answers.first().valueQuantity
assertThat(responseValue.code).isNull()
assertThat(responseValue.system).isNull()
assertThat(responseValue.value).isEqualTo(BigDecimal(22))
}

/** Method to run code snippet on UI/main thread */
private fun runOnUI(action: () -> Unit) {
activityScenarioRule.scenario.onActivity { action() }
}

/** Method to set content view for test activity */
private fun setTestLayout(view: View) {
activityScenarioRule.scenario.onActivity { activity -> activity.setContentView(view) }
InstrumentationRegistry.getInstrumentation().waitForIdleSync()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ internal const val EXTENSION_ITEM_CONTROL_SYSTEM = "http://hl7.org/fhir/question
internal const val EXTENSION_HIDDEN_URL =
"http://hl7.org/fhir/StructureDefinition/questionnaire-hidden"

internal const val EXTENSION_CALCULATED_EXPRESSION_URL =
"http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-calculatedExpression"

internal const val EXTENSION_ENTRY_FORMAT_URL =
"http://hl7.org/fhir/StructureDefinition/entryFormat"

Expand All @@ -83,6 +86,9 @@ internal const val EXTENSION_CHOICE_COLUMN_URL: String =

internal const val EXTENSION_VARIABLE_URL = "http://hl7.org/fhir/StructureDefinition/variable"

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

internal const val EXTENSION_SLIDER_STEP_VALUE_URL =
"http://hl7.org/fhir/StructureDefinition/questionnaire-sliderStepValue"

Expand All @@ -104,8 +110,32 @@ internal fun Questionnaire.QuestionnaireItemComponent.findVariableExpression(
return variableExpressions.find { it.name == variableName }
}

internal const val CQF_CALCULATED_EXPRESSION_URL: String =
"http://hl7.org/fhir/StructureDefinition/cqf-calculatedValue"
/** Returns Calculated expression, or null */
internal val Questionnaire.QuestionnaireItemComponent.calculatedExpression: Expression?
get() =
this.getExtensionByUrl(EXTENSION_CALCULATED_EXPRESSION_URL)?.let {
it.castToExpression(it.value)
}

/** Returns list of extensions whose value is of type [Expression] */
internal val Questionnaire.QuestionnaireItemComponent.expressionBasedExtensions
get() = this.extension.filter { it.value is Expression }

/**
* Whether [item] has any expression directly referencing the current questionnaire item by link ID
* (e.g. if [item] has an expression `%resource.item.where(linkId='this-question')` where
* `this-question` is the link ID of the current questionnaire item).
*/
internal fun Questionnaire.QuestionnaireItemComponent.isReferencedBy(
item: Questionnaire.QuestionnaireItemComponent
) =
item.expressionBasedExtensions.any {
it
.castToExpression(it.value)
.expression
.replace(" ", "")
.contains(Regex(".*linkId='${this.linkId}'.*"))
}

// Item control code, or null
internal val Questionnaire.QuestionnaireItemComponent.itemControl: ItemControlTypes?
Expand Down Expand Up @@ -358,7 +388,12 @@ val Questionnaire.QuestionnaireItemComponent.enableWhenExpression: Expression?
*/
private fun Questionnaire.QuestionnaireItemComponent.createQuestionnaireResponseItemAnswers():
MutableList<QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent>? {
if (initial.isEmpty()) {
// https://build.fhir.org/ig/HL7/sdc/behavior.html#initial
// quantity given as initial without value is for unit reference purpose only. Answer conversion
// not needed
if (initial.isEmpty() ||
(initialFirstRep.hasValueQuantity() && initialFirstRep.valueQuantity.value == null)
) {
return null
}

Expand Down Expand Up @@ -481,6 +516,15 @@ internal fun Questionnaire.QuestionnaireItemComponent.extractAnswerOptions(
}.map { Questionnaire.QuestionnaireItemAnswerOptionComponent(it) }
}

/**
* Flatten a nested list of [Questionnaire.QuestionnaireItemComponent] recursively and returns a
* flat list of all items into list embedded at any level
*/
fun List<Questionnaire.QuestionnaireItemComponent>.flattened():
List<Questionnaire.QuestionnaireItemComponent> {
return this + this.flatMap { it.item.flattened() }
}

/**
* Creates a list of [QuestionnaireResponse.QuestionnaireResponseItemComponent]s from the nested
* items in the [Questionnaire.QuestionnaireItemComponent].
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import org.hl7.fhir.r4.model.Quantity
import org.hl7.fhir.r4.model.QuestionnaireResponse
import org.hl7.fhir.r4.model.StringType
import org.hl7.fhir.r4.model.TimeType
import org.hl7.fhir.r4.model.Type
import org.hl7.fhir.r4.model.UriType

/**
Expand Down Expand Up @@ -73,3 +74,8 @@ internal fun QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent.disp
else -> context.getString(R.string.not_answered)
}
}

internal fun List<QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent>
.hasDifferentAnswerSet(answers: List<Type>) =
this.size != answers.size ||
this.map { it.value }.zip(answers).any { (v1, v2) -> v1.equalsDeep(v2).not() }
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import ca.uhn.fhir.context.FhirVersionEnum
import ca.uhn.fhir.parser.IParser
import com.google.android.fhir.FhirEngineProvider
import com.google.android.fhir.datacapture.enablement.EnablementEvaluator
import com.google.android.fhir.datacapture.fhirpath.ExpressionEvaluator.detectExpressionCyclicDependency
import com.google.android.fhir.datacapture.fhirpath.ExpressionEvaluator.evaluateCalculatedExpressions
import com.google.android.fhir.datacapture.utilities.fhirPathEngine
import com.google.android.fhir.datacapture.validation.NotValidated
import com.google.android.fhir.datacapture.validation.QuestionnaireResponseItemValidator
Expand Down Expand Up @@ -274,8 +276,10 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
if (questionnaireItem.hasNestedItemsWithinAnswers) {
questionnaireResponseItem.addNestedItemsToAnswer(questionnaireItem)
}

modifiedQuestionnaireResponseItemSet.add(questionnaireResponseItem)

updateDependentQuestionnaireResponseItems(questionnaireItem)

modificationCount.update { it + 1 }
}

Expand Down Expand Up @@ -377,12 +381,50 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
SharingStarted.Lazily,
initialValue =
getQuestionnaireState(
questionnaireItemList = questionnaire.item,
questionnaireResponseItemList = questionnaireResponse.item,
currentPageIndex = getInitialPageIndex(),
reviewMode = enableReviewPage
)
questionnaireItemList = questionnaire.item,
questionnaireResponseItemList = questionnaireResponse.item,
currentPageIndex = getInitialPageIndex(),
reviewMode = enableReviewPage
)
.also { detectExpressionCyclicDependency(questionnaire.item) }
.also {
questionnaire.item.flattened().forEach {
updateDependentQuestionnaireResponseItems(it)
}
}
)

fun updateDependentQuestionnaireResponseItems(
updatedQuestionnaireItem: Questionnaire.QuestionnaireItemComponent
) {
evaluateCalculatedExpressions(
updatedQuestionnaireItem,
questionnaire,
questionnaireResponse,
questionnaireItemParentMap
)
.forEach { (questionnaireItem, calculatedAnswers) ->
// update all response item with updated values
questionnaireResponseItemPreOrderList
// Item answer should not be modified and touched by user;
// https://build.fhir.org/ig/HL7/sdc/StructureDefinition-sdc-questionnaire-calculatedExpression.html
.filter {
it.linkId == questionnaireItem.linkId &&
!modifiedQuestionnaireResponseItemSet.contains(it)
}
.forEach { questionnaireResponseItem ->
// update and notify only if new answer has changed to prevent any event loop
if (questionnaireResponseItem.answer.hasDifferentAnswerSet(calculatedAnswers)) {
questionnaireResponseItem.answer =
calculatedAnswers.map {
QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply {
value = it
}
}
}
}
}
}

@PublishedApi
internal suspend fun resolveAnswerValueSet(
Expand Down
Loading