From 8bc800cffef3d0e7a7b40f6667ddccd289d9c132 Mon Sep 17 00:00:00 2001 From: Santosh Pingle Date: Tue, 16 May 2023 15:07:38 +0530 Subject: [PATCH 01/11] Add nested QuestionnaireResponseItemComponent in the group. --- .../datacapture/QuestionnaireViewModel.kt | 33 +++++++ .../MoreQuestionnaireItemComponents.kt | 5 +- .../datacapture/QuestionnaireViewModelTest.kt | 95 +++++++++++++++++++ 3 files changed, 132 insertions(+), 1 deletion(-) diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt index 3e37c65bfe..4e93a1417b 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt @@ -129,6 +129,10 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat parser.parseResource(application.contentResolver.openInputStream(uri)) as QuestionnaireResponse checkQuestionnaireResponse(questionnaire, questionnaireResponse) + addQuestionnaireResponseItemComponentToQuestionnaireResponse( + questionnaire.item, + questionnaireResponse.item + ) } state.contains(QuestionnaireFragment.EXTRA_QUESTIONNAIRE_RESPONSE_JSON_STRING) -> { val questionnaireResponseJson: String = @@ -136,6 +140,10 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat questionnaireResponse = parser.parseResource(questionnaireResponseJson) as QuestionnaireResponse checkQuestionnaireResponse(questionnaire, questionnaireResponse) + addQuestionnaireResponseItemComponentToQuestionnaireResponse( + questionnaire.item, + questionnaireResponse.item + ) } else -> { questionnaireResponse = @@ -152,6 +160,31 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat questionnaireResponse.packRepeatedGroups() } + private fun addQuestionnaireResponseItemComponentToQuestionnaireResponse( + questionnaireItemComponents: List, + responseItemComponents: MutableList + ) { + questionnaireItemComponents.forEachIndexed { index, questionnaireItemComponent -> + when { + index >= responseItemComponents.size -> { + responseItemComponents.add(questionnaireItemComponent.createQuestionnaireResponseItem()) + } + questionnaireItemComponent.linkId != responseItemComponents[index].linkId -> { + responseItemComponents.add( + index, + questionnaireItemComponent.createQuestionnaireResponseItem() + ) + } + } + if (!questionnaireItemComponent.shouldHaveNestedItemsUnderAnswers) { + addQuestionnaireResponseItemComponentToQuestionnaireResponse( + questionnaireItemComponent.item, + responseItemComponents[index].item + ) + } + } + } + /** * The launch context allows information to be passed into questionnaire based on the context in * which he questionnaire is being evaluated. For example, what patient, what encounter, what diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponents.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponents.kt index 2f9ad14109..e19db41d98 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponents.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponents.kt @@ -448,7 +448,10 @@ internal val Questionnaire.QuestionnaireItemComponent.sliderStepValue: Int? * For background, see https://build.fhir.org/questionnaireresponse.html#link. */ internal val Questionnaire.QuestionnaireItemComponent.shouldHaveNestedItemsUnderAnswers: Boolean - get() = item.isNotEmpty() && (type != Questionnaire.QuestionnaireItemType.GROUP || !repeats) + get() = + item.isNotEmpty() && + ((type != Questionnaire.QuestionnaireItemType.GROUP && !repeats) || + (type == Questionnaire.QuestionnaireItemType.GROUP && repeats)) /** * Creates a list of [QuestionnaireResponse.QuestionnaireResponseItemComponent]s from the nested diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewModelTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewModelTest.kt index 73b43b140f..d242fd47e1 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewModelTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewModelTest.kt @@ -938,6 +938,101 @@ class QuestionnaireViewModelTest { createQuestionnaireViewModel(questionnaire, questionnaireResponse) } + @Test + fun `add nested QuestionnaireResponseItemComponent in the group if items are not answered`() { + val questionnaireString = + """ + { + "resourceType": "Questionnaire", + "id": "client-registration-sample", + "language": "en", + "status": "active", + "date": "2020-11-18T07:24:47.111Z", + "subjectType": [ + "Patient" + ], + "extension": [ + { + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemExtractionContext", + "valueExpression": { + "language": "application/x-fhir-query", + "expression": "Patient", + "name": "patient" + } + } + ], + "item": [ + { + "linkId": "1", + "text": "Group Item", + "type": "group", + "required": true, + "item": [ + { + "linkId": "1.1", + "text": "First Nested Item", + "type": "boolean", + "required": true + }, + { + "linkId": "1.2", + "text": "Second Nested Item", + "type": "boolean", + "required": true + } + ] + } + ] + } + """.trimIndent() + + val questionnaireResponseString = + """ + { + "resourceType": "QuestionnaireResponse", + "item": [ + { + "linkId": "1", + "text":"Group Item" + } + ] + } + """.trimIndent() + + val expectedResponseString = + """ + { + "resourceType": "QuestionnaireResponse", + "item": [ + { + "linkId": "1", + "text": "Group Item", + "item": [ + { + "linkId": "1.1", + "text": "First Nested Item" + }, + { + "linkId": "1.2", + "text": "Second Nested Item" + } + ] + } + ] + } + """.trimIndent() + + state.set(EXTRA_QUESTIONNAIRE_JSON_STRING, questionnaireString) + state.set(EXTRA_QUESTIONNAIRE_RESPONSE_JSON_STRING, questionnaireResponseString) + val viewModel = QuestionnaireViewModel(context, state) + val value = viewModel.getQuestionnaireResponse() + val expectedResponse = + printer.parseResource(QuestionnaireResponse::class.java, expectedResponseString) + as QuestionnaireResponse + + assertResourceEquals(value, expectedResponse) + } + // ==================================================================== // // // // Questionnaire State Flow // From 580eb0576bce90280cf769e915cdc562ad464514 Mon Sep 17 00:00:00 2001 From: Santosh Pingle Date: Tue, 16 May 2023 18:37:16 +0530 Subject: [PATCH 02/11] Add kotlin doc. --- .../android/fhir/datacapture/QuestionnaireViewModel.kt | 6 ++++++ .../android/fhir/datacapture/QuestionnaireViewModelTest.kt | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt index c21e3e0b67..ba2b2dd693 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt @@ -161,6 +161,12 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat questionnaireResponse.packRepeatedGroups() } + /** + * If the corresponding [QuestionnaireItemComponent] lacks a response item component, the + * [QuestionnaireResponseItemComponent] is added to the QuestionnaireResponse. Note : However, the + * aforementioned does not apply if the [QuestionnaireItemComponent] is a question (type other tha + * group type) with a nested questionnaire,or repeated group. + */ private fun addQuestionnaireResponseItemComponentToQuestionnaireResponse( questionnaireItemComponents: List, responseItemComponents: MutableList diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewModelTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewModelTest.kt index d242fd47e1..8bfa466bf6 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewModelTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewModelTest.kt @@ -939,7 +939,7 @@ class QuestionnaireViewModelTest { } @Test - fun `add nested QuestionnaireResponseItemComponent in the group if items are not answered`() { + fun `add empty nested QuestionnaireResponseItemComponent to the group if questions are not answered`() { val questionnaireString = """ { From e649583321197503558b12e763ee30d906e3b265 Mon Sep 17 00:00:00 2001 From: Santosh Pingle Date: Mon, 22 May 2023 18:08:29 +0530 Subject: [PATCH 03/11] Address review comments. --- .../datacapture/QuestionnaireViewModel.kt | 69 +++++++++++-------- .../MoreQuestionnaireItemComponents.kt | 5 +- 2 files changed, 40 insertions(+), 34 deletions(-) diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt index ba2b2dd693..1b0e236f21 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt @@ -18,6 +18,7 @@ package com.google.android.fhir.datacapture import android.app.Application import android.net.Uri +import android.util.Log import androidx.annotation.VisibleForTesting import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.SavedStateHandle @@ -129,22 +130,16 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat questionnaireResponse = parser.parseResource(application.contentResolver.openInputStream(uri)) as QuestionnaireResponse + addMissingResponseItems(questionnaire.item, questionnaireResponse.item) checkQuestionnaireResponse(questionnaire, questionnaireResponse) - addQuestionnaireResponseItemComponentToQuestionnaireResponse( - questionnaire.item, - questionnaireResponse.item - ) } state.contains(QuestionnaireFragment.EXTRA_QUESTIONNAIRE_RESPONSE_JSON_STRING) -> { val questionnaireResponseJson: String = state[QuestionnaireFragment.EXTRA_QUESTIONNAIRE_RESPONSE_JSON_STRING]!! questionnaireResponse = parser.parseResource(questionnaireResponseJson) as QuestionnaireResponse - checkQuestionnaireResponse(questionnaire, questionnaireResponse) - addQuestionnaireResponseItemComponentToQuestionnaireResponse( - questionnaire.item, - questionnaireResponse.item - ) + addMissingResponseItems(questionnaire.item, questionnaireResponse.item) + checkQuestionnaireResponse(questionnaire, questionnaireResponse!!) } else -> { questionnaireResponse = @@ -164,30 +159,29 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat /** * If the corresponding [QuestionnaireItemComponent] lacks a response item component, the * [QuestionnaireResponseItemComponent] is added to the QuestionnaireResponse. Note : However, the - * aforementioned does not apply if the [QuestionnaireItemComponent] is a question (type other tha - * group type) with a nested questionnaire,or repeated group. + * aforementioned does not apply if the [QuestionnaireItemComponent] is a question (type other + * than group type) with a nested question items,or repeated group. */ - private fun addQuestionnaireResponseItemComponentToQuestionnaireResponse( - questionnaireItemComponents: List, - responseItemComponents: MutableList + private fun addMissingResponseItems( + questionnaireItems: List, + responseItems: MutableList ) { - questionnaireItemComponents.forEachIndexed { index, questionnaireItemComponent -> - when { - index >= responseItemComponents.size -> { - responseItemComponents.add(questionnaireItemComponent.createQuestionnaireResponseItem()) - } - questionnaireItemComponent.linkId != responseItemComponents[index].linkId -> { - responseItemComponents.add( - index, - questionnaireItemComponent.createQuestionnaireResponseItem() - ) - } + val questionnaireIterator = questionnaireItems.iterator() + while (questionnaireIterator.hasNext()) { + val questionnaireItem = questionnaireIterator.next() + var responseItem = responseItems.firstOrNull { it.linkId == questionnaireItem.linkId } + + // add missing item + if (responseItem == null) { + responseItem = questionnaireItem.createQuestionnaireResponseItem() + responseItems.add(questionnaireItems.indexOf(questionnaireItem), responseItem) } - if (!questionnaireItemComponent.shouldHaveNestedItemsUnderAnswers) { - addQuestionnaireResponseItemComponentToQuestionnaireResponse( - questionnaireItemComponent.item, - responseItemComponents[index].item - ) + + // recursion + if (questionnaireItem.type == Questionnaire.QuestionnaireItemType.GROUP && + !questionnaireItem.repeats + ) { + addMissingResponseItems(questionnaireItem.item, responseItem.item) } } } @@ -609,6 +603,9 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat val questionnaireItemList = questionnaire.item val questionnaireResponseItemList = questionnaireResponse.item + questionnaireItemList.forEach { Log.d("GROUP", "q adpter item ${it.linkId}") } + questionnaireResponseItemList.forEach { Log.d("GROUP", "r adpter item ${it.linkId}") } + // Only display items on the current page while editing a paginated questionnaire, otherwise, // display all items. val questionnaireItemViewItems = @@ -624,6 +621,10 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat } else { getQuestionnaireAdapterItems(questionnaireItemList, questionnaireResponseItemList) } + // questionnaireItemViewItems.forEach { item -> + // item. + // // Log.d("GROUP", "view item : ${it}") + // } // Reviewing the questionnaire or the questionnaire is read-only if (isReadOnly || isInReviewModeFlow.value) { @@ -672,8 +673,16 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat questionnaireItemList: List, questionnaireResponseItemList: List, ): List { + // questionnaireItemList.forEach { + // Log.d("GROUP", "q adpter item ${it.linkId}") + // } + // questionnaireResponseItemList.forEach { + // Log.d("GROUP", "r adpter item ${it.linkId}") + // } return questionnaireItemList .zipByLinkId(questionnaireResponseItemList) { questionnaireItem, questionnaireResponseItem -> + // Log.d("GROUP", "adpter item ${questionnaireItem.linkId} : + // ${questionnaireResponseItem.linkId}") getQuestionnaireAdapterItems(questionnaireItem, questionnaireResponseItem) } .flatten() diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponents.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponents.kt index ff2d235e33..ab2cd0eb02 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponents.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponents.kt @@ -478,10 +478,7 @@ internal inline fun List.zipByLink * For background, see https://build.fhir.org/questionnaireresponse.html#link. */ internal val Questionnaire.QuestionnaireItemComponent.shouldHaveNestedItemsUnderAnswers: Boolean - get() = - item.isNotEmpty() && - ((type != Questionnaire.QuestionnaireItemType.GROUP && !repeats) || - (type == Questionnaire.QuestionnaireItemType.GROUP && repeats)) + get() = item.isNotEmpty() && (type != Questionnaire.QuestionnaireItemType.GROUP || !repeats) /** * Creates a list of [QuestionnaireResponse.QuestionnaireResponseItemComponent]s from the nested From 567e7ea24899351a75d9bda11f35fcb5f6194776 Mon Sep 17 00:00:00 2001 From: Santosh Pingle Date: Mon, 22 May 2023 18:37:44 +0530 Subject: [PATCH 04/11] Remove commented code. --- .../fhir/datacapture/QuestionnaireViewModel.kt | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt index 1b0e236f21..945b7997e0 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt @@ -18,7 +18,6 @@ package com.google.android.fhir.datacapture import android.app.Application import android.net.Uri -import android.util.Log import androidx.annotation.VisibleForTesting import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.SavedStateHandle @@ -139,7 +138,7 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat questionnaireResponse = parser.parseResource(questionnaireResponseJson) as QuestionnaireResponse addMissingResponseItems(questionnaire.item, questionnaireResponse.item) - checkQuestionnaireResponse(questionnaire, questionnaireResponse!!) + checkQuestionnaireResponse(questionnaire, questionnaireResponse) } else -> { questionnaireResponse = @@ -603,9 +602,6 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat val questionnaireItemList = questionnaire.item val questionnaireResponseItemList = questionnaireResponse.item - questionnaireItemList.forEach { Log.d("GROUP", "q adpter item ${it.linkId}") } - questionnaireResponseItemList.forEach { Log.d("GROUP", "r adpter item ${it.linkId}") } - // Only display items on the current page while editing a paginated questionnaire, otherwise, // display all items. val questionnaireItemViewItems = @@ -621,10 +617,6 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat } else { getQuestionnaireAdapterItems(questionnaireItemList, questionnaireResponseItemList) } - // questionnaireItemViewItems.forEach { item -> - // item. - // // Log.d("GROUP", "view item : ${it}") - // } // Reviewing the questionnaire or the questionnaire is read-only if (isReadOnly || isInReviewModeFlow.value) { @@ -673,16 +665,8 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat questionnaireItemList: List, questionnaireResponseItemList: List, ): List { - // questionnaireItemList.forEach { - // Log.d("GROUP", "q adpter item ${it.linkId}") - // } - // questionnaireResponseItemList.forEach { - // Log.d("GROUP", "r adpter item ${it.linkId}") - // } return questionnaireItemList .zipByLinkId(questionnaireResponseItemList) { questionnaireItem, questionnaireResponseItem -> - // Log.d("GROUP", "adpter item ${questionnaireItem.linkId} : - // ${questionnaireResponseItem.linkId}") getQuestionnaireAdapterItems(questionnaireItem, questionnaireResponseItem) } .flatten() From 8b5b1be806e315c132585080f0bf4c0fefaa7a4f Mon Sep 17 00:00:00 2001 From: Santosh Pingle Date: Tue, 30 May 2023 14:50:56 +0530 Subject: [PATCH 05/11] Address review comment. --- .../android/fhir/datacapture/QuestionnaireViewModel.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt index 945b7997e0..e6eb562583 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt @@ -156,10 +156,11 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat } /** - * If the corresponding [QuestionnaireItemComponent] lacks a response item component, the - * [QuestionnaireResponseItemComponent] is added to the QuestionnaireResponse. Note : However, the - * aforementioned does not apply if the [QuestionnaireItemComponent] is a question (type other - * than group type) with a nested question items,or repeated group. + * Adds empty [QuestionnaireResponseItemComponent]s to `responseItems` so that each + * [QuestionnaireItemComponent] in `questionnaireItems` has at least one corresponding + * [QuestionnaireResponseItemComponent]. This is because user-provided [QuestionnaireResponse] + * might not contain answers to unanswered or disabled questions. Note : this only applies to + * [QuestionnaireItemComponent]s nested under a group. */ private fun addMissingResponseItems( questionnaireItems: List, From bdafec80cfe8a694c874ead3412b60b279f3b72f Mon Sep 17 00:00:00 2001 From: Santosh Pingle Date: Mon, 19 Jun 2023 12:17:00 +0530 Subject: [PATCH 06/11] Address review comments. --- .../datacapture/QuestionnaireViewModel.kt | 158 +++++++++++--- .../datacapture/QuestionnaireViewModelTest.kt | 204 ++++++++++++++---- 2 files changed, 289 insertions(+), 73 deletions(-) diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt index fe06f5952b..92421be98b 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt @@ -142,7 +142,7 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat state[QuestionnaireFragment.EXTRA_QUESTIONNAIRE_RESPONSE_JSON_STRING]!! questionnaireResponse = parser.parseResource(questionnaireResponseJson) as QuestionnaireResponse - addMissingResponseItems(questionnaire.item, questionnaireResponse.item) + addMissingResponseItems() checkQuestionnaireResponse(questionnaire, questionnaireResponse) } else -> { @@ -160,37 +160,6 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat questionnaireResponse.packRepeatedGroups() } - /** - * Adds empty [QuestionnaireResponseItemComponent]s to `responseItems` so that each - * [QuestionnaireItemComponent] in `questionnaireItems` has at least one corresponding - * [QuestionnaireResponseItemComponent]. This is because user-provided [QuestionnaireResponse] - * might not contain answers to unanswered or disabled questions. Note : this only applies to - * [QuestionnaireItemComponent]s nested under a group. - */ - private fun addMissingResponseItems( - questionnaireItems: List, - responseItems: MutableList - ) { - val questionnaireIterator = questionnaireItems.iterator() - while (questionnaireIterator.hasNext()) { - val questionnaireItem = questionnaireIterator.next() - var responseItem = responseItems.firstOrNull { it.linkId == questionnaireItem.linkId } - - // add missing item - if (responseItem == null) { - responseItem = questionnaireItem.createQuestionnaireResponseItem() - responseItems.add(questionnaireItems.indexOf(questionnaireItem), responseItem) - } - - // recursion - if (questionnaireItem.type == Questionnaire.QuestionnaireItemType.GROUP && - !questionnaireItem.repeats - ) { - addMissingResponseItems(questionnaireItem.item, responseItem.item) - } - } - } - /** * The launch context allows information to be passed into questionnaire based on the context in * which the questionnaire is being evaluated. For example, what patient, what encounter, what @@ -379,6 +348,131 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat private val answerExpressionMap = mutableMapOf>() + /** + * Adds empty [QuestionnaireResponseItemComponent]s to `responseItems` so that each + * [QuestionnaireItemComponent] in `questionnaireItems` has at least one corresponding + * [QuestionnaireResponseItemComponent]. This is because user-provided [QuestionnaireResponse] + * might not contain answers to unanswered or disabled questions. Note : this only applies to + * [QuestionnaireItemComponent]s nested under a group. + */ + private fun addMissingResponseItems() { + val linkIdToResponseItemsMap = + mutableMapOf>() + associateLinkId(questionnaire.item, linkIdToResponseItemsMap) + + addResponseItemsToMap(questionnaire.item, questionnaireResponse.item, linkIdToResponseItemsMap) + + val responseItems = mutableListOf() + buildResponseItemList(linkIdToResponseItemsMap, questionnaire.item, responseItems) + questionnaireResponse.item = responseItems + } + + /** + * Adds linkId of the item present in the list [questionnaireItems] as a key, and an empty list of + * type [QuestionnaireResponseItemComponent] as a value to the map [linkIdToResponseItemsMap]. + */ + private fun associateLinkId( + questionnaireItems: List, + linkIdToResponseItemsMap: MutableMap> + ) { + questionnaireItems.forEach { + linkIdToResponseItemsMap[it.linkId] = mutableListOf() + associateLinkId(it.item, linkIdToResponseItemsMap) + } + } + + /** + * Adds response item present in the list [responseItems] corresponding to the linkId present in + * [questionnaireItems] to the map [linkedToResponseItemsMap], and if response item is missing + * then adds empty response item. + */ + private fun addResponseItemsToMap( + questionnaireItems: List, + responseItems: MutableList, + linkedToResponseItemsMap: MutableMap>, + ) { + val questionnaireIterator = questionnaireItems.iterator() + while (questionnaireIterator.hasNext()) { + val questionnaireItem = questionnaireIterator.next() + val responseItemComponents = responseItems.filter { questionnaireItem.linkId == it.linkId } + if (responseItemComponents.isNotEmpty()) { // add existing response items to the map. + linkedToResponseItemsMap[questionnaireItem.linkId]!!.addAll(responseItemComponents) + } else { // add empty response items to the map + linkedToResponseItemsMap[questionnaireItem.linkId]!!.add( + questionnaireItem.createQuestionnaireResponseItem() + ) + } + // traverse the nested item list of group type. + if (questionnaireItem.type == Questionnaire.QuestionnaireItemType.GROUP && + !questionnaireItem.repeats + ) { + addResponseItemsToMap( + questionnaireItem.item, + linkedToResponseItemsMap[questionnaireItem.linkId]!!.first().item, + linkedToResponseItemsMap + ) + } + } + } + + /** + * Adds response items present in the map [linkedToResponseItemsMap] to the list [responseItems]. + * The added items to the list [responseItems] correspond to the linkId of the item present in the + * list [questionnaireItems] + */ + private fun buildResponseItemList( + linkedToResponseItemsMap: MutableMap>, + questionnaireItems: List, + responseItems: MutableList, + ) { + val questionnaireIterator = questionnaireItems.iterator() + while (questionnaireIterator.hasNext()) { + val questionnaireItem = questionnaireIterator.next() + val responseItemComponents = linkedToResponseItemsMap[questionnaireItem.linkId]!! + if (!responseItems.containsAll(responseItemComponents) + ) { // checks if response items are already present in the list. + responseItems.addAll(responseItemComponents) + } + // traverse nested item list of group type. + if (questionnaireItem.type == Questionnaire.QuestionnaireItemType.GROUP && + !questionnaireItem.repeats + ) { + buildResponseItemList( + linkedToResponseItemsMap, + questionnaireItem.item, + responseItemComponents.first().item, + ) + } + } + } + + // Index based approach, this function will be removed. + private fun addMissingResponseItems( + questionnaireItems: List, + responseItems: MutableList + ) { + val questionnaireIterator = questionnaireItems.iterator() + var previousIndex = -1 + var currentIndex = -1 + while (questionnaireIterator.hasNext()) { + previousIndex = currentIndex + val questionnaireItem = questionnaireIterator.next() + currentIndex = responseItems.indexOfLast { it.linkId == questionnaireItem.linkId } + // add empty response item if missing. + if (currentIndex == -1) { + currentIndex = previousIndex + 1 + var responseItem = questionnaireItem.createQuestionnaireResponseItem() + responseItems.add(currentIndex, responseItem) + } + // traverse nested item list of group type. + if (questionnaireItem.type == Questionnaire.QuestionnaireItemType.GROUP && + !questionnaireItem.repeats + ) { + addMissingResponseItems(questionnaireItem.item, responseItems[currentIndex].item) + } + } + } + /** * Returns current [QuestionnaireResponse] captured by the UI which includes answers of enabled * questions. diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewModelTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewModelTest.kt index 8685bfc341..ace95f6379 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewModelTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewModelTest.kt @@ -501,7 +501,7 @@ class QuestionnaireViewModelTest { } @Test - fun `should throw exception for non-matching question linkIds`() { + fun `should remove response item of non-matching question linkIds`() { val questionnaire = Questionnaire().apply { id = "a-questionnaire" @@ -528,18 +528,25 @@ class QuestionnaireViewModelTest { ) } - val errorMessage = - assertFailsWith { - createQuestionnaireViewModel(questionnaire, questionnaireResponse) - } - .localizedMessage + val expectedQuestionnaireResponse = + QuestionnaireResponse().apply { + id = "a-questionnaire-response" + addItem( + QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { + linkId = "a-link-id" + text = "Basic question" + } + ) + } - assertThat(errorMessage) - .isEqualTo("Missing questionnaire item for questionnaire response item a-different-link-id") + assertResourceEquals( + createQuestionnaireViewModel(questionnaire, questionnaireResponse).getQuestionnaireResponse(), + expectedQuestionnaireResponse + ) } @Test - fun `should throw an exception for extra questionnaire response items`() { + fun `should remove an extra questionnaire response items`() { val questionnaire = Questionnaire().apply { id = "a-questionnaire" @@ -557,6 +564,7 @@ class QuestionnaireViewModelTest { addItem( QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { linkId = "a-link-id" + text = "Basic question" addAnswer( QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { value = BooleanType(true) @@ -567,6 +575,7 @@ class QuestionnaireViewModelTest { addItem( QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { linkId = "a-different-link-id" + text = "Basic question" addAnswer( QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { value = BooleanType(true) @@ -576,14 +585,26 @@ class QuestionnaireViewModelTest { ) } - val errorMessage = - assertFailsWith { - createQuestionnaireViewModel(questionnaire, questionnaireResponse) - } - .localizedMessage + val expectedQuestionnaireResponse = + QuestionnaireResponse().apply { + id = "a-questionnaire-response" + addItem( + QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { + linkId = "a-link-id" + text = "Basic question" + addAnswer( + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { + value = BooleanType(true) + } + ) + } + ) + } - assertThat(errorMessage) - .isEqualTo("Missing questionnaire item for questionnaire response item a-different-link-id") + assertResourceEquals( + createQuestionnaireViewModel(questionnaire, questionnaireResponse).getQuestionnaireResponse(), + expectedQuestionnaireResponse + ) } @Test @@ -949,40 +970,20 @@ class QuestionnaireViewModelTest { { "resourceType": "Questionnaire", "id": "client-registration-sample", - "language": "en", - "status": "active", - "date": "2020-11-18T07:24:47.111Z", - "subjectType": [ - "Patient" - ], - "extension": [ - { - "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemExtractionContext", - "valueExpression": { - "language": "application/x-fhir-query", - "expression": "Patient", - "name": "patient" - } - } - ], "item": [ { "linkId": "1", - "text": "Group Item", "type": "group", - "required": true, "item": [ { "linkId": "1.1", "text": "First Nested Item", - "type": "boolean", - "required": true + "type": "boolean" }, { "linkId": "1.2", "text": "Second Nested Item", - "type": "boolean", - "required": true + "type": "boolean" } ] } @@ -996,8 +997,7 @@ class QuestionnaireViewModelTest { "resourceType": "QuestionnaireResponse", "item": [ { - "linkId": "1", - "text":"Group Item" + "linkId": "1" } ] } @@ -1010,7 +1010,6 @@ class QuestionnaireViewModelTest { "item": [ { "linkId": "1", - "text": "Group Item", "item": [ { "linkId": "1.1", @@ -1037,6 +1036,129 @@ class QuestionnaireViewModelTest { assertResourceEquals(value, expectedResponse) } + @Test + fun `maintain an order while adding empty QuestionnaireResponseItemComponent to the response items`() { + val questionnaireString = + """ + { + "resourceType": "Questionnaire", + "item": [ + { + "linkId": "1", + "type": "group", + "text": "Repeated Group", + "repeats": true, + "item": [ + { + "linkId": "1-1", + "type": "date", + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/entryFormat", + "valueString": "yyyy-mm-dd" + } + ] + } + ] + }, + { + "linkId": "2", + "text": "Is this the first visit?", + "type": "boolean" + } + ] + } + """.trimIndent() + + val questionnaireResponseString = + """ + { + "resourceType": "QuestionnaireResponse", + "item": [ + { + "linkId": "1", + "text": "Repeated Group", + "item": [ + { + "linkId": "1-1", + "answer": [ + { + "valueDate": "2023-06-14" + } + ] + } + ] + }, + { + "linkId": "1", + "text": "Repeated Group", + "item": [ + { + "linkId": "1-1", + "answer": [ + { + "valueDate": "2023-06-13" + } + ] + } + ] + } + ] + } + """.trimIndent() + + val expectedResponseString = + """ + { + "resourceType": "QuestionnaireResponse", + "item": [ + { + "linkId": "1", + "text": "Repeated Group", + "item": [ + { + "linkId": "1-1", + "answer": [ + { + "valueDate": "2023-06-14" + } + ] + } + ] + }, + { + "linkId": "1", + "text": "Repeated Group", + "item": [ + { + "linkId": "1-1", + "answer": [ + { + "valueDate": "2023-06-13" + } + ] + } + ] + }, + { + "linkId": "2", + "text": "Is this the first visit?" + } + ] + } + """.trimIndent() + + state.set(EXTRA_QUESTIONNAIRE_JSON_STRING, questionnaireString) + state.set(EXTRA_QUESTIONNAIRE_RESPONSE_JSON_STRING, questionnaireResponseString) + val viewModel = QuestionnaireViewModel(context, state) + val value = viewModel.getQuestionnaireResponse() + val expectedResponse = + printer.parseResource(QuestionnaireResponse::class.java, expectedResponseString) + as QuestionnaireResponse + + assertResourceEquals(value, expectedResponse) + } + // ==================================================================== // // // // Questionnaire State Flow // From 68a6d1cb66fde45710cda924f3f86337e06e1f49 Mon Sep 17 00:00:00 2001 From: Santosh Pingle Date: Mon, 19 Jun 2023 13:00:48 +0530 Subject: [PATCH 07/11] Address review comments. --- .../android/fhir/datacapture/QuestionnaireViewModel.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt index 92421be98b..656905db76 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt @@ -402,7 +402,7 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat questionnaireItem.createQuestionnaireResponseItem() ) } - // traverse the nested item list of group type. + // Add missing response items for non-repeating groups if (questionnaireItem.type == Questionnaire.QuestionnaireItemType.GROUP && !questionnaireItem.repeats ) { @@ -430,10 +430,10 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat val questionnaireItem = questionnaireIterator.next() val responseItemComponents = linkedToResponseItemsMap[questionnaireItem.linkId]!! if (!responseItems.containsAll(responseItemComponents) - ) { // checks if response items are already present in the list. + ) { // checks if response items present in the list. responseItems.addAll(responseItemComponents) } - // traverse nested item list of group type. + // Add missing response items for non-repeating groups if (questionnaireItem.type == Questionnaire.QuestionnaireItemType.GROUP && !questionnaireItem.repeats ) { @@ -458,13 +458,13 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat previousIndex = currentIndex val questionnaireItem = questionnaireIterator.next() currentIndex = responseItems.indexOfLast { it.linkId == questionnaireItem.linkId } - // add empty response item if missing. + // add empty response item. if (currentIndex == -1) { currentIndex = previousIndex + 1 var responseItem = questionnaireItem.createQuestionnaireResponseItem() responseItems.add(currentIndex, responseItem) } - // traverse nested item list of group type. + // Add missing response items for non-repeating groups if (questionnaireItem.type == Questionnaire.QuestionnaireItemType.GROUP && !questionnaireItem.repeats ) { From 084231ff54703879b9c3e66931f88fc5dc1fc17f Mon Sep 17 00:00:00 2001 From: Santosh Pingle Date: Fri, 30 Jun 2023 16:38:29 +0530 Subject: [PATCH 08/11] Address review comments. --- .../datacapture/QuestionnaireViewModel.kt | 152 +++++------------- 1 file changed, 39 insertions(+), 113 deletions(-) diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt index 656905db76..7754d6bcfc 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt @@ -134,7 +134,8 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat questionnaireResponse = parser.parseResource(application.contentResolver.openInputStream(uri)) as QuestionnaireResponse - addMissingResponseItems(questionnaire.item, questionnaireResponse.item) + questionnaireResponse.item = + addMissingResponseItems(questionnaire.item, questionnaireResponse.item) checkQuestionnaireResponse(questionnaire, questionnaireResponse) } state.contains(QuestionnaireFragment.EXTRA_QUESTIONNAIRE_RESPONSE_JSON_STRING) -> { @@ -142,7 +143,8 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat state[QuestionnaireFragment.EXTRA_QUESTIONNAIRE_RESPONSE_JSON_STRING]!! questionnaireResponse = parser.parseResource(questionnaireResponseJson) as QuestionnaireResponse - addMissingResponseItems() + questionnaireResponse.item = + addMissingResponseItems(questionnaire.item, questionnaireResponse.item) checkQuestionnaireResponse(questionnaire, questionnaireResponse) } else -> { @@ -355,124 +357,48 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat * might not contain answers to unanswered or disabled questions. Note : this only applies to * [QuestionnaireItemComponent]s nested under a group. */ - private fun addMissingResponseItems() { - val linkIdToResponseItemsMap = - mutableMapOf>() - associateLinkId(questionnaire.item, linkIdToResponseItemsMap) - - addResponseItemsToMap(questionnaire.item, questionnaireResponse.item, linkIdToResponseItemsMap) - - val responseItems = mutableListOf() - buildResponseItemList(linkIdToResponseItemsMap, questionnaire.item, responseItems) - questionnaireResponse.item = responseItems - } - - /** - * Adds linkId of the item present in the list [questionnaireItems] as a key, and an empty list of - * type [QuestionnaireResponseItemComponent] as a value to the map [linkIdToResponseItemsMap]. - */ - private fun associateLinkId( - questionnaireItems: List, - linkIdToResponseItemsMap: MutableMap> - ) { - questionnaireItems.forEach { - linkIdToResponseItemsMap[it.linkId] = mutableListOf() - associateLinkId(it.item, linkIdToResponseItemsMap) - } - } - - /** - * Adds response item present in the list [responseItems] corresponding to the linkId present in - * [questionnaireItems] to the map [linkedToResponseItemsMap], and if response item is missing - * then adds empty response item. - */ - private fun addResponseItemsToMap( - questionnaireItems: List, - responseItems: MutableList, - linkedToResponseItemsMap: MutableMap>, - ) { - val questionnaireIterator = questionnaireItems.iterator() - while (questionnaireIterator.hasNext()) { - val questionnaireItem = questionnaireIterator.next() - val responseItemComponents = responseItems.filter { questionnaireItem.linkId == it.linkId } - if (responseItemComponents.isNotEmpty()) { // add existing response items to the map. - linkedToResponseItemsMap[questionnaireItem.linkId]!!.addAll(responseItemComponents) - } else { // add empty response items to the map - linkedToResponseItemsMap[questionnaireItem.linkId]!!.add( - questionnaireItem.createQuestionnaireResponseItem() - ) - } - // Add missing response items for non-repeating groups - if (questionnaireItem.type == Questionnaire.QuestionnaireItemType.GROUP && - !questionnaireItem.repeats - ) { - addResponseItemsToMap( - questionnaireItem.item, - linkedToResponseItemsMap[questionnaireItem.linkId]!!.first().item, - linkedToResponseItemsMap - ) - } - } - } - - /** - * Adds response items present in the map [linkedToResponseItemsMap] to the list [responseItems]. - * The added items to the list [responseItems] correspond to the linkId of the item present in the - * list [questionnaireItems] - */ - private fun buildResponseItemList( - linkedToResponseItemsMap: MutableMap>, + fun addMissingResponseItems( questionnaireItems: List, responseItems: MutableList, - ) { - val questionnaireIterator = questionnaireItems.iterator() - while (questionnaireIterator.hasNext()) { - val questionnaireItem = questionnaireIterator.next() - val responseItemComponents = linkedToResponseItemsMap[questionnaireItem.linkId]!! - if (!responseItems.containsAll(responseItemComponents) - ) { // checks if response items present in the list. - responseItems.addAll(responseItemComponents) - } - // Add missing response items for non-repeating groups - if (questionnaireItem.type == Questionnaire.QuestionnaireItemType.GROUP && - !questionnaireItem.repeats - ) { - buildResponseItemList( - linkedToResponseItemsMap, - questionnaireItem.item, - responseItemComponents.first().item, + parentResponseItem: QuestionnaireResponseItemComponent? = null, + ): List { + // To associate the linkId to QuestionnaireResponseItemComponent, do not use associateBy(). + // Instead, use groupBy(). + // This is because a questionnaire response may have multiple + // QuestionnaireResponseItemComponents with the same linkId. + val responseItemMap = responseItems.groupBy { it.linkId } + // Create a list to add missing response items to the existing items to update the questionnaire + // response items. + val questionnaireResponseItems = mutableListOf() + questionnaireItems.forEach { + val responseItems = + if (responseItemMap.get(it.linkId).isNullOrEmpty()) { + listOf(it.createQuestionnaireResponseItem()) + } else { + responseItemMap[it.linkId]!! + } + if (it.type == Questionnaire.QuestionnaireItemType.GROUP && !it.repeats) { + val responseItem = responseItems.first() + // Clear the nested list, as the updated list with missing response items will be added back + // to the original list. + val nestedResponseItems = responseItem.copy().item + responseItem.item.clear() + addMissingResponseItems( + questionnaireItems = it.item, + responseItems = nestedResponseItems, + parentResponseItem = responseItem ) } - } - } - - // Index based approach, this function will be removed. - private fun addMissingResponseItems( - questionnaireItems: List, - responseItems: MutableList - ) { - val questionnaireIterator = questionnaireItems.iterator() - var previousIndex = -1 - var currentIndex = -1 - while (questionnaireIterator.hasNext()) { - previousIndex = currentIndex - val questionnaireItem = questionnaireIterator.next() - currentIndex = responseItems.indexOfLast { it.linkId == questionnaireItem.linkId } - // add empty response item. - if (currentIndex == -1) { - currentIndex = previousIndex + 1 - var responseItem = questionnaireItem.createQuestionnaireResponseItem() - responseItems.add(currentIndex, responseItem) - } - // Add missing response items for non-repeating groups - if (questionnaireItem.type == Questionnaire.QuestionnaireItemType.GROUP && - !questionnaireItem.repeats - ) { - addMissingResponseItems(questionnaireItem.item, responseItems[currentIndex].item) + // Now update the existing nested response items by adding missing response items. + // Maintain the existing order of response items while adding missing items. + if (parentResponseItem != null) { + parentResponseItem.item.addAll(responseItems) + } else { + questionnaireResponseItems.addAll(responseItems) } } + return questionnaireResponseItems } - /** * Returns current [QuestionnaireResponse] captured by the UI which includes answers of enabled * questions. From 8ff5037dd609859ab6fc5c009b34f345b6458570 Mon Sep 17 00:00:00 2001 From: Santosh Pingle Date: Fri, 30 Jun 2023 17:48:19 +0530 Subject: [PATCH 09/11] Addres review comments. --- .../android/fhir/datacapture/QuestionnaireViewModel.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt index 7754d6bcfc..86d8664f80 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt @@ -372,7 +372,7 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat val questionnaireResponseItems = mutableListOf() questionnaireItems.forEach { val responseItems = - if (responseItemMap.get(it.linkId).isNullOrEmpty()) { + if (responseItemMap[it.linkId].isNullOrEmpty()) { listOf(it.createQuestionnaireResponseItem()) } else { responseItemMap[it.linkId]!! @@ -380,7 +380,7 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat if (it.type == Questionnaire.QuestionnaireItemType.GROUP && !it.repeats) { val responseItem = responseItems.first() // Clear the nested list, as the updated list with missing response items will be added back - // to the original list. + // to the list. val nestedResponseItems = responseItem.copy().item responseItem.item.clear() addMissingResponseItems( @@ -389,8 +389,8 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat parentResponseItem = responseItem ) } - // Now update the existing nested response items by adding missing response items. - // Maintain the existing order of response items while adding missing items. + // Add the list of existing response items and missing items back to the list, maintaining the + // order of the items. if (parentResponseItem != null) { parentResponseItem.item.addAll(responseItems) } else { From 264ac6deeaa3fa6c1e7e93547c6c765208b846ff Mon Sep 17 00:00:00 2001 From: Santosh Pingle Date: Wed, 12 Jul 2023 18:10:06 +0530 Subject: [PATCH 10/11] Address review comments. --- .../datacapture/QuestionnaireViewModel.kt | 52 +++++++------------ 1 file changed, 18 insertions(+), 34 deletions(-) diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt index 86d8664f80..12622e50ca 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt @@ -134,8 +134,7 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat questionnaireResponse = parser.parseResource(application.contentResolver.openInputStream(uri)) as QuestionnaireResponse - questionnaireResponse.item = - addMissingResponseItems(questionnaire.item, questionnaireResponse.item) + addMissingResponseItems(questionnaire.item, questionnaireResponse.item) checkQuestionnaireResponse(questionnaire, questionnaireResponse) } state.contains(QuestionnaireFragment.EXTRA_QUESTIONNAIRE_RESPONSE_JSON_STRING) -> { @@ -143,8 +142,7 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat state[QuestionnaireFragment.EXTRA_QUESTIONNAIRE_RESPONSE_JSON_STRING]!! questionnaireResponse = parser.parseResource(questionnaireResponseJson) as QuestionnaireResponse - questionnaireResponse.item = - addMissingResponseItems(questionnaire.item, questionnaireResponse.item) + addMissingResponseItems(questionnaire.item, questionnaireResponse.item) checkQuestionnaireResponse(questionnaire, questionnaireResponse) } else -> { @@ -357,47 +355,33 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat * might not contain answers to unanswered or disabled questions. Note : this only applies to * [QuestionnaireItemComponent]s nested under a group. */ - fun addMissingResponseItems( + private fun addMissingResponseItems( questionnaireItems: List, responseItems: MutableList, - parentResponseItem: QuestionnaireResponseItemComponent? = null, - ): List { + ) { // To associate the linkId to QuestionnaireResponseItemComponent, do not use associateBy(). // Instead, use groupBy(). // This is because a questionnaire response may have multiple // QuestionnaireResponseItemComponents with the same linkId. val responseItemMap = responseItems.groupBy { it.linkId } - // Create a list to add missing response items to the existing items to update the questionnaire - // response items. - val questionnaireResponseItems = mutableListOf() + + // Clear the response item list, and then add the missing and existing response items back to + // the list + responseItems.clear() + questionnaireItems.forEach { - val responseItems = - if (responseItemMap[it.linkId].isNullOrEmpty()) { - listOf(it.createQuestionnaireResponseItem()) - } else { - responseItemMap[it.linkId]!! - } - if (it.type == Questionnaire.QuestionnaireItemType.GROUP && !it.repeats) { - val responseItem = responseItems.first() - // Clear the nested list, as the updated list with missing response items will be added back - // to the list. - val nestedResponseItems = responseItem.copy().item - responseItem.item.clear() - addMissingResponseItems( - questionnaireItems = it.item, - responseItems = nestedResponseItems, - parentResponseItem = responseItem - ) - } - // Add the list of existing response items and missing items back to the list, maintaining the - // order of the items. - if (parentResponseItem != null) { - parentResponseItem.item.addAll(responseItems) + if (responseItemMap[it.linkId].isNullOrEmpty()) { + responseItems.add(it.createQuestionnaireResponseItem()) } else { - questionnaireResponseItems.addAll(responseItems) + if (it.type == Questionnaire.QuestionnaireItemType.GROUP && !it.repeats) { + addMissingResponseItems( + questionnaireItems = it.item, + responseItems = responseItemMap[it.linkId]!!.first().item, + ) + } + responseItems.addAll(responseItemMap[it.linkId]!!) } } - return questionnaireResponseItems } /** * Returns current [QuestionnaireResponse] captured by the UI which includes answers of enabled From f9dda737ef80fbccaf7c3455467b710789de97d6 Mon Sep 17 00:00:00 2001 From: Santosh Pingle Date: Wed, 19 Jul 2023 23:15:22 +0530 Subject: [PATCH 11/11] Address review comments. --- .../datacapture/QuestionnaireViewModel.kt | 2 +- .../datacapture/QuestionnaireViewModelTest.kt | 62 ------------------- 2 files changed, 1 insertion(+), 63 deletions(-) diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt index 12622e50ca..36e3cd9c18 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt @@ -376,7 +376,7 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat if (it.type == Questionnaire.QuestionnaireItemType.GROUP && !it.repeats) { addMissingResponseItems( questionnaireItems = it.item, - responseItems = responseItemMap[it.linkId]!!.first().item, + responseItems = responseItemMap[it.linkId]!!.single().item, ) } responseItems.addAll(responseItemMap[it.linkId]!!) diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewModelTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewModelTest.kt index fb82cb227e..e05243d31c 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewModelTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewModelTest.kt @@ -550,68 +550,6 @@ class QuestionnaireViewModelTest { ) } - @Test - fun `should remove an extra questionnaire response items`() { - val questionnaire = - Questionnaire().apply { - id = "a-questionnaire" - addItem( - Questionnaire.QuestionnaireItemComponent().apply { - linkId = "a-link-id" - text = "Basic question" - type = Questionnaire.QuestionnaireItemType.BOOLEAN - } - ) - } - val questionnaireResponse = - QuestionnaireResponse().apply { - id = "a-questionnaire-response" - addItem( - QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { - linkId = "a-link-id" - text = "Basic question" - addAnswer( - QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { - value = BooleanType(true) - } - ) - } - ) - addItem( - QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { - linkId = "a-different-link-id" - text = "Basic question" - addAnswer( - QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { - value = BooleanType(true) - } - ) - } - ) - } - - val expectedQuestionnaireResponse = - QuestionnaireResponse().apply { - id = "a-questionnaire-response" - addItem( - QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { - linkId = "a-link-id" - text = "Basic question" - addAnswer( - QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { - value = BooleanType(true) - } - ) - } - ) - } - - assertResourceEquals( - createQuestionnaireViewModel(questionnaire, questionnaireResponse).getQuestionnaireResponse(), - expectedQuestionnaireResponse - ) - } - @Test fun `should throw exception for non-matching question types`() { val questionnaire =