Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import androidx.lifecycle.viewModelScope
import ca.uhn.fhir.context.FhirContext
import ca.uhn.fhir.context.FhirVersionEnum
import ca.uhn.fhir.parser.IParser
import com.google.android.fhir.compareTo
import com.google.android.fhir.datacapture.enablement.EnablementEvaluator
import com.google.android.fhir.datacapture.expressions.EnabledAnswerOptionsEvaluator
import com.google.android.fhir.datacapture.extensions.EntryMode
Expand Down Expand Up @@ -77,12 +78,16 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.flow.withIndex
import kotlinx.coroutines.launch
import org.hl7.fhir.r4.model.DateTimeType
import org.hl7.fhir.r4.model.DateType
import org.hl7.fhir.r4.model.DecimalType
import org.hl7.fhir.r4.model.IntegerType
import org.hl7.fhir.r4.model.Questionnaire
import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemComponent
import org.hl7.fhir.r4.model.QuestionnaireResponse
import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent
import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseItemComponent
import org.hl7.fhir.r4.model.Resource
import org.hl7.fhir.r4.model.Type
import timber.log.Timber

internal class QuestionnaireViewModel(application: Application, state: SavedStateHandle) :
Expand Down Expand Up @@ -204,12 +209,12 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
item: QuestionnaireItemComponent,
questionnaireItemToParentMap: ItemToParentMap,
) {
checkMinAndMaxExtensionValues(item.minValue, item.maxValue)
for (child in item.item) {
questionnaireItemToParentMap[child] = item
buildParentList(child, questionnaireItemToParentMap)
}
}

questionnaireItemParentMap = buildMap {
for (item in questionnaire.item) {
buildParentList(item, this)
Expand Down Expand Up @@ -1135,6 +1140,22 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
block()
}
}

private fun checkMinAndMaxExtensionValues(minValue: Type?, maxValue: Type?) {
if (minValue == null || maxValue == null) {
return
}
if (
(minValue is IntegerType && maxValue is IntegerType) ||
(minValue is DecimalType && maxValue is DecimalType) ||
(minValue is DateType && maxValue is DateType) ||
(minValue is DateTimeType && maxValue is DateTimeType)
) {
if (minValue > maxValue) {
throw IllegalArgumentException("minValue cannot be greater than maxValue")
}
}
}
}

typealias ItemToParentMap = MutableMap<QuestionnaireItemComponent, QuestionnaireItemComponent>
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 @@ -169,10 +169,6 @@ internal object DatePickerViewHolderFactory :
val min = (questionnaireViewItem.minAnswerValue as? DateType)?.value?.time
val max = (questionnaireViewItem.maxAnswerValue as? DateType)?.value?.time

if (min != null && max != null && min > max) {
throw IllegalArgumentException("minValue cannot be greater than maxValue")
}

