Skip to content

Commit

Permalink
Support for displaying display items based on conditions. (#1898)
Browse files Browse the repository at this point in the history
* ac

* Review changes and updated kdoc

* Espresso test for enabled display items

* Added unit test for enabledDisplayItems

* modified test for initial value set to true
  • Loading branch information
MJ1998 authored Mar 15, 2023
1 parent b314fc3 commit e0c92b2
Show file tree
Hide file tree
Showing 27 changed files with 687 additions and 326 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
{
"resourceType": "Questionnaire",
"id": "questionnaire.enabled.display",
"name": "Questionnaire Enabled Display",
"title": "Questionnaire Enabled Display",
"status": "active",
"item": [
{
"linkId": "1",
"text": "Questionnaire Text",
"type": "boolean",
"item": [
{
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory",
"valueCodeableConcept": {
"coding": [
{
"system": "http://hl7.org/fhir/questionnaire-display-category",
"code": "instructions"
}
]
}
},
{
"url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-enableWhenExpression",
"valueExpression": {
"language": "text/fhirpath",
"expression": "%resource.repeat(item).where(linkId='1' and answer.empty().not()).select(answer.value) = false"
}
}
],
"linkId": "1.1",
"text": "Text when no is selected",
"type": "display"
},
{
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory",
"valueCodeableConcept": {
"coding": [
{
"system": "http://hl7.org/fhir/questionnaire-display-category",
"code": "instructions"
}
]
}
},
{
"url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-enableWhenExpression",
"valueExpression": {
"language": "text/fhirpath",
"expression": "%resource.repeat(item).where(linkId='1' and answer.empty().not()).select(answer.value) = true"
}
}
],
"linkId": "1.2",
"text": "Text when yes is selected",
"type": "display"
}
]
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@

package com.google.android.fhir.datacapture

import android.view.View
import android.widget.FrameLayout
import android.widget.TextView
import androidx.fragment.app.commitNow
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions
Expand Down Expand Up @@ -361,6 +363,41 @@ class QuestionnaireUiEspressoTest {
assertThat(exception.message).isEqualTo("minValue cannot be greater than maxValue")
}

