Skip to content

Commit

Permalink
Support repeated groups
Browse files Browse the repository at this point in the history
  • Loading branch information
kevinmost committed Oct 18, 2022
1 parent ad22145 commit f7e512f
Show file tree
Hide file tree
Showing 9 changed files with 285 additions and 43 deletions.
97 changes: 97 additions & 0 deletions catalog/src/main/assets/repeated_group_questionnaire.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
{
"resourceType": "Questionnaire",
"item": [
{
"linkId": "1",
"type": "group",
"text": "COVID Immunizations",
"repeats": true,
"item": [
{
"linkId": "1-1",
"text": "Immunization",
"type": "display"
},
{
"linkId": "1-2",
"text": "Please enter the date of a received COVID vaccine.",
"type": "date",
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/entryFormat",
"valueString": "yyyy-mm-dd"
}
]
},
{
"linkId": "1-3",
"type": "choice",
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl",
"valueCodeableConcept": {
"coding": [
{
"system": "http://hl7.org/fhir/questionnaire-item-control",
"code": "drop-down",
"display": "Drop down"
}
],
"text": "Drop down"
}
}
],
"text": "Please select the type of vaccine received",
"answerOption": [
{
"valueCoding": {
"code": "pfizer-biontech",
"display": "Pfizer-BioNTech"
}
},
{
"valueCoding": {
"code": "moderna",
"display": "Moderna"
}
},
{
"valueCoding": {
"code": "jj",
"display": "Johnson & Johnson's (Janssen)"
}
},
{
"valueCoding": {
"code": "other",
"display": "Other"
}
}
],
"item": [
{
"linkId": "1-3-1",
"text": "Vaccine type",
"type": "display",
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl",
"valueCodeableConcept": {
"coding": [
{
"system": "http://hl7.org/fhir/questionnaire-item-control",
"code": "flyover",
"display": "Fly-over"
}
],
"text": "Flyover"
}
}
]
}
]
}
]
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -109,5 +109,10 @@ class ComponentListViewModel(application: Application, private val state: SavedS
"component_auto_complete.json",
"component_auto_complete_with_validation.json"
),
REPEATED_GROUP(
R.drawable.ic_textfield,
R.string.component_name_repeated_group,
"repeated_group_questionnaire.json",
),
}
}
8 changes: 6 additions & 2 deletions catalog/src/main/res/menu/component_menu.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
android:icon="@drawable/ic_icon"
android:title="@string/error"
app:showAsAction="always"
>
</item>
/>
<item
android:id="@+id/submit_questionnaire"
android:title="@string/submit"
app:showAsAction="never"
/>
</menu>
3 changes: 2 additions & 1 deletion catalog/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
<string name="component_name_dropdown">Dropdown</string>
<string name="component_name_image">Image</string>
<string name="component_name_auto_complete">Auto Complete</string>
<string name="layout_name_default_text">Default</string>
<string name="component_name_repeated_group">Repeated Group</string>
<string name="layout_name_default_text">Default</string>
<string name="layout_name_paginated">Paginated</string>
<string name="layout_name_review">Review</string>
<string name="layout_name_read_only">Read only</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -532,5 +532,5 @@ fun List<Questionnaire.QuestionnaireItemComponent>.flattened():
* The hierarchy and order of child items will be retained as specified in the standard. See
* https://www.hl7.org/fhir/questionnaireresponse.html#notes for more details.
*/
private inline fun Questionnaire.QuestionnaireItemComponent.getNestedQuestionnaireResponseItems() =
fun Questionnaire.QuestionnaireItemComponent.getNestedQuestionnaireResponseItems() =
item.map { it.createQuestionnaireResponseItem() }
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
/** Adds each child-parent pair in the [Questionnaire] to the parent map. */
fun buildParentList(
item: Questionnaire.QuestionnaireItemComponent,
questionnaireItemToParentMap: ItemToParentMap
questionnaireItemToParentMap: ItemToParentMap,
) {
for (child in item.item) {
questionnaireItemToParentMap[child] = item
Expand Down Expand Up @@ -632,8 +632,8 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
} else {
NotValidated
}
val items =
listOf(
val items = buildList {
add(
QuestionnaireItemViewItem(
questionnaireItem,
questionnaireResponseItem,
Expand All @@ -642,22 +642,27 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
resolveAnswerValueSet = { resolveAnswerValueSet(it) },
resolveAnswerExpression = { resolveAnswerExpression(it) }
)
) +
getQuestionnaireItemViewItems(
// If nested display item is identified as instructions or flyover, then do not create
// questionnaire state for it.
questionnaireItemList =
questionnaireItem.item.filterNot {
it.type == Questionnaire.QuestionnaireItemType.DISPLAY &&
(it.isInstructionsCode || it.isFlyoverCode || it.isHelpCode)
},
questionnaireResponseItemList =
if (questionnaireResponseItem.answer.isEmpty()) {
questionnaireResponseItem.item
} else {
questionnaireResponseItem.answer.first().item
},
)
)
questionnaireResponseItem.answer
.map { it.item }
.ifEmpty {
if (questionnaireItem.repeats) emptyList() else listOf(questionnaireResponseItem.item)
}
.forEach { nestedResponse ->
addAll(
getQuestionnaireItemViewItems(
// If nested display item is identified as instructions or flyover, then do not create
// questionnaire state for it.
questionnaireItemList =
questionnaireItem.item.filterNot {
it.type == Questionnaire.QuestionnaireItemType.DISPLAY &&
(it.isInstructionsCode || it.isFlyoverCode || it.isHelpCode)
},
questionnaireResponseItemList = nestedResponse,
)
)
}
}
currentPageItems = items
return items
}
Expand All @@ -680,20 +685,59 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
findEnableWhenQuestionnaireResponseItem(item, linkId) ?: return@evaluate null
}
}
.map { (questionnaireItem, questionnaireResponseItem) ->
questionnaireResponseItem.text = questionnaireItem.localizedTextSpanned?.toString()
// Nested group items
questionnaireResponseItem.item =
getEnabledResponseItems(questionnaireItem.item, questionnaireResponseItem.item)
// Nested question items
questionnaireResponseItem.answer.forEach {
it.item = getEnabledResponseItems(questionnaireItem.item, it.item)
.flatMap { (questionnaireItem, questionnaireResponseItem) ->
if (questionnaireItem.type == Questionnaire.QuestionnaireItemType.GROUP &&
questionnaireItem.repeats
) {
createRepeatedGroupResponse(questionnaireItem, questionnaireResponseItem)
} else {
listOf(
questionnaireResponseItem.apply {
text = questionnaireItem.localizedTextSpanned?.toString()
// Nested group items
item = getEnabledResponseItems(questionnaireItem.item, questionnaireResponseItem.item)
// Nested question items
answer.forEach { it.item = getEnabledResponseItems(questionnaireItem.item, it.item) }
}
)
}
questionnaireResponseItem
}
.toList()
}

/**
* Repeated groups need some massaging for their returned data-format; each instance of the group
* should be flattened out to be its own item in the parent, rather than an answer to the main
* item. See discussion:
* http://community.fhir.org/t/questionnaire-repeating-groups-what-is-the-correct-format/2276/3
*
* For example, if the group contains 2 questions, and the user answered the group 3 times, this
* function will return a list with 3 responses; each of those responses will have the linkId of
* the provided group, and each will contain an item array with 2 items (the answers to the
* individual questions within this particular group instance).
*/
private fun createRepeatedGroupResponse(
questionnaireItem: Questionnaire.QuestionnaireItemComponent,
questionnaireResponseItem: QuestionnaireResponse.QuestionnaireResponseItemComponent
): List<QuestionnaireResponse.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 {
linkId = questionnaireItem.linkId
text = questionnaireItem.localizedTextSpanned?.toString()
item =
getEnabledResponseItems(
questionnaireItemList = individualQuestions,
questionnaireResponseItemList = responsesToIndividualQuestions,
)
}
}
}

