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

Stop validating disabled questions #1763

Merged
merged 8 commits into from
Dec 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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

import org.hl7.fhir.r4.model.QuestionnaireResponse

/**
* Pre-order list of descendants of the questionnaire response item (inclusive of the current item).
*/
val QuestionnaireResponse.QuestionnaireResponseItemComponent.descendant:
List<QuestionnaireResponse.QuestionnaireResponseItemComponent>
get() {
return listOf(this) +
this.item.flatMap { it.descendant } +
this.answer.flatMap { answer -> answer.item.flatMap { it.descendant } }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* 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

import org.hl7.fhir.r4.model.QuestionnaireResponse

/** Pre-order list of all questionnaire response items in the questionnaire. */
val QuestionnaireResponse.allItems: List<QuestionnaireResponse.QuestionnaireResponseItemComponent>
get() = item.flatMap { it.descendant }
Original file line number Diff line number Diff line change
Expand Up @@ -139,36 +139,6 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
}
}

/**
* The pre-order traversal trace of the items in the [QuestionnaireResponse]. This essentially
* represents the order in which all items are displayed in the UI.
*/
private val questionnaireResponseItemPreOrderList =
mutableListOf<QuestionnaireResponse.QuestionnaireResponseItemComponent>()

init {
/**
* Adds all items in the [QuestionnaireResponse] to the pre-order list. Note that each
* questionnaire response item may either have child items (in the case of a group type
* question) or have answer items with nested questions.
*/
fun buildPreOrderList(item: QuestionnaireResponse.QuestionnaireResponseItemComponent) {
questionnaireResponseItemPreOrderList.add(item)
for (child in item.item) {
buildPreOrderList(child)
}
for (answer in item.answer) {
for (answerItem in answer.item) {
buildPreOrderList(answerItem)
}
}
}

for (item in questionnaireResponse.item) {
buildPreOrderList(item)
}
}

/** The map from each item in the [Questionnaire] to its parent. */
private var questionnaireItemParentMap:
Map<Questionnaire.QuestionnaireItemComponent, Questionnaire.QuestionnaireItemComponent>
Expand All @@ -192,27 +162,6 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
}
}

/** The map from each item in the [QuestionnaireResponse] to its parent. */
private val questionnaireResponseItemParentMap =
mutableMapOf<
QuestionnaireResponse.QuestionnaireResponseItemComponent,
QuestionnaireResponse.QuestionnaireResponseItemComponent
>()

init {
/** Adds each child-parent pair in the [QuestionnaireResponse] to the parent map. */
fun buildParentList(item: QuestionnaireResponse.QuestionnaireResponseItemComponent) {
for (child in item.item) {
questionnaireResponseItemParentMap[child] = item
buildParentList(child)
}
}

for (item in questionnaireResponse.item) {
buildParentList(item)
}
}

/** Flag to determine if the questionnaire should be read-only. */
private val isReadOnly = state[QuestionnaireFragment.EXTRA_READ_ONLY] ?: false

