diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt index e96bd28a50..8140c891ef 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt @@ -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 @@ -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) : @@ -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) @@ -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 diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/DatePickerViewHolderFactory.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/DatePickerViewHolderFactory.kt index 8d65eddea2..aff706052b 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/DatePickerViewHolderFactory.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/DatePickerViewHolderFactory.kt @@ -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. @@ -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() min?.let { listValidators.add(DateValidatorPointForward.from(it)) } max?.let { listValidators.add(DateValidatorPointBackward.before(it)) } diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/EditTextIntegerViewHolderFactory.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/EditTextIntegerViewHolderFactory.kt index f619802494..6d0bcf9c52 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/EditTextIntegerViewHolderFactory.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/EditTextIntegerViewHolderFactory.kt @@ -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. @@ -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, @@ -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), ) } } diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/SliderViewHolderFactory.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/SliderViewHolderFactory.kt index eaec4454e4..a101250cf5 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/SliderViewHolderFactory.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/SliderViewHolderFactory.kt @@ -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. @@ -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() diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewModelTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewModelTest.kt index 659429a95a..b53308e1f6 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewModelTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewModelTest.kt @@ -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. @@ -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 @@ -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 { 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 { 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 { 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 { 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 = diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/views/factories/SliderViewHolderFactoryTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/views/factories/SliderViewHolderFactoryTest.kt index 33195746bf..f2c5073ebc 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/views/factories/SliderViewHolderFactoryTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/views/factories/SliderViewHolderFactoryTest.kt @@ -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. @@ -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 @@ -185,29 +184,6 @@ class SliderViewHolderFactoryTest { assertThat(viewHolder.itemView.findViewById(R.id.slider).valueFrom).isEqualTo(0.0F) } - @Test - fun `throws exception if minValue is greater than maxvalue`() { - assertFailsWith { - 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? = null