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

Include the corresponding questionnaireResponseItemComponents for nested items in the group. #2005

Merged
Merged
Show file tree
Hide file tree
Changes from 13 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
Expand Up @@ -134,13 +134,17 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
questionnaireResponse =
parser.parseResource(application.contentResolver.openInputStream(uri))
as QuestionnaireResponse
questionnaireResponse.item =
addMissingResponseItems(questionnaire.item, questionnaireResponse.item)
checkQuestionnaireResponse(questionnaire, questionnaireResponse)
}
state.contains(QuestionnaireFragment.EXTRA_QUESTIONNAIRE_RESPONSE_JSON_STRING) -> {
val questionnaireResponseJson: String =
state[QuestionnaireFragment.EXTRA_QUESTIONNAIRE_RESPONSE_JSON_STRING]!!
questionnaireResponse =
parser.parseResource(questionnaireResponseJson) as QuestionnaireResponse
questionnaireResponse.item =
addMissingResponseItems(questionnaire.item, questionnaireResponse.item)
checkQuestionnaireResponse(questionnaire, questionnaireResponse)
}
else -> {
Expand Down Expand Up @@ -346,6 +350,55 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
private val answerExpressionMap =
mutableMapOf<String, List<Questionnaire.QuestionnaireItemAnswerOptionComponent>>()

/**
* 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.
*/
fun addMissingResponseItems(
questionnaireItems: List<QuestionnaireItemComponent>,
responseItems: MutableList<QuestionnaireResponseItemComponent>,
parentResponseItem: QuestionnaireResponseItemComponent? = null,
santosh-pingle marked this conversation as resolved.
Show resolved Hide resolved
): List<QuestionnaireResponseItemComponent> {
// 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<QuestionnaireResponseItemComponent>()
questionnaireItems.forEach {
val responseItems =
if (responseItemMap[it.linkId].isNullOrEmpty()) {
listOf(it.createQuestionnaireResponseItem())
} else {
responseItemMap[it.linkId]!!
}
if (it.type == Questionnaire.QuestionnaireItemType.GROUP && !it.repeats) {
santosh-pingle marked this conversation as resolved.
Show resolved Hide resolved
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)
} else {
questionnaireResponseItems.addAll(responseItems)
}
santosh-pingle marked this conversation as resolved.
Show resolved Hide resolved
}
return questionnaireResponseItems
}
/**
* Returns current [QuestionnaireResponse] captured by the UI which includes answers of enabled
* questions.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,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"
Expand All @@ -533,18 +533,25 @@ class QuestionnaireViewModelTest {
)
}

val errorMessage =
assertFailsWith<IllegalArgumentException> {
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`() {
santosh-pingle marked this conversation as resolved.
Show resolved Hide resolved
val questionnaire =
Questionnaire().apply {
id = "a-questionnaire"
Expand All @@ -562,6 +569,7 @@ class QuestionnaireViewModelTest {
addItem(
QuestionnaireResponse.QuestionnaireResponseItemComponent().apply {
linkId = "a-link-id"
text = "Basic question"
addAnswer(
QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply {
value = BooleanType(true)
Expand All @@ -572,6 +580,7 @@ class QuestionnaireViewModelTest {
addItem(
QuestionnaireResponse.QuestionnaireResponseItemComponent().apply {
linkId = "a-different-link-id"
text = "Basic question"
addAnswer(
QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply {
value = BooleanType(true)
Expand All @@ -581,14 +590,26 @@ class QuestionnaireViewModelTest {
)
}

val errorMessage =
assertFailsWith<IllegalArgumentException> {
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
Expand Down Expand Up @@ -947,6 +968,202 @@ class QuestionnaireViewModelTest {
createQuestionnaireViewModel(questionnaire, questionnaireResponse)
}

@Test
fun `add empty nested QuestionnaireResponseItemComponent to the group if questions are not answered`() {
val questionnaireString =
"""
{
"resourceType": "Questionnaire",
"id": "client-registration-sample",
"item": [
{
"linkId": "1",
"type": "group",
"item": [
{
"linkId": "1.1",
"text": "First Nested Item",
"type": "boolean"
},
{
"linkId": "1.2",
"text": "Second Nested Item",
"type": "boolean"
}
]
}
]
}
""".trimIndent()

val questionnaireResponseString =
"""
{
"resourceType": "QuestionnaireResponse",
"item": [
{
"linkId": "1"
}
]
}
""".trimIndent()

val expectedResponseString =
"""
{
"resourceType": "QuestionnaireResponse",
"item": [
{
"linkId": "1",
"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)
}

@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 //
Expand Down