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 5d8e480214..c62db7a27c 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 @@ -50,6 +50,8 @@ import com.google.android.fhir.datacapture.extensions.validateLaunchContextExten import com.google.android.fhir.datacapture.extensions.zipByLinkId import com.google.android.fhir.datacapture.fhirpath.ExpressionEvaluator import com.google.android.fhir.datacapture.validation.Invalid +import com.google.android.fhir.datacapture.validation.MAX_VALUE_EXTENSION_URL +import com.google.android.fhir.datacapture.validation.MIN_VALUE_EXTENSION_URL import com.google.android.fhir.datacapture.validation.NotValidated import com.google.android.fhir.datacapture.validation.QuestionnaireResponseItemValidator import com.google.android.fhir.datacapture.validation.QuestionnaireResponseValidator @@ -68,7 +70,6 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.withIndex import org.hl7.fhir.r4.model.Base -import org.hl7.fhir.r4.model.Element import org.hl7.fhir.r4.model.Expression import org.hl7.fhir.r4.model.Questionnaire import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemComponent @@ -565,36 +566,21 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat } } - private fun resolveCqfExpression( - questionnaireItem: QuestionnaireItemComponent, - questionnaireResponseItem: QuestionnaireResponseItemComponent, - element: Element, - ): List { - val cqfExpression = element.cqfExpression ?: return emptyList() - - if (!cqfExpression.isFhirPath) { - throw UnsupportedOperationException("${cqfExpression.language} not supported yet") - } - return expressionEvaluator.evaluateExpression( - questionnaireItem, - questionnaireResponseItem, - cqfExpression, - ) - } - - private fun resolveCqfCalculatedValueExpression( + private fun resolveExpression( questionnaireItem: QuestionnaireItemComponent, questionnaireResponseItem: QuestionnaireResponseItemComponent, expression: Expression, - ): Type? { + ): Base? { if (!expression.isFhirPath) { throw UnsupportedOperationException("${expression.language} not supported yet") } - return expressionEvaluator - .evaluateExpression(questionnaireItem, questionnaireResponseItem, expression) + .evaluateExpression( + questionnaireItem, + questionnaireResponseItem, + expression, + ) .singleOrNull() - ?.let { it as Type } } private fun removeDisabledAnswers( @@ -687,6 +673,28 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat ) } + private fun resolveCqfCalculatedValueExpressions( + questionnaireItem: QuestionnaireItemComponent, + questionnaireResponseItem: QuestionnaireResponseItemComponent, + ) { + questionnaireItem.extension + .filter { it.hasValue() && it.value.hasExtension(EXTENSION_CQF_CALCULATED_VALUE_URL) } + .forEach { extension -> + val currentExtensionValue = extension.value + val currentExtensionValueExtensions = currentExtensionValue.extension + val evaluatedCqfCalculatedValue = + currentExtensionValue.getExtensionByUrl(EXTENSION_CQF_CALCULATED_VALUE_URL).value.let { + it as Expression + resolveExpression(questionnaireItem, questionnaireResponseItem, it) as? Type + } + // add previous extensions to the evaluated value + evaluatedCqfCalculatedValue?.setExtension(currentExtensionValueExtensions) + if (evaluatedCqfCalculatedValue != null) { + extension.setValue(evaluatedCqfCalculatedValue) + } + } + } + /** * Returns the list of [QuestionnaireViewItem]s generated for the questionnaire items and * questionnaire response items. @@ -726,21 +734,7 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat restoreFromDisabledQuestionnaireItemAnswersCache(questionnaireResponseItem) // Evaluate cqf-calculatedValues - questionnaireItem.extension - .filter { it.hasValue() && it.value.hasExtension(EXTENSION_CQF_CALCULATED_VALUE_URL) } - .forEach { extension -> - val expression = - extension.value.getExtensionByUrl(EXTENSION_CQF_CALCULATED_VALUE_URL).value as Expression - resolveCqfCalculatedValueExpression( - questionnaireItem, - questionnaireResponseItem, - expression, - ) - ?.let { - it.apply { setExtension(extension.value.extension) } - extension.setValue(it) - } - } + resolveCqfCalculatedValueExpressions(questionnaireItem, questionnaireResponseItem) // Determine the validation result, which will be displayed on the item itself val validationResult = @@ -759,11 +753,12 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat } // Set question text dynamically from CQL expression - questionnaireResponseItem.apply { - resolveCqfExpression(questionnaireItem, this, questionnaireItem.textElement) - .firstOrNull() - ?.let { text = it.primitiveValue() } + questionnaireItem.textElement.cqfExpression?.let { expression -> + resolveExpression(questionnaireItem, questionnaireResponseItem, expression) + ?.primitiveValue() + ?.let { questionnaireResponseItem.text = it } } + val (enabledQuestionnaireAnswerOptions, disabledQuestionnaireResponseAnswers) = answerOptionsEvaluator.evaluate( questionnaireItem, @@ -787,6 +782,8 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat validationResult = validationResult, answersChangedCallback = answersChangedCallback, enabledAnswerOptions = enabledQuestionnaireAnswerOptions, + minValue = questionnaireItem.getExtensionByUrl(MIN_VALUE_EXTENSION_URL)?.value, + maxValue = questionnaireItem.getExtensionByUrl(MAX_VALUE_EXTENSION_URL)?.value, draftAnswer = draftAnswerMap[questionnaireResponseItem], enabledDisplayItems = questionnaireItem.item.filter { diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireViewItem.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireViewItem.kt index 4d75941954..4aa4f822c5 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireViewItem.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireViewItem.kt @@ -23,13 +23,12 @@ import com.google.android.fhir.datacapture.R import com.google.android.fhir.datacapture.extensions.displayString import com.google.android.fhir.datacapture.extensions.localizedTextSpanned import com.google.android.fhir.datacapture.extensions.toSpanned -import com.google.android.fhir.datacapture.validation.MAX_VALUE_EXTENSION_URL -import com.google.android.fhir.datacapture.validation.MIN_VALUE_EXTENSION_URL import com.google.android.fhir.datacapture.validation.NotValidated import com.google.android.fhir.datacapture.validation.Valid import com.google.android.fhir.datacapture.validation.ValidationResult import org.hl7.fhir.r4.model.Questionnaire import org.hl7.fhir.r4.model.QuestionnaireResponse +import org.hl7.fhir.r4.model.Type /** * Data item for [QuestionnaireItemViewHolder] in [RecyclerView]. @@ -54,7 +53,9 @@ import org.hl7.fhir.r4.model.QuestionnaireResponse * @param validationResult the [ValidationResult] of the answer(s) against the `questionnaireItem` * @param answersChangedCallback the callback to notify the view model that the answers have been * changed for the [QuestionnaireResponse.QuestionnaireResponseItemComponent] - * @param enabledAnswerOptions the enabled answer options in [questionnaireItem]] + * @param enabledAnswerOptions the enabled answer options in [questionnaireItem] + * @param minValue the inclusive lower bound on the range of allowed answer values + * @param maxValue the inclusive upper bound on the range of allowed answer values * @param draftAnswer the draft input that cannot be stored in the [QuestionnaireResponse]. * @param enabledDisplayItems the enabled display items in the given [questionnaireItem] * @param questionViewTextConfiguration configuration to show asterisk, required and optional text @@ -73,6 +74,8 @@ data class QuestionnaireViewItem( ) -> Unit, val enabledAnswerOptions: List = questionnaireItem.answerOption.ifEmpty { emptyList() }, + val minValue: Type? = null, + val maxValue: Type? = null, val draftAnswer: Any? = null, val enabledDisplayItems: List = emptyList(), val questionViewTextConfiguration: QuestionTextConfiguration = QuestionTextConfiguration(), @@ -93,10 +96,6 @@ data class QuestionnaireViewItem( val answers: List = questionnaireResponseItem.answer.map { it.copy() } - val minValue by lazy { questionnaireItem.getExtensionByUrl(MIN_VALUE_EXTENSION_URL)?.value } - - val maxValue by lazy { questionnaireItem.getExtensionByUrl(MAX_VALUE_EXTENSION_URL)?.value } - /** Updates the answers. This will override any existing answers and removes the draft answer. */ fun setAnswer( vararg questionnaireResponseItemAnswerComponent: