Skip to content

Commit bdaff86

Browse files
authored
Merge branch 'master' into feature/add-custom-callback-for-autocomplete
2 parents 1adc09e + bf19b7a commit bdaff86

36 files changed

+2751
-274
lines changed

buildSrc/src/main/kotlin/Releases.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2023-2024 Google LLC
2+
* Copyright 2023-2025 Google LLC
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -48,13 +48,13 @@ object Releases {
4848

4949
object Engine : LibraryArtifact {
5050
override val artifactId = "engine"
51-
override val version = "1.1.0"
51+
override val version = "1.2.0"
5252
override val name = "Android FHIR Engine Library"
5353
}
5454

5555
object DataCapture : LibraryArtifact {
5656
override val artifactId = "data-capture"
57-
override val version = "1.2.0"
57+
override val version = "1.3.0"
5858
override val name = "Android FHIR Structured Data Capture Library"
5959
}
6060

datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -299,12 +299,6 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
299299

300300
private lateinit var currentPageItems: List<QuestionnaireAdapterItem>
301301

302-
/**
303-
* True if the user has tapped the next/previous pagination buttons on the current page. This is
304-
* needed to avoid spewing validation errors before any questions are answered.
305-
*/
306-
private var forceValidation = false
307-
308302
/**
309303
* Map of [QuestionnaireResponseItemAnswerComponent] for
310304
* [Questionnaire.QuestionnaireItemComponent]s that are disabled now. The answers will be used to
@@ -921,7 +915,6 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
921915
val validationResult =
922916
if (
923917
modifiedQuestionnaireResponseItemSet.contains(questionnaireResponseItem) ||
924-
forceValidation ||
925918
isInReviewModeFlow.value
926919
) {
927920
questionnaireResponseItemValidator.validate(
@@ -1144,13 +1137,14 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
11441137
it.item.validationResult is NotValidated
11451138
}
11461139
) {
1147-
// Force update validation results for all questions on the current page. This is needed
1148-
// when the user has not answered any questions so no validation has been done.
1149-
forceValidation = true
1140+
// Add all items on the current page to modifiedQuestionnaireResponseItemSet.
1141+
// This will ensure that all fields are validated even when they're not filled by the user
1142+
currentPageItems.filterIsInstance<QuestionnaireAdapterItem.Question>().forEach {
1143+
modifiedQuestionnaireResponseItemSet.add(it.item.getQuestionnaireResponseItem())
1144+
}
11501145
// Results in a new questionnaire state being generated synchronously, i.e., the current
11511146
// thread will be suspended until the new state is generated.
11521147
modificationCount.update { it + 1 }
1153-
forceValidation = false
11541148
}
11551149

11561150
if (

datacapture/src/main/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseValidator.kt

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2022-2024 Google LLC
2+
* Copyright 2022-2025 Google LLC
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -151,10 +151,12 @@ object QuestionnaireResponseValidator {
151151
questionnaireResponseItemValidator: QuestionnaireResponseItemValidator,
152152
linkIdToValidationResultMap: MutableMap<String, MutableList<ValidationResult>>,
153153
): Map<String, List<ValidationResult>> {
154-
when (checkNotNull(questionnaireItem.type) { "Questionnaire item must have type" }) {
155-
Questionnaire.QuestionnaireItemType.DISPLAY,
156-
Questionnaire.QuestionnaireItemType.NULL, -> Unit
157-
Questionnaire.QuestionnaireItemType.GROUP ->
154+
checkNotNull(questionnaireItem.type) { "Questionnaire item must have type" }
155+
when {
156+
questionnaireItem.type == Questionnaire.QuestionnaireItemType.DISPLAY ||
157+
questionnaireItem.type == Questionnaire.QuestionnaireItemType.NULL -> Unit
158+
questionnaireItem.type == Questionnaire.QuestionnaireItemType.GROUP &&
159+
!questionnaireItem.repeats ->
158160
// Nested items under group
159161
// http://www.hl7.org/fhir/questionnaireresponse-definitions.html#QuestionnaireResponse.item.item
160162
validateQuestionnaireResponseItems(
@@ -262,10 +264,13 @@ object QuestionnaireResponseValidator {
262264
questionnaireItem: Questionnaire.QuestionnaireItemComponent,
263265
questionnaireResponseItem: QuestionnaireResponse.QuestionnaireResponseItemComponent,
264266
) {
265-
when (checkNotNull(questionnaireItem.type) { "Questionnaire item must have type" }) {
266-
Questionnaire.QuestionnaireItemType.DISPLAY,
267-
Questionnaire.QuestionnaireItemType.NULL, -> Unit
268-
Questionnaire.QuestionnaireItemType.GROUP ->
267+
checkNotNull(questionnaireItem.type) { "Questionnaire item must have type" }
268+
269+
when {
270+
questionnaireItem.type == Questionnaire.QuestionnaireItemType.DISPLAY ||
271+
questionnaireItem.type == Questionnaire.QuestionnaireItemType.NULL -> Unit
272+
questionnaireItem.type == Questionnaire.QuestionnaireItemType.GROUP &&
273+
!questionnaireItem.repeats ->
269274
// Nested items under group
270275
// http://www.hl7.org/fhir/questionnaireresponse-definitions.html#QuestionnaireResponse.item.item
271276
checkQuestionnaireResponseItems(questionnaireItem.item, questionnaireResponseItem.item)

datacapture/src/main/res/layout/edit_text_single_line_view.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
<com.google.android.material.textfield.TextInputEditText
5353
android:id="@+id/text_input_edit_text"
5454
style="?attr/questionnaireTextInputEditTextStyle"
55+
android:inputType="text"
5556
android:layout_width="match_parent"
5657
android:layout_height="wrap_content"
5758
android:maxLines="1"

datacapture/src/test/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseValidatorTest.kt

Lines changed: 138 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2022-2024 Google LLC
2+
* Copyright 2022-2025 Google LLC
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -20,6 +20,7 @@ import android.content.Context
2020
import android.os.Build
2121
import androidx.test.core.app.ApplicationProvider
2222
import com.google.android.fhir.datacapture.extensions.EXTENSION_HIDDEN_URL
23+
import com.google.android.fhir.datacapture.extensions.packRepeatedGroups
2324
import com.google.common.truth.Truth.assertThat
2425
import java.math.BigDecimal
2526
import kotlinx.coroutines.test.runTest
@@ -596,6 +597,79 @@ class QuestionnaireResponseValidatorTest {
596597
)
597598
}
598599

600+
@Test
601+
fun `validation fails for required item in a questionnaire repeating group item with answer value`() {
602+
val questionnaire1 =
603+
Questionnaire().apply {
604+
url = "questionnaire-1"
605+
addItem(
606+
Questionnaire.QuestionnaireItemComponent(
607+
StringType("group-1"),
608+
Enumeration(
609+
Questionnaire.QuestionnaireItemTypeEnumFactory(),
610+
Questionnaire.QuestionnaireItemType.GROUP,
611+
),
612+
)
613+
.apply {
614+
repeats = true
615+
addItem(
616+
Questionnaire.QuestionnaireItemComponent(
617+
StringType("question-0"),
618+
Enumeration(
619+
Questionnaire.QuestionnaireItemTypeEnumFactory(),
620+
Questionnaire.QuestionnaireItemType.INTEGER,
621+
),
622+
)
623+
.apply { required = true },
624+
)
625+
},
626+
)
627+
}
628+
629+
val questionnaireResponse1 =
630+
QuestionnaireResponse()
631+
.apply {
632+
questionnaire = "questionnaire-1"
633+
addItem(
634+
QuestionnaireResponse.QuestionnaireResponseItemComponent(StringType("group-1")).apply {
635+
addItem(
636+
QuestionnaireResponse.QuestionnaireResponseItemComponent(StringType("question-0"))
637+
.apply {
638+
addAnswer(
639+
QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply {
640+
value = IntegerType(1)
641+
},
642+
)
643+
},
644+
)
645+
},
646+
)
647+
648+
addItem(
649+
QuestionnaireResponse.QuestionnaireResponseItemComponent(StringType("group-1")).apply {
650+
addItem(
651+
QuestionnaireResponse.QuestionnaireResponseItemComponent(StringType("question-0")),
652+
)
653+
},
654+
)
655+
}
656+
.apply { packRepeatedGroups(questionnaire1) }
657+
658+
runTest {
659+
val result =
660+
QuestionnaireResponseValidator.validateQuestionnaireResponse(
661+
questionnaire1,
662+
questionnaireResponse1,
663+
context,
664+
)
665+
666+
assertThat(result.keys).containsExactly("question-0", "group-1")
667+
assertThat(result["question-0"]!!.first()).isInstanceOf(Invalid::class.java)
668+
assertThat((result["question-0"]!!.first() as Invalid).getSingleStringValidationMessage())
669+
.isEqualTo("Missing answer for required field.")
670+
}
671+
}
672+
599673
@Test
600674
fun `check passes if questionnaire response matches questionnaire`() {
601675
QuestionnaireResponseValidator.checkQuestionnaireResponse(
@@ -1653,6 +1727,69 @@ class QuestionnaireResponseValidatorTest {
16531727
)
16541728
}
16551729

1730+
@Test
1731+
fun `check fails for wrong answer type to a nested question in repeating group`() {
1732+
assertException_checkQuestionnaireResponse_throwsIllegalArgumentException(
1733+
Questionnaire().apply {
1734+
url = "questionnaire-1"
1735+
addItem(
1736+
Questionnaire.QuestionnaireItemComponent(
1737+
StringType("group-1"),
1738+
Enumeration(
1739+
Questionnaire.QuestionnaireItemTypeEnumFactory(),
1740+
Questionnaire.QuestionnaireItemType.GROUP,
1741+
),
1742+
)
1743+
.apply {
1744+
repeats = true
1745+
addItem(
1746+
Questionnaire.QuestionnaireItemComponent(
1747+
StringType("question-0"),
1748+
Enumeration(
1749+
Questionnaire.QuestionnaireItemTypeEnumFactory(),
1750+
Questionnaire.QuestionnaireItemType.INTEGER,
1751+
),
1752+
),
1753+
)
1754+
},
1755+
)
1756+
},
1757+
QuestionnaireResponse().apply {
1758+
questionnaire = "questionnaire-1"
1759+
addItem(
1760+
QuestionnaireResponse.QuestionnaireResponseItemComponent(StringType("group-1")).apply {
1761+
addItem(
1762+
QuestionnaireResponse.QuestionnaireResponseItemComponent(StringType("question-0"))
1763+
.apply {
1764+
addAnswer(
1765+
QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply {
1766+
value = IntegerType(1)
1767+
},
1768+
)
1769+
},
1770+
)
1771+
},
1772+
)
1773+
1774+
addItem(
1775+
QuestionnaireResponse.QuestionnaireResponseItemComponent(StringType("group-1")).apply {
1776+
addItem(
1777+
QuestionnaireResponse.QuestionnaireResponseItemComponent(StringType("question-0"))
1778+
.apply {
1779+
addAnswer(
1780+
QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply {
1781+
value = DecimalType(2.0)
1782+
},
1783+
)
1784+
},
1785+
)
1786+
},
1787+
)
1788+
},
1789+
"Mismatching question type INTEGER and answer type decimal for question-0",
1790+
)
1791+
}
1792+
16561793
private fun assertException_checkQuestionnaireResponse_throwsIllegalArgumentException(
16571794
questionnaire: Questionnaire,
16581795
questionnaireResponse: QuestionnaireResponse,

0 commit comments

Comments
 (0)