@Test
fun displayItems_shouldGetEnabled_withAnswerChoice() {
buildFragmentFromQuestionnaire("/questionnaire_with_enabled_display_items.json")

onView(withId(R.id.hint)).check { view, _ ->
val hintVisibility = (view as TextView).visibility
assertThat(hintVisibility).isEqualTo(View.GONE)
}

onView(withId(R.id.yes_radio_button)).perform(ViewActions.click())

onView(withId(R.id.hint)).check { view, _ ->
val hintVisibility = (view as TextView).visibility
val hintText = view.text.toString()
assertThat(hintVisibility).isEqualTo(View.VISIBLE)
assertThat(hintText).isEqualTo("Text when yes is selected")
}

onView(withId(R.id.no_radio_button)).perform(ViewActions.click())

onView(withId(R.id.hint)).check { view, _ ->
val hintVisibility = (view as TextView).visibility
val hintText = view.text.toString()
assertThat(hintVisibility).isEqualTo(View.VISIBLE)
assertThat(hintText).isEqualTo("Text when no is selected")
}

onView(withId(R.id.no_radio_button)).perform(ViewActions.click())

onView(withId(R.id.hint)).check { view, _ ->
val hintVisibility = (view as TextView).visibility
assertThat(hintVisibility).isEqualTo(View.GONE)
}
}

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 @@ -200,31 +200,32 @@ class QuestionnaireItemDialogMultiSelectViewHolderFactoryEspressoTest {

@Test
fun bindView_setHintText() {
val hintItem =
Questionnaire.QuestionnaireItemComponent().apply {
linkId = "1.1"
text = "Select code"
type = Questionnaire.QuestionnaireItemType.DISPLAY
addExtension(
Extension()
.setUrl(EXTENSION_ITEM_CONTROL_URL)
.setValue(
CodeableConcept()
.addCoding(
Coding()
.setCode(DisplayItemControlType.FLYOVER.extensionCode)
.setSystem(EXTENSION_ITEM_CONTROL_SYSTEM)
)
)
)
}
val questionnaireViewItem =
QuestionnaireViewItem(
answerOptions(false, "Coding 1", "Coding 2", "Coding 3", "Coding 4", "Coding 5")
.addItem(
Questionnaire.QuestionnaireItemComponent().apply {
linkId = "1.1"
text = "Select code"
type = Questionnaire.QuestionnaireItemType.DISPLAY
addExtension(
Extension()
.setUrl(EXTENSION_ITEM_CONTROL_URL)
.setValue(
CodeableConcept()
.addCoding(
Coding()
.setCode(DisplayItemControlType.FLYOVER.extensionCode)
.setSystem(EXTENSION_ITEM_CONTROL_SYSTEM)
)
)
)
}
),
.addItem(hintItem),
responseOptions(),
validationResult = NotValidated,
answersChangedCallback = { _, _, _, _ -> },
enabledDisplayItems = listOf(hintItem)
)
runOnUI { viewHolder.bind(questionnaireViewItem) }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,9 @@ import com.google.android.fhir.datacapture.extensions.entryMode
import com.google.android.fhir.datacapture.extensions.extractAnswerOptions
import com.google.android.fhir.datacapture.extensions.flattened
import com.google.android.fhir.datacapture.extensions.hasDifferentAnswerSet
import com.google.android.fhir.datacapture.extensions.isDisplayItem
import com.google.android.fhir.datacapture.extensions.isFhirPath
import com.google.android.fhir.datacapture.extensions.isFlyoverCode
import com.google.android.fhir.datacapture.extensions.isHelpCode
import com.google.android.fhir.datacapture.extensions.isHidden
import com.google.android.fhir.datacapture.extensions.isInstructionsCode
import com.google.android.fhir.datacapture.extensions.isPaginated
import com.google.android.fhir.datacapture.extensions.isXFhirQuery
import com.google.android.fhir.datacapture.extensions.localizedTextSpanned
Expand Down Expand Up @@ -643,7 +641,12 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
answersChangedCallback = answersChangedCallback,
resolveAnswerValueSet = { resolveAnswerValueSet(it) },
resolveAnswerExpression = { resolveAnswerExpression(it) },
draftAnswer = draftAnswerMap[questionnaireResponseItem]
draftAnswer = draftAnswerMap[questionnaireResponseItem],
enabledDisplayItems =
questionnaireItem.item.filter {
it.isDisplayItem &&
EnablementEvaluator(questionnaireResponse).evaluate(it, questionnaireResponseItem)
}
)
)
)
Expand Down Expand Up @@ -672,11 +675,7 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
getQuestionnaireAdapterItems(
// If nested display item is identified as instructions or flyover, then do not create
// questionnaire state for it.
questionnaireItemList =
questionnaireItem.item.filterNot {
it.type == Questionnaire.QuestionnaireItemType.DISPLAY &&
(it.isInstructionsCode || it.isFlyoverCode || it.isHelpCode)
},
questionnaireItemList = questionnaireItem.item.filterNot { it.isDisplayItem },
questionnaireResponseItemList = nestedResponseItemList,
)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ internal class EnablementEvaluator(val questionnaireResponse: QuestionnaireRespo
if (enableWhenList.size == 1) {
return evaluateEnableWhen(
enableWhenList.single(),
questionnaireItem,
questionnaireResponseItem,
)
}
Expand All @@ -124,9 +125,9 @@ internal class EnablementEvaluator(val questionnaireResponse: QuestionnaireRespo
// enabled if ANY `enableWhen` constraint is satisfied.
return when (val value = questionnaireItem.enableBehavior) {
Questionnaire.EnableWhenBehavior.ALL ->
enableWhenList.all { evaluateEnableWhen(it, questionnaireResponseItem) }
enableWhenList.all { evaluateEnableWhen(it, questionnaireItem, questionnaireResponseItem) }
Questionnaire.EnableWhenBehavior.ANY ->
enableWhenList.any { evaluateEnableWhen(it, questionnaireResponseItem) }
enableWhenList.any { evaluateEnableWhen(it, questionnaireItem, questionnaireResponseItem) }
else -> throw IllegalStateException("Unrecognized enable when behavior $value")
}
}
Expand All @@ -136,10 +137,15 @@ internal class EnablementEvaluator(val questionnaireResponse: QuestionnaireRespo
*/
private fun evaluateEnableWhen(
enableWhen: Questionnaire.QuestionnaireItemEnableWhenComponent,
questionnaireItem: Questionnaire.QuestionnaireItemComponent,
questionnaireResponseItem: QuestionnaireResponse.QuestionnaireResponseItemComponent,
): Boolean {
val targetQuestionnaireResponseItem =
findEnableWhenQuestionnaireResponseItem(questionnaireResponseItem, enableWhen.question)
val targetQuestionnaireResponseItem: QuestionnaireResponse.QuestionnaireResponseItemComponent? =
if (questionnaireItem.type == Questionnaire.QuestionnaireItemType.DISPLAY &&
questionnaireResponseItem.linkId == enableWhen.question
)
questionnaireResponseItem
else findEnableWhenQuestionnaireResponseItem(questionnaireResponseItem, enableWhen.question)
return if (Questionnaire.QuestionnaireItemOperator.EXISTS == enableWhen.operator) {
// True iff the answer value of the enable when is equal to whether an answer exists in the
// target questionnaire response item
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -273,18 +273,6 @@ internal val Questionnaire.QuestionnaireItemComponent.hasHelpButton: Boolean
return item.any { it.isHelpCode }
}

/** Whether item type is display and [displayItemControl] is [DisplayItemControlType.HELP]. */
internal val Questionnaire.QuestionnaireItemComponent.isHelpCode: Boolean
get() {
return when (type) {
Questionnaire.QuestionnaireItemType.DISPLAY -> {
displayItemControl == DisplayItemControlType.HELP
}
else -> {
false
}
}
}
/** Converts Text with HTML Tag to formatted text. */
private fun String.toSpanned(): Spanned {
return HtmlCompat.fromHtml(this, HtmlCompat.FROM_HTML_MODE_COMPACT)
Expand All @@ -309,9 +297,12 @@ val Questionnaire.QuestionnaireItemComponent.localizedPrefixSpanned: Spanned?
* code is used as the instructions of the parent question.
*/
internal val Questionnaire.QuestionnaireItemComponent.localizedInstructionsSpanned: Spanned?
get() = item.localizedInstructionsSpanned

/** [localizedInstructionsSpanned] over list of [Questionnaire.QuestionnaireItemComponent] */
internal val List<Questionnaire.QuestionnaireItemComponent>.localizedInstructionsSpanned: Spanned?
get() {
return item
.firstOrNull { questionnaireItem ->
return this.firstOrNull { questionnaireItem ->
questionnaireItem.type == Questionnaire.QuestionnaireItemType.DISPLAY &&
questionnaireItem.isInstructionsCode
}
Expand All @@ -323,9 +314,12 @@ internal val Questionnaire.QuestionnaireItemComponent.localizedInstructionsSpann
* present) is used as the fly-over text of the parent question.
*/
internal val Questionnaire.QuestionnaireItemComponent.localizedFlyoverSpanned: Spanned?
get() = item.localizedFlyoverSpanned

/** [localizedFlyoverSpanned] over list of [Questionnaire.QuestionnaireItemComponent] */
internal val List<Questionnaire.QuestionnaireItemComponent>.localizedFlyoverSpanned: Spanned?
get() =
item
.firstOrNull { questionnaireItem ->
this.firstOrNull { questionnaireItem ->
questionnaireItem.type == Questionnaire.QuestionnaireItemType.DISPLAY &&
questionnaireItem.displayItemControl == DisplayItemControlType.FLYOVER
}
Expand All @@ -336,9 +330,12 @@ internal val Questionnaire.QuestionnaireItemComponent.localizedFlyoverSpanned: S
* code is used as the instructions of the parent question.
*/
internal val Questionnaire.QuestionnaireItemComponent.localizedHelpSpanned: Spanned?
get() = item.localizedHelpSpanned

/** [localizedHelpSpanned] over list of [Questionnaire.QuestionnaireItemComponent] */
internal val List<Questionnaire.QuestionnaireItemComponent>.localizedHelpSpanned: Spanned?
get() {
return item
.firstOrNull { questionnaireItem -> questionnaireItem.isHelpCode }
return this.firstOrNull { questionnaireItem -> questionnaireItem.isHelpCode }
?.localizedTextSpanned
}

Expand Down Expand Up @@ -405,6 +402,25 @@ internal val Questionnaire.QuestionnaireItemComponent.isFlyoverCode: Boolean
}
}

/** Whether item type is display and [displayItemControl] is [DisplayItemControlType.HELP]. */
internal val Questionnaire.QuestionnaireItemComponent.isHelpCode: Boolean
get() {
return when (type) {
Questionnaire.QuestionnaireItemType.DISPLAY -> {
displayItemControl == DisplayItemControlType.HELP
}
else -> {
false
}
}
}

/** Whether item type is display. */
internal val Questionnaire.QuestionnaireItemComponent.isDisplayItem: Boolean
get() =
(type == Questionnaire.QuestionnaireItemType.DISPLAY &&
(isInstructionsCode || isFlyoverCode || isHelpCode))

/** Slider step extension value. */
internal val Questionnaire.QuestionnaireItemComponent.sliderStepValue: Int?
get() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import com.google.android.fhir.datacapture.extensions.localizedInstructionsSpann
import com.google.android.fhir.datacapture.extensions.localizedPrefixSpanned
import com.google.android.fhir.datacapture.extensions.localizedTextSpanned
import com.google.android.fhir.datacapture.extensions.updateTextAndVisibility
import org.hl7.fhir.r4.model.Questionnaire

internal class GroupHeaderView(context: Context, attrs: AttributeSet?) :
LinearLayout(context, attrs) {
Expand All @@ -37,19 +36,22 @@ internal class GroupHeaderView(context: Context, attrs: AttributeSet?) :
LayoutInflater.from(context).inflate(R.layout.group_type_header_view, this, true)
}

fun bind(questionnaireItem: Questionnaire.QuestionnaireItemComponent) {
val prefix = findViewById<TextView>(R.id.prefix)
val question = findViewById<TextView>(R.id.question)
val hint = findViewById<TextView>(R.id.hint)
private val prefix = findViewById<TextView>(R.id.prefix)
private val question = findViewById<TextView>(R.id.question)
private val hint = findViewById<TextView>(R.id.hint)

fun bind(questionnaireViewItem: QuestionnaireViewItem) {
initHelpViews(
helpButton = findViewById(R.id.helpButton),
helpCardView = findViewById(R.id.helpCardView),
helpTextView = findViewById(R.id.helpText),
questionnaireItem
questionnaireItem = questionnaireViewItem.questionnaireItem
)
prefix.updateTextAndVisibility(questionnaireViewItem.questionnaireItem.localizedPrefixSpanned)
question.updateTextAndVisibility(questionnaireViewItem.questionnaireItem.localizedTextSpanned)
hint.updateTextAndVisibility(
questionnaireViewItem.enabledDisplayItems.localizedInstructionsSpanned
)
prefix.updateTextAndVisibility(questionnaireItem.localizedPrefixSpanned)
question.updateTextAndVisibility(questionnaireItem.localizedTextSpanned)
hint.updateTextAndVisibility(questionnaireItem.localizedInstructionsSpanned)
visibility = getHeaderViewVisibility(prefix, question, hint)
}
}
Loading

0 comments on commit e0c92b2

Please sign in to comment.