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 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
Expand Up @@ -134,13 +134,15 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
questionnaireResponse =
parser.parseResource(application.contentResolver.openInputStream(uri))
as QuestionnaireResponse
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
addMissingResponseItems(questionnaire.item, questionnaireResponse.item)
checkQuestionnaireResponse(questionnaire, questionnaireResponse)
}
else -> {
Expand Down Expand Up @@ -346,6 +348,41 @@ 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.
*/
private fun addMissingResponseItems(
questionnaireItems: List<QuestionnaireItemComponent>,
responseItems: MutableList<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 }

// Clear the response item list, and then add the missing and existing response items back to
// the list
responseItems.clear()

questionnaireItems.forEach {
if (responseItemMap[it.linkId].isNullOrEmpty()) {
responseItems.add(it.createQuestionnaireResponseItem())
} else {
if (it.type == Questionnaire.QuestionnaireItemType.GROUP && !it.repeats) {
addMissingResponseItems(
questionnaireItems = it.item,
responseItems = responseItemMap[it.linkId]!!.single().item,
)
}
responseItems.addAll(responseItemMap[it.linkId]!!)
}
}
}
/**
* 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,62 +533,21 @@ class QuestionnaireViewModelTest {
)
}

val errorMessage =
assertFailsWith<IllegalArgumentException> {
createQuestionnaireViewModel(questionnaire, questionnaireResponse)
}
.localizedMessage

assertThat(errorMessage)
.isEqualTo("Missing questionnaire item for questionnaire response item a-different-link-id")
}

@Test
fun `should throw an exception for 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 =
val expectedQuestionnaireResponse =
QuestionnaireResponse().apply {
id = "a-questionnaire-response"
addItem(
QuestionnaireResponse.QuestionnaireResponseItemComponent().apply {
linkId = "a-link-id"
addAnswer(
QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply {
value = BooleanType(true)
}
)
}
)
addItem(
QuestionnaireResponse.QuestionnaireResponseItemComponent().apply {
linkId = "a-different-link-id"
addAnswer(
QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply {
value = BooleanType(true)
}
)
text = "Basic question"
}
)
}

val errorMessage =
assertFailsWith<IllegalArgumentException> {
createQuestionnaireViewModel(questionnaire, questionnaireResponse)
}
.localizedMessage

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 +906,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