Expand Down Expand Up @@ -412,7 +361,7 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
)
.forEach { (questionnaireItem, calculatedAnswers) ->
// update all response item with updated values
questionnaireResponseItemPreOrderList
questionnaireResponse.allItems
// Item answer should not be modified and touched by user;
// https://build.fhir.org/ig/HL7/sdc/StructureDefinition-sdc-questionnaire-calculatedExpression.html
.filter {
Expand Down Expand Up @@ -610,11 +559,8 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
): List<QuestionnaireItemViewItem> {
// Disabled/hidden questions should not get QuestionnaireItemViewItem instances
val enabled =
EnablementEvaluator.evaluate(
questionnaireItem,
questionnaireResponseItem,
questionnaireResponse
) { item, linkId -> findEnableWhenQuestionnaireResponseItem(item, linkId) }
EnablementEvaluator(questionnaireResponse)
.evaluate(questionnaireItem, questionnaireResponseItem)
if (!enabled || questionnaireItem.isHidden) {
return emptyList()
}
Expand Down Expand Up @@ -676,19 +622,17 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
questionnaireItemList: List<Questionnaire.QuestionnaireItemComponent>,
questionnaireResponseItemList: List<QuestionnaireResponse.QuestionnaireResponseItemComponent>,
): List<QuestionnaireResponse.QuestionnaireResponseItemComponent> {
val enablementEvaluator = EnablementEvaluator(questionnaireResponse)
val responseItemKeys = questionnaireResponseItemList.map { it.linkId }
return questionnaireItemList
.asSequence()
.filter { responseItemKeys.contains(it.linkId) }
.zip(questionnaireResponseItemList.asSequence())
.filter { (questionnaireItem, questionnaireResponseItem) ->
EnablementEvaluator.evaluate(
enablementEvaluator.evaluate(
questionnaireItem,
questionnaireResponseItem,
questionnaireResponse
) { item, linkId ->
findEnableWhenQuestionnaireResponseItem(item, linkId) ?: return@evaluate null
}
)
}
.flatMap { (questionnaireItem, questionnaireResponseItem) ->
val isRepeatedGroup =
Expand Down Expand Up @@ -761,69 +705,24 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
}

/** Gets a list of [QuestionnairePage]s for a paginated questionnaire. */
private fun getQuestionnairePages() =
private fun getQuestionnairePages(): List<QuestionnairePage>? =
if (questionnaire.isPaginated) {
jingtang10 marked this conversation as resolved.
Show resolved Hide resolved
questionnaire.item.zip(questionnaireResponse.item).mapIndexed {
index,
(questionnaireItem, questionnaireResponseItem) ->
QuestionnairePage(
index,
EnablementEvaluator.evaluate(
questionnaireItem,
questionnaireResponseItem,
questionnaireResponse
) { item, linkId -> findEnableWhenQuestionnaireResponseItem(item, linkId) },
EnablementEvaluator(questionnaireResponse)
.evaluate(
questionnaireItem,
questionnaireResponseItem,
),
questionnaireItem.isHidden
)
}
} else {
null
}

/**
* Find a questionnaire response item in [QuestionnaireResponse] with the given `linkId` starting
* from the `origin`.
*
* This is used by the enableWhen logic to evaluate if a question should be enabled/displayed.
*
* If multiple questionnaire response items are present for the same question (same linkId),
* either as a result of repeated group or nested question under repeated answers, this returns
* the nearest question occurrence reachable by tracing first the "ancestor" axis and then the
* "preceding" axis and then the "following" axis.
*
* See
* https://www.hl7.org/fhir/questionnaire-definitions.html#Questionnaire.item.enableWhen.question.
*/
private fun findEnableWhenQuestionnaireResponseItem(
origin: QuestionnaireResponse.QuestionnaireResponseItemComponent,
linkId: String,
): QuestionnaireResponse.QuestionnaireResponseItemComponent? {
// Find the nearest ancestor with the linkId
var parent = questionnaireResponseItemParentMap[origin]
while (parent != null) {
if (parent.linkId == linkId) {
return parent
}
parent = questionnaireResponseItemParentMap[parent]
}

// Find the nearest item preceding the origin
val itemIndex = questionnaireResponseItemPreOrderList.indexOf(origin)
for (index in itemIndex - 1 downTo 0) {
if (questionnaireResponseItemPreOrderList[index].linkId == linkId) {
return questionnaireResponseItemPreOrderList[index]
}
}

// Find the nearest item succeeding the origin
for (index in itemIndex + 1 until questionnaireResponseItemPreOrderList.size) {
if (questionnaireResponseItemPreOrderList[index].linkId == linkId) {
return questionnaireResponseItemPreOrderList[index]
}
}

return null
}
}

typealias ItemToParentMap =
Expand Down
Loading