/**
* Checks if this questionnaire uses pagination via the "page" extension.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,43 @@
package com.google.android.fhir.datacapture.views

import android.view.View
import android.widget.Button
import android.widget.TextView
import com.google.android.fhir.datacapture.R
import com.google.android.fhir.datacapture.getNestedQuestionnaireResponseItems
import com.google.android.fhir.datacapture.validation.Invalid
import com.google.android.fhir.datacapture.validation.NotValidated
import com.google.android.fhir.datacapture.validation.Valid
import com.google.android.fhir.datacapture.validation.ValidationResult
import org.hl7.fhir.r4.model.QuestionnaireResponse

internal object QuestionnaireItemGroupViewHolderFactory :
QuestionnaireItemViewHolderFactory(R.layout.questionnaire_item_group_header_view) {
override fun getQuestionnaireItemViewHolderDelegate() =
object : QuestionnaireItemViewHolderDelegate {
private lateinit var header: QuestionnaireGroupTypeHeaderView
private lateinit var error: TextView
private lateinit var addItemButton: Button
override lateinit var questionnaireItemViewItem: QuestionnaireItemViewItem

override fun init(itemView: View) {
header = itemView.findViewById(R.id.header)
error = itemView.findViewById(R.id.error)
addItemButton = itemView.findViewById(R.id.add_item)
}

override fun bind(questionnaireItemViewItem: QuestionnaireItemViewItem) {
header.bind(questionnaireItemViewItem.questionnaireItem)
addItemButton.visibility =
if (questionnaireItemViewItem.questionnaireItem.repeats) View.VISIBLE else View.GONE
addItemButton.setOnClickListener {
questionnaireItemViewItem.addAnswer(
QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply {
item =
questionnaireItemViewItem.questionnaireItem.getNestedQuestionnaireResponseItems()
}
)
}
}

override fun displayValidationResult(validationResult: ValidationResult) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,37 @@
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:orientation="vertical"
>
<com.google.android.fhir.datacapture.views.QuestionnaireGroupTypeHeaderView
android:id="@+id/header"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/item_margin_horizontal"
android:layout_marginTop="@dimen/padding_default"
/>
android:orientation="horizontal"
>
<com.google.android.fhir.datacapture.views.QuestionnaireGroupTypeHeaderView
android:id="@+id/header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/padding_default"
android:layout_marginHorizontal="@dimen/item_margin_horizontal"
/>

<include
android:id="@+id/error"
layout="@layout/input_error_text_view"
<include
android:id="@+id/error"
layout="@layout/input_error_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/item_margin_horizontal"
android:layout_marginVertical="@dimen/item_margin_vertical"
/>
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/add_item"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/item_margin_horizontal"
android:layout_marginVertical="@dimen/item_margin_vertical"
android:layout_gravity="center_horizontal"
android:orientation="vertical"
android:text="Add item"
/>

</LinearLayout>
Loading

0 comments on commit f7e512f

Please sign in to comment.