Skip to content

Commit

Permalink
Hide items if all items they are depending on have also been hidden. (#…
Browse files Browse the repository at this point in the history
…1790)

* Disable all question items in a dependent chain when top item is disabled

* Reverted the default layout

* Caching answers for disabled items incase they are enabled again before submitting

* Updated review comments

* Review comments: Refactored code and addded testcase to check cache restoration
  • Loading branch information
aditya-07 committed Jan 17, 2023
1 parent da528d4 commit baca4d2
Show file tree
Hide file tree
Showing 2 changed files with 277 additions and 34 deletions.
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

0 comments on commit baca4d2

Please sign in to comment.