Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Include min and max values in the validation message in a number question if the values are in the extension #2763

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
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 All @@ -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 @@ -1141,6 +1146,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