val listValidators = ArrayList<DateValidator>()
min?.let { listValidators.add(DateValidatorPointForward.from(it)) }
max?.let { listValidators.add(DateValidatorPointBackward.before(it)) }
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 @@ -38,6 +38,7 @@ internal object EditTextIntegerViewHolderFactory :
QuestionnaireItemEditTextViewHolderDelegate(
InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_SIGNED,
) {

override suspend fun handleInput(
editable: Editable,
questionnaireViewItem: QuestionnaireViewItem,
Expand Down Expand Up @@ -90,13 +91,18 @@ internal object EditTextIntegerViewHolderFactory :
questionnaireViewItem,
questionnaireViewItem.validationResult,
)
val minValue =
(questionnaireViewItem.minAnswerValue as? IntegerType)?.value ?: Int.MIN_VALUE
val maxValue =
(questionnaireViewItem.maxAnswerValue as? IntegerType)?.value ?: Int.MAX_VALUE

// Update error message if draft answer present
if (questionnaireViewItem.draftAnswer != null) {
textInputLayout.error =
textInputLayout.context.getString(
R.string.integer_format_validation_error_msg,
formatInteger(Int.MIN_VALUE),
formatInteger(Int.MAX_VALUE),
formatInteger(minValue),
formatInteger(maxValue),
)
}
}
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 @@ -58,9 +58,6 @@ internal object SliderViewHolderFactory : QuestionnaireItemViewHolderFactory(R.l
val answer = questionnaireViewItem.answers.singleOrNull()
val minValue = getMinValue(questionnaireViewItem.minAnswerValue)
val maxValue = getMaxValue(questionnaireViewItem.maxAnswerValue)
if (minValue >= maxValue) {
throw IllegalStateException("minValue $minValue must be smaller than maxValue $maxValue")
}

with(slider) {
clearOnChangeListeners()
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 @@ -86,7 +86,9 @@ import org.hl7.fhir.r4.model.BooleanType
import org.hl7.fhir.r4.model.CodeType
import org.hl7.fhir.r4.model.CodeableConcept
import org.hl7.fhir.r4.model.Coding
import org.hl7.fhir.r4.model.DateTimeType
import org.hl7.fhir.r4.model.DateType
import org.hl7.fhir.r4.model.DecimalType
import org.hl7.fhir.r4.model.Enumerations
import org.hl7.fhir.r4.model.Expression
import org.hl7.fhir.r4.model.Extension
Expand Down Expand Up @@ -228,6 +230,94 @@ class QuestionnaireViewModelTest {
}
}

@Test
fun `should throw exception if minValue is greater than maxValue for integer type`() {
val questionnaire =
Questionnaire().apply {
addItem().apply {
type = Questionnaire.QuestionnaireItemType.INTEGER
addExtension().apply {
url = MIN_VALUE_EXTENSION_URL
setValue(IntegerType(10))
}
addExtension().apply {
url = MAX_VALUE_EXTENSION_URL
setValue(IntegerType(1))
}
}
}
val errorMessage =
assertFailsWith<IllegalArgumentException> { createQuestionnaireViewModel(questionnaire) }
.localizedMessage
assertThat(errorMessage).isEqualTo("minValue cannot be greater than maxValue")
}

@Test
fun `should throw exception if minValue is greater than maxValue for decimal type`() {
val questionnaire =
Questionnaire().apply {
addItem().apply {
type = Questionnaire.QuestionnaireItemType.INTEGER
addExtension().apply {
url = MIN_VALUE_EXTENSION_URL
setValue(DecimalType(10.0))
}
addExtension().apply {
url = MAX_VALUE_EXTENSION_URL
setValue(DecimalType(1.5))
}
}
}
val errorMessage =
assertFailsWith<IllegalArgumentException> { createQuestionnaireViewModel(questionnaire) }
.localizedMessage
assertThat(errorMessage).isEqualTo("minValue cannot be greater than maxValue")
}

@Test
fun `should throw exception if minValue is greater than maxValue for datetime type`() {
val questionnaire =
Questionnaire().apply {
addItem().apply {
type = Questionnaire.QuestionnaireItemType.DATETIME
addExtension().apply {
url = MIN_VALUE_EXTENSION_URL
setValue(DateTimeType("2020-01-01T00:00:00Z"))
}
addExtension().apply {
url = MAX_VALUE_EXTENSION_URL
setValue(DateTimeType("2019-01-01T00:00:00Z"))
}
}
}
val errorMessage =
assertFailsWith<IllegalArgumentException> { createQuestionnaireViewModel(questionnaire) }
.localizedMessage
assertThat(errorMessage).isEqualTo("minValue cannot be greater than maxValue")
}

@Test
fun `should throw exception if minValue is greater than maxValue for date type`() {
val questionnaire =
Questionnaire().apply {
addItem().apply {
type = Questionnaire.QuestionnaireItemType.DATE
addExtension().apply {
url = MIN_VALUE_EXTENSION_URL
setValue(DateType("2020-01-01"))
}
addExtension().apply {
url = MAX_VALUE_EXTENSION_URL
setValue(DateType("2019-01-01"))
}
}
}
val errorMessage =
assertFailsWith<IllegalArgumentException> { createQuestionnaireViewModel(questionnaire) }
.localizedMessage
assertThat(errorMessage).isEqualTo("minValue cannot be greater than maxValue")
}

@Test
fun `should copy nested questions if no response is provided`() {
val questionnaire =
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 @@ -31,7 +31,6 @@ import com.google.android.fhir.datacapture.views.QuestionTextConfiguration
import com.google.android.fhir.datacapture.views.QuestionnaireViewItem
import com.google.android.material.slider.Slider
import com.google.common.truth.Truth.assertThat
import kotlin.test.assertFailsWith
import org.hl7.fhir.r4.model.CodeableConcept
import org.hl7.fhir.r4.model.Coding
import org.hl7.fhir.r4.model.Extension
Expand Down Expand Up @@ -185,29 +184,6 @@ class SliderViewHolderFactoryTest {
assertThat(viewHolder.itemView.findViewById<Slider>(R.id.slider).valueFrom).isEqualTo(0.0F)
}

@Test
fun `throws exception if minValue is greater than maxvalue`() {
assertFailsWith<IllegalStateException> {
viewHolder.bind(
QuestionnaireViewItem(
Questionnaire.QuestionnaireItemComponent().apply {
addExtension().apply {
url = "http://hl7.org/fhir/StructureDefinition/minValue"
setValue(IntegerType("100"))
}
addExtension().apply {
url = "http://hl7.org/fhir/StructureDefinition/maxValue"
setValue(IntegerType("50"))
}
},
QuestionnaireResponse.QuestionnaireResponseItemComponent(),
validationResult = NotValidated,
answersChangedCallback = { _, _, _, _ -> },
),
)
}
}

@Test
fun shouldSetQuestionnaireResponseSliderAnswer() {
var answerHolder: List<QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent>? = null
Expand Down