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

Hide items if all items they are depending on have also been hidden. #1790

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ import org.hl7.fhir.r4.model.Coding
import org.hl7.fhir.r4.model.Expression
import org.hl7.fhir.r4.model.Questionnaire
import org.hl7.fhir.r4.model.QuestionnaireResponse
import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent
import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseItemComponent
import org.hl7.fhir.r4.model.ResourceType
import org.hl7.fhir.r4.model.ValueSet
import timber.log.Timber
Expand All @@ -61,7 +63,6 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat

/** The current questionnaire as questions are being answered. */
internal val questionnaire: Questionnaire
private lateinit var currentPageItems: List<QuestionnaireAdapterItem>

init {
questionnaire =
Expand Down Expand Up @@ -94,15 +95,6 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
/** The current questionnaire response as questions are being answered. */
private val questionnaireResponse: QuestionnaireResponse

/**
* True if the user has tapped the next/previous pagination buttons on the current page. This is
* needed to avoid spewing validation errors before any questions are answered.
*/
private var isPaginationButtonPressed = false

/** Forces response validation each time [getQuestionnaireAdapterItems] is called. */
private var hasPressedSubmitButton = false

init {
when {
state.contains(QuestionnaireFragment.EXTRA_QUESTIONNAIRE_RESPONSE_JSON_URI) -> {
Expand Down Expand Up @@ -192,13 +184,33 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
private val isInReviewModeFlow = MutableStateFlow(shouldShowReviewPageFirst)

/**
* Contains [QuestionnaireResponse.QuestionnaireResponseItemComponent]s that have been modified by
* the user. [QuestionnaireResponse.QuestionnaireResponseItemComponent]s that have not been
* modified by the user will not be validated. This is to avoid spamming the user with a sea of
* validation errors when the questionnaire is loaded initially.
* Contains [QuestionnaireResponseItemComponent]s that have been modified by the user.
* [QuestionnaireResponseItemComponent]s that have not been modified by the user will not be
* validated. This is to avoid spamming the user with a sea of validation errors when the
* questionnaire is loaded initially.
*/
private val modifiedQuestionnaireResponseItemSet =
mutableSetOf<QuestionnaireResponse.QuestionnaireResponseItemComponent>()
mutableSetOf<QuestionnaireResponseItemComponent>()

private lateinit var currentPageItems: List<QuestionnaireAdapterItem>

/**
* True if the user has tapped the next/previous pagination buttons on the current page. This is
* needed to avoid spewing validation errors before any questions are answered.
*/
private var isPaginationButtonPressed = false

/** Forces response validation each time [getQuestionnaireAdapterItems] is called. */
private var hasPressedSubmitButton = false

/**
* Map of [QuestionnaireResponseItemAnswerComponent] for
* [Questionnaire.QuestionnaireItemComponent]s that are disabled now. The answers will be used to
* pre-populate the [QuestionnaireResponseItemComponent] once the item is enabled again.
*/
private val responseItemToAnswersMapForDisabledQuestionnaireItem =
mutableMapOf<
QuestionnaireResponseItemComponent, List<QuestionnaireResponseItemAnswerComponent>>()

/**
* Callback function to update the view model after the answer(s) to a question have been changed.
Expand All @@ -211,16 +223,15 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
*
* This callback function has 3 params:
* - the reference to the [Questionnaire.QuestionnaireItemComponent] in the [Questionnaire]
* - the reference to the [QuestionnaireResponse.QuestionnaireResponseItemComponent] in the
* [QuestionnaireResponse]
* - a [List] of [QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent] which are the
* new answers to the question.
* - the reference to the [QuestionnaireResponseItemComponent] in the [QuestionnaireResponse]
* - a [List] of [QuestionnaireResponseItemAnswerComponent] which are the new answers to the
* question.
*/
private val answersChangedCallback:
(
Questionnaire.QuestionnaireItemComponent,
QuestionnaireResponse.QuestionnaireResponseItemComponent,
List<QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent>,
QuestionnaireResponseItemComponent,
List<QuestionnaireResponseItemAnswerComponent>,
) -> Unit =
{ questionnaireItem, questionnaireResponseItem, answers ->
// TODO(jingtang10): update the questionnaire response item pre-order list and the parent map
Expand Down Expand Up @@ -377,9 +388,7 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
if (questionnaireResponseItem.answer.hasDifferentAnswerSet(calculatedAnswers)) {
questionnaireResponseItem.answer =
calculatedAnswers.map {
QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply {
value = it
}
QuestionnaireResponseItemAnswerComponent().apply { value = it }
}
}
}
Expand Down Expand Up @@ -537,7 +546,7 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
*/
private fun getQuestionnaireAdapterItems(
questionnaireItemList: List<Questionnaire.QuestionnaireItemComponent>,
questionnaireResponseItemList: List<QuestionnaireResponse.QuestionnaireResponseItemComponent>,
questionnaireResponseItemList: List<QuestionnaireResponseItemComponent>,
): List<QuestionnaireAdapterItem> {
var responseIndex = 0
return questionnaireItemList
Expand All @@ -564,16 +573,21 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
*/
private fun getQuestionnaireAdapterItems(
questionnaireItem: Questionnaire.QuestionnaireItemComponent,
questionnaireResponseItem: QuestionnaireResponse.QuestionnaireResponseItemComponent,
questionnaireResponseItem: QuestionnaireResponseItemComponent,
): List<QuestionnaireAdapterItem> {
// Disabled/hidden questions should not get QuestionnaireItemViewItem instances
// Hidden questions should not get QuestionnaireItemViewItem instances
if (questionnaireItem.isHidden) return emptyList()
val enabled =
EnablementEvaluator(questionnaireResponse)
.evaluate(questionnaireItem, questionnaireResponseItem)
if (!enabled || questionnaireItem.isHidden) {
// Disabled questions should not get QuestionnaireItemViewItem instances
if (!enabled) {
cacheDisabledQuestionnaireItemAnswers(questionnaireResponseItem)
return emptyList()
}

restoreFromDisabledQuestionnaireItemAnswersCache(questionnaireResponseItem)

// Determine the validation result, which will be displayed on the item itself
val validationResult =
if (modifiedQuestionnaireResponseItemSet.contains(questionnaireResponseItem) ||
Expand Down Expand Up @@ -603,7 +617,7 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
)
)
)
val nestedResponses: List<List<QuestionnaireResponse.QuestionnaireResponseItemComponent>> =
val nestedResponses: List<List<QuestionnaireResponseItemComponent>> =
when {
// Repeated questions have one answer item per response instance, which we must display
// after the question.
Expand All @@ -630,10 +644,37 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
return items
}

/**
* If the item is not enabled, clear the answers that it may have from the previous enabled state.
* This will also prevent any questionnaire item that depends on the answer of this questionnaire
* item to be wrongly evaluated as well.
*/
private fun cacheDisabledQuestionnaireItemAnswers(
questionnaireResponseItem: QuestionnaireResponseItemComponent
) {
if (questionnaireResponseItem.hasAnswer()) {
responseItemToAnswersMapForDisabledQuestionnaireItem[questionnaireResponseItem] =
questionnaireResponseItem.answer
questionnaireResponseItem.answer = listOf()
}
}

/**
* If the questionnaire item was previously disabled, check the cache to restore previous answers.
*/
private fun restoreFromDisabledQuestionnaireItemAnswersCache(
questionnaireResponseItem: QuestionnaireResponseItemComponent
) {
if (responseItemToAnswersMapForDisabledQuestionnaireItem.contains(questionnaireResponseItem)) {
questionnaireResponseItem.answer =
responseItemToAnswersMapForDisabledQuestionnaireItem.remove(questionnaireResponseItem)
}
}

private fun getEnabledResponseItems(
questionnaireItemList: List<Questionnaire.QuestionnaireItemComponent>,
questionnaireResponseItemList: List<QuestionnaireResponse.QuestionnaireResponseItemComponent>,
): List<QuestionnaireResponse.QuestionnaireResponseItemComponent> {
questionnaireResponseItemList: List<QuestionnaireResponseItemComponent>,
): List<QuestionnaireResponseItemComponent> {
val enablementEvaluator = EnablementEvaluator(questionnaireResponse)
val responseItemKeys = questionnaireResponseItemList.map { it.linkId }
return questionnaireItemList
Expand Down Expand Up @@ -680,15 +721,15 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
*/
private fun createRepeatedGroupResponse(
questionnaireItem: Questionnaire.QuestionnaireItemComponent,
questionnaireResponseItem: QuestionnaireResponse.QuestionnaireResponseItemComponent,
): List<QuestionnaireResponse.QuestionnaireResponseItemComponent> {
questionnaireResponseItem: QuestionnaireResponseItemComponent,
): List<QuestionnaireResponseItemComponent> {
val individualQuestions = questionnaireItem.item
return questionnaireResponseItem.answer.map { repeatedGroupInstance ->
val responsesToIndividualQuestions = repeatedGroupInstance.item
check(responsesToIndividualQuestions.size == individualQuestions.size) {
"Repeated groups responses must have the same # of responses as the group has questions"
}
QuestionnaireResponse.QuestionnaireResponseItemComponent().apply {
QuestionnaireResponseItemComponent().apply {
linkId = questionnaireItem.linkId
text = questionnaireItem.localizedTextSpanned?.toString()
item =
Expand Down
Loading