Skip to content

Commit

Permalink
Merge branch 'master' into add-empty-response-items-for-repeat-groups
Browse files Browse the repository at this point in the history
  • Loading branch information
ndegwamartin authored Feb 3, 2025
2 parents d2eb1e4 + 66e14ea commit 8b32701
Show file tree
Hide file tree
Showing 23 changed files with 1,750 additions and 229 deletions.
6 changes: 3 additions & 3 deletions buildSrc/src/main/kotlin/Releases.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2023-2024 Google LLC
* Copyright 2023-2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -48,13 +48,13 @@ object Releases {

object Engine : LibraryArtifact {
override val artifactId = "engine"
override val version = "1.1.0"
override val version = "1.2.0"
override val name = "Android FHIR Engine Library"
}

object DataCapture : LibraryArtifact {
override val artifactId = "data-capture"
override val version = "1.2.0"
override val version = "1.3.0"
override val name = "Android FHIR Structured Data Capture Library"
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2023-2024 Google LLC
* Copyright 2023-2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -298,12 +298,6 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat

private lateinit var currentPageItems: List<QuestionnaireAdapterItem>

/**
* True if the user has tapped the next/previous pagination buttons on the current page. This is
* needed to avoid spewing validation errors before any questions are answered.
*/
private var forceValidation = false

/**
* Map of [QuestionnaireResponseItemAnswerComponent] for
* [Questionnaire.QuestionnaireItemComponent]s that are disabled now. The answers will be used to
Expand Down Expand Up @@ -903,7 +897,6 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
val validationResult =
if (
modifiedQuestionnaireResponseItemSet.contains(questionnaireResponseItem) ||
forceValidation ||
isInReviewModeFlow.value
) {
questionnaireResponseItemValidator.validate(
Expand Down Expand Up @@ -1124,13 +1117,14 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
it.item.validationResult is NotValidated
}
) {
// Force update validation results for all questions on the current page. This is needed
// when the user has not answered any questions so no validation has been done.
forceValidation = true
// Add all items on the current page to modifiedQuestionnaireResponseItemSet.
// This will ensure that all fields are validated even when they're not filled by the user
currentPageItems.filterIsInstance<QuestionnaireAdapterItem.Question>().forEach {
modifiedQuestionnaireResponseItemSet.add(it.item.getQuestionnaireResponseItem())
}
// Results in a new questionnaire state being generated synchronously, i.e., the current
// thread will be suspended until the new state is generated.
modificationCount.update { it + 1 }
forceValidation = false
}

if (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2022-2024 Google LLC
* Copyright 2022-2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -151,10 +151,12 @@ object QuestionnaireResponseValidator {
questionnaireResponseItemValidator: QuestionnaireResponseItemValidator,
linkIdToValidationResultMap: MutableMap<String, MutableList<ValidationResult>>,
): Map<String, List<ValidationResult>> {
when (checkNotNull(questionnaireItem.type) { "Questionnaire item must have type" }) {
Questionnaire.QuestionnaireItemType.DISPLAY,
Questionnaire.QuestionnaireItemType.NULL, -> Unit
Questionnaire.QuestionnaireItemType.GROUP ->
checkNotNull(questionnaireItem.type) { "Questionnaire item must have type" }
when {
questionnaireItem.type == Questionnaire.QuestionnaireItemType.DISPLAY ||
questionnaireItem.type == Questionnaire.QuestionnaireItemType.NULL -> Unit
questionnaireItem.type == Questionnaire.QuestionnaireItemType.GROUP &&
!questionnaireItem.repeats ->
// Nested items under group
// http://www.hl7.org/fhir/questionnaireresponse-definitions.html#QuestionnaireResponse.item.item
validateQuestionnaireResponseItems(
Expand Down Expand Up @@ -262,10 +264,13 @@ object QuestionnaireResponseValidator {
questionnaireItem: Questionnaire.QuestionnaireItemComponent,
questionnaireResponseItem: QuestionnaireResponse.QuestionnaireResponseItemComponent,
) {
when (checkNotNull(questionnaireItem.type) { "Questionnaire item must have type" }) {
Questionnaire.QuestionnaireItemType.DISPLAY,
Questionnaire.QuestionnaireItemType.NULL, -> Unit
Questionnaire.QuestionnaireItemType.GROUP ->
checkNotNull(questionnaireItem.type) { "Questionnaire item must have type" }

when {
questionnaireItem.type == Questionnaire.QuestionnaireItemType.DISPLAY ||
questionnaireItem.type == Questionnaire.QuestionnaireItemType.NULL -> Unit
questionnaireItem.type == Questionnaire.QuestionnaireItemType.GROUP &&
!questionnaireItem.repeats ->
// Nested items under group
// http://www.hl7.org/fhir/questionnaireresponse-definitions.html#QuestionnaireResponse.item.item
checkQuestionnaireResponseItems(questionnaireItem.item, questionnaireResponseItem.item)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2022-2024 Google LLC
* Copyright 2022-2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -20,6 +20,7 @@ import android.content.Context
import android.os.Build
import androidx.test.core.app.ApplicationProvider
import com.google.android.fhir.datacapture.extensions.EXTENSION_HIDDEN_URL
import com.google.android.fhir.datacapture.extensions.packRepeatedGroups
import com.google.common.truth.Truth.assertThat
import java.math.BigDecimal
import kotlinx.coroutines.test.runTest
Expand Down Expand Up @@ -596,6 +597,79 @@ class QuestionnaireResponseValidatorTest {
)
}

@Test
fun `validation fails for required item in a questionnaire repeating group item with answer value`() {
val questionnaire1 =
Questionnaire().apply {
url = "questionnaire-1"
addItem(
Questionnaire.QuestionnaireItemComponent(
StringType("group-1"),
Enumeration(
Questionnaire.QuestionnaireItemTypeEnumFactory(),
Questionnaire.QuestionnaireItemType.GROUP,
),
)
.apply {
repeats = true
addItem(
Questionnaire.QuestionnaireItemComponent(
StringType("question-0"),
Enumeration(
Questionnaire.QuestionnaireItemTypeEnumFactory(),
Questionnaire.QuestionnaireItemType.INTEGER,
),
)
.apply { required = true },
)
},
)
}

val questionnaireResponse1 =
QuestionnaireResponse()
.apply {
questionnaire = "questionnaire-1"
addItem(
QuestionnaireResponse.QuestionnaireResponseItemComponent(StringType("group-1")).apply {
addItem(
QuestionnaireResponse.QuestionnaireResponseItemComponent(StringType("question-0"))
.apply {
addAnswer(
QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply {
value = IntegerType(1)
},
)
},
)
},
)

addItem(
QuestionnaireResponse.QuestionnaireResponseItemComponent(StringType("group-1")).apply {
addItem(
QuestionnaireResponse.QuestionnaireResponseItemComponent(StringType("question-0")),
)
},
)
}
.apply { packRepeatedGroups(questionnaire1) }

runTest {
val result =
QuestionnaireResponseValidator.validateQuestionnaireResponse(
questionnaire1,
questionnaireResponse1,
context,
)

assertThat(result.keys).containsExactly("question-0", "group-1")
assertThat(result["question-0"]!!.first()).isInstanceOf(Invalid::class.java)
assertThat((result["question-0"]!!.first() as Invalid).getSingleStringValidationMessage())
.isEqualTo("Missing answer for required field.")
}
}

@Test
fun `check passes if questionnaire response matches questionnaire`() {
QuestionnaireResponseValidator.checkQuestionnaireResponse(
Expand Down Expand Up @@ -1653,6 +1727,69 @@ class QuestionnaireResponseValidatorTest {
)
}

@Test
fun `check fails for wrong answer type to a nested question in repeating group`() {
assertException_checkQuestionnaireResponse_throwsIllegalArgumentException(
Questionnaire().apply {
url = "questionnaire-1"
addItem(
Questionnaire.QuestionnaireItemComponent(
StringType("group-1"),
Enumeration(
Questionnaire.QuestionnaireItemTypeEnumFactory(),
Questionnaire.QuestionnaireItemType.GROUP,
),
)
.apply {
repeats = true
addItem(
Questionnaire.QuestionnaireItemComponent(
StringType("question-0"),
Enumeration(
Questionnaire.QuestionnaireItemTypeEnumFactory(),
Questionnaire.QuestionnaireItemType.INTEGER,
),
),
)
},
)
},
QuestionnaireResponse().apply {
questionnaire = "questionnaire-1"
addItem(
QuestionnaireResponse.QuestionnaireResponseItemComponent(StringType("group-1")).apply {
addItem(
QuestionnaireResponse.QuestionnaireResponseItemComponent(StringType("question-0"))
.apply {
addAnswer(
QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply {
value = IntegerType(1)
},
)
},
)
},
)

addItem(
QuestionnaireResponse.QuestionnaireResponseItemComponent(StringType("group-1")).apply {
addItem(
QuestionnaireResponse.QuestionnaireResponseItemComponent(StringType("question-0"))
.apply {
addAnswer(
QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply {
value = DecimalType(2.0)
},
)
},
)
},
)
},
"Mismatching question type INTEGER and answer type decimal for question-0",
)
}

private fun assertException_checkQuestionnaireResponse_throwsIllegalArgumentException(
questionnaire: Questionnaire,
questionnaireResponse: QuestionnaireResponse,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2024 Google LLC
* Copyright 2024-2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -21,6 +21,7 @@ import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ProgressBar
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
Expand Down Expand Up @@ -48,6 +49,7 @@ class PeriodicSyncFragment : Fragment() {
setUpActionBar()
setHasOptionsMenu(true)
refreshPeriodicSynUi()
setUpSyncButtons(view)
}

override fun onOptionsItemSelected(item: MenuItem): Boolean {
Expand All @@ -67,6 +69,30 @@ class PeriodicSyncFragment : Fragment() {
}
}

private fun setUpSyncButtons(view: View) {
val syncNowButton = view.findViewById<Button>(R.id.sync_now_button)
val cancelSyncButton = view.findViewById<Button>(R.id.cancel_sync_button)
syncNowButton.apply {
setOnClickListener {
periodicSyncViewModel.collectPeriodicSyncJobStatus()
toggleButtonVisibility(hiddenButton = syncNowButton, visibleButton = cancelSyncButton)
visibility = View.GONE
}
}
cancelSyncButton.apply {
setOnClickListener {
periodicSyncViewModel.cancelPeriodicSyncJob()
toggleButtonVisibility(hiddenButton = cancelSyncButton, visibleButton = syncNowButton)
visibility = View.GONE
}
}
}

private fun toggleButtonVisibility(hiddenButton: View, visibleButton: View) {
hiddenButton.visibility = View.GONE
visibleButton.visibility = View.VISIBLE
}

private fun refreshPeriodicSynUi() {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
Expand Down
Loading

0 comments on commit 8b32701

Please sign in to comment.