diff --git a/buildSrc/src/main/kotlin/Releases.kt b/buildSrc/src/main/kotlin/Releases.kt index a8667a76d0..b5948ecf1f 100644 --- a/buildSrc/src/main/kotlin/Releases.kt +++ b/buildSrc/src/main/kotlin/Releases.kt @@ -53,7 +53,7 @@ object Releases { object DataCapture : LibraryArtifact { override val artifactId = "data-capture" - override val version = "0.1.0-beta04" + override val version = "0.1.0-beta05" override val name = "Android FHIR Structured Data Capture Library" } diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponents.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponents.kt index 78b24660dd..c6f21fd0b6 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponents.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponents.kt @@ -103,7 +103,7 @@ internal fun Questionnaire.QuestionnaireItemComponent.isReferencedBy( item: Questionnaire.QuestionnaireItemComponent ) = item.expressionBasedExtensions.any { - it.castToExpression(it.value).expression.contains("'${this.linkId}'") + it.castToExpression(it.value).expression.replace(" ", "").contains(Regex(".*linkId='${this.linkId}'.*")) } // Item control code, or null @@ -330,7 +330,7 @@ fun QuestionnaireResponse.QuestionnaireResponseItemComponent.addNestedItemsToAns */ fun List.flattened(): List { - return this + this.flatMap { if (it.hasItem()) it.item.flattened() else it.item } + return this + this.flatMap { it.item.flattened() } } /** 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 023b023136..29a3ac0b62 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 @@ -387,16 +387,11 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat questionnaireResponseItemPreOrderList.find { it.linkId == updatedCalculable.first.linkId } val evaluatedAnswer = updatedCalculable.second - val currentAnswer = updatedCalculableResponse?.answer?.map { it.value } - - // update and notify only if answer has changed to prevent any event loop - // if current and previous both are not empty and the answer is changed in count or - // content - if ((evaluatedAnswer + currentAnswer).isNotEmpty() && - (evaluatedAnswer != currentAnswer || - evaluatedAnswer - .filterIndexed { i, v -> currentAnswer[i].equalsDeep(v).not() } - .isEmpty()) + val currentAnswer = updatedCalculableResponse?.answer?.map { it.value } ?: emptyList() + + // update and notify only if new answer has changed to prevent any event loop + if (evaluatedAnswer.size != currentAnswer.size || + evaluatedAnswer.zip(currentAnswer).any { (v1, v2) -> v1.equalsDeep(v2).not() } ) { updatedCalculableResponse?.let { it.answer = diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluator.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluator.kt index af7a8b6b02..cf9f511cfc 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluator.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluator.kt @@ -19,6 +19,7 @@ package com.google.android.fhir.datacapture.fhirpath import ca.uhn.fhir.context.FhirContext import ca.uhn.fhir.context.FhirVersionEnum import com.google.android.fhir.datacapture.calculatedExpression +import com.google.android.fhir.datacapture.expressionBasedExtensions import com.google.android.fhir.datacapture.findVariableExpression import com.google.android.fhir.datacapture.flattened import com.google.android.fhir.datacapture.isReferencedBy @@ -85,6 +86,17 @@ object ExpressionEvaluator { } } + /** Detects if any item into list is referencing a dependent item in its calculated expression */ + internal fun extractExpressionReferenceMap( + items: List + ) { + items.flattened().filter { it.expressionBasedExtensions.isNotEmpty() }.run { + forEach { current -> + + } + } + } + /** * Returns a pair of item and the calculated and evaluated value for all items with calculated * expression extension, which is dependent on value of updated response @@ -101,7 +113,10 @@ object ExpressionEvaluator { .item .flattened() .filter { item -> - // item is calculable and not modified yet and depends on the updated answer + // 1- item is calculable + // 2- item answer is not modified and touched by user; + // https://build.fhir.org/ig/HL7/sdc/StructureDefinition-sdc-questionnaire-calculatedExpression.html + // 3- item answer depends on the updated item answer item.calculatedExpression != null && modifiedResponses.none { it.linkId == item.linkId } && updatedQuestionnaireItem.isReferencedBy(item) diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/mapping/ResourceMapper.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/mapping/ResourceMapper.kt index 8a15638b38..aa641478c3 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/mapping/ResourceMapper.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/mapping/ResourceMapper.kt @@ -37,6 +37,7 @@ 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.DecimalType +import org.hl7.fhir.r4.model.DomainResource import org.hl7.fhir.r4.model.Enumeration import org.hl7.fhir.r4.model.Expression import org.hl7.fhir.r4.model.Extension @@ -444,9 +445,47 @@ object ResourceMapper { .javaClass .getMethod("setValue", Type::class.java) .invoke(base, questionnaireResponseItem.answer.singleOrNull()?.value) + return } catch (e: NoSuchMethodException) { // Do nothing } + + if (base.javaClass.getFieldOrNull(fieldName) == null) { + // If field not found in resource class, assume this is an extension + addDefinitionBasedCustomExtension(questionnaireItem, questionnaireResponseItem, base) + } + } +} + +/** + * Adds custom extension for Resource. + * @param questionnaireItem QuestionnaireItemComponent with details for extension + * @param questionnaireResponseItem QuestionnaireResponseItemComponent for response value + * @param base + * - resource's Base class instance See + * https://hapifhir.io/hapi-fhir/docs/model/profiles_and_extensions.html#extensions for more on + * custom extensions + */ +private fun addDefinitionBasedCustomExtension( + questionnaireItem: Questionnaire.QuestionnaireItemComponent, + questionnaireResponseItem: QuestionnaireResponse.QuestionnaireResponseItemComponent, + base: Base +) { + if (base is Type) { + // Create an extension + val ext = Extension() + ext.url = questionnaireItem.definition + ext.setValue(questionnaireResponseItem.answer.first().value) + // Add the extension to the resource + base.addExtension(ext) + } + if (base is DomainResource) { + // Create an extension + val ext = Extension() + ext.url = questionnaireItem.definition + ext.setValue(questionnaireResponseItem.answer.first().value) + // Add the extension to the resource + base.addExtension(ext) } } diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDatePickerViewHolderFactory.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDatePickerViewHolderFactory.kt index 24bcf79ce1..cf4d8f6daf 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDatePickerViewHolderFactory.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDatePickerViewHolderFactory.kt @@ -41,6 +41,7 @@ import java.time.ZoneId import java.time.chrono.IsoChronology import java.time.format.DateTimeFormatterBuilder import java.time.format.FormatStyle +import java.util.Date import java.util.Locale import kotlin.math.abs import kotlin.math.log10 @@ -103,16 +104,22 @@ internal object QuestionnaireItemDatePickerViewHolderFactory : textInputLayout.hint = localePattern textInputEditText.removeTextChangedListener(textWatcher) - textInputEditText.setText( - questionnaireItemViewItem - .answers - .singleOrNull() - ?.takeIf { it.hasValue() } - ?.valueDateType - ?.localDate - ?.localizedString - ) - + if (isTextUpdateRequired( + textInputEditText.context, + questionnaireItemViewItem.answers.singleOrNull()?.valueDateType, + textInputEditText.text.toString() + ) + ) { + textInputEditText.setText( + questionnaireItemViewItem + .answers + .singleOrNull() + ?.takeIf { it.hasValue() } + ?.valueDateType + ?.localDate + ?.localizedString + ) + } textWatcher = textInputEditText.doAfterTextChanged { text -> updateAnswer(text.toString()) } } @@ -159,6 +166,20 @@ internal object QuestionnaireItemDatePickerViewHolderFactory : } } } + + private fun isTextUpdateRequired( + context: Context, + answer: DateType?, + inputText: String? + ): Boolean { + val inputDate = + try { + parseDate(inputText, context) + } catch (e: Exception) { + null + } + return answer?.localDate != inputDate + } } internal const val TAG = "date-picker" @@ -196,14 +217,17 @@ internal val DateType.localDate internal val LocalDate.dateType get() = DateType(year, monthValue - 1, dayOfMonth) +internal val Date.localDate + get() = LocalDate.of(year + 1900, month + 1, date) + internal fun parseDate(text: CharSequence?, context: Context): LocalDate { - val date = + val localDate = if (isAndroidIcuSupported()) { - DateFormat.getDateInstance(DateFormat.SHORT).parse(text.toString()) - } else { - android.text.format.DateFormat.getDateFormat(context).parse(text.toString()) - } - val localDate = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate() + DateFormat.getDateInstance(DateFormat.SHORT).parse(text.toString()) + } else { + android.text.format.DateFormat.getDateFormat(context).parse(text.toString()) + } + .localDate // date/localDate with year more than 4 digit throws data format exception if deep copy // operation get performed on QuestionnaireResponse, // QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent in org.hl7.fhir.r4.model diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDateTimePickerViewHolderFactory.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDateTimePickerViewHolderFactory.kt index e8c6926799..75b1197589 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDateTimePickerViewHolderFactory.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDateTimePickerViewHolderFactory.kt @@ -146,7 +146,10 @@ internal object QuestionnaireItemDateTimePickerViewHolderFactory : val dateTime = questionnaireItemViewItem.answers.singleOrNull()?.valueDateTimeType updateDateTimeInput( dateTime?.let { - LocalDateTime.of(it.year, it.month + 1, it.day, it.hour, it.minute, it.second) + it.localDateTime.also { + localDate = it.toLocalDate() + localTime = it.toLocalTime() + } } ) textWatcher = @@ -197,7 +200,12 @@ internal object QuestionnaireItemDateTimePickerViewHolderFactory : /** Update the date and time input fields in the UI. */ private fun updateDateTimeInput(localDateTime: LocalDateTime?) { enableOrDisableTimePicker(enableIt = localDateTime != null) - if (dateInputEditText.text.isNullOrEmpty()) { + if (isTextUpdateRequired( + dateInputEditText.context, + localDateTime, + dateInputEditText.text.toString() + ) + ) { dateInputEditText.setText(localDateTime?.localizedDateString ?: "") } timeInputEditText.setText( @@ -281,6 +289,21 @@ internal object QuestionnaireItemDateTimePickerViewHolderFactory : timeInputLayout.isEnabled = enableIt timeInputLayout.isEnabled = enableIt } + + private fun isTextUpdateRequired( + context: Context, + answer: LocalDateTime?, + inputText: String? + ): Boolean { + val inputDate = + try { + generateLocalDateTime(parseDate(inputText, context), localTime) + } catch (e: Exception) { + null + } + if (answer == null || inputDate == null) return true + return answer.toLocalDate() != inputDate.toLocalDate() + } } } @@ -301,3 +324,14 @@ internal val DateTimeType.localTime minute, second, ) + +internal val DateTimeType.localDateTime + get() = + LocalDateTime.of( + year, + month + 1, + day, + hour, + minute, + second, + ) diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemRadioGroupViewHolderFactory.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemRadioGroupViewHolderFactory.kt index 817845d8e4..07d8ea3355 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemRadioGroupViewHolderFactory.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemRadioGroupViewHolderFactory.kt @@ -141,16 +141,11 @@ internal object QuestionnaireItemRadioGroupViewHolderFactory : } private fun updateAnswer(answerOption: Questionnaire.QuestionnaireItemAnswerOptionComponent) { - // if-else block to prevent over-writing of "items" nested within "answer" - if (questionnaireItemViewItem.answers.isNotEmpty()) { - questionnaireItemViewItem.answers.apply { this[0].value = answerOption.value } - } else { - questionnaireItemViewItem.setAnswer( - QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { - value = answerOption.value - } - ) - } + questionnaireItemViewItem.setAnswer( + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { + value = answerOption.value + } + ) } } } diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/mapping/ResourceMapperTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/mapping/ResourceMapperTest.kt index cd459efab2..f126fcf7d3 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/mapping/ResourceMapperTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/mapping/ResourceMapperTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Google LLC + * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,6 +37,7 @@ import org.hl7.fhir.r4.model.BooleanType import org.hl7.fhir.r4.model.Coding import org.hl7.fhir.r4.model.ContactPoint import org.hl7.fhir.r4.model.DateType +import org.hl7.fhir.r4.model.Encounter import org.hl7.fhir.r4.model.Enumerations import org.hl7.fhir.r4.model.Expression import org.hl7.fhir.r4.model.Extension @@ -875,81 +876,82 @@ class ResourceMapperTest { runBlocking { @Language("JSON") val questionnaireJson = - """{ - "resourceType": "Questionnaire", - "subjectType": [ - "Encounter" - ], - "item": [ - { - "text": "Pulse Oximetry", - "linkId": "6.0.0", - "type": "group", - "extension": [ - { - "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemExtractionContext", - "valueExpression": { - "language": "application/x-fhir-query", - "expression": "Observation", - "name": "pulse" - } - } - ], - "item": [ + """ { - "linkId": "6.2.0", - "type": "group", - "definition": "http://hl7.org/fhir/StructureDefinition/Observation#Observation.valueQuantity", + "resourceType": "Questionnaire", + "subjectType": [ + "Encounter" + ], "item": [ { - "text": "Pulse oximetry reading", - "type": "integer", - "linkId": "6.2.1", - "definition": "http://hl7.org/fhir/StructureDefinition/Observation#Observation.valueQuantity.value", + "text": "Pulse Oximetry", + "linkId": "6.0.0", + "type": "group", "extension": [ { - "url": "http://hl7.org/fhir/StructureDefinition/minValue", - "valueInteger": 60 - }, + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemExtractionContext", + "valueExpression": { + "language": "application/x-fhir-query", + "expression": "Observation", + "name": "pulse" + } + } + ], + "item": [ { - "url": "http://hl7.org/fhir/StructureDefinition/maxValue", - "valueInteger": 100 + "linkId": "6.2.0", + "type": "group", + "definition": "http://hl7.org/fhir/StructureDefinition/Observation#Observation.valueQuantity", + "item": [ + { + "text": "Pulse oximetry reading", + "type": "integer", + "linkId": "6.2.1", + "definition": "http://hl7.org/fhir/StructureDefinition/Observation#Observation.valueQuantity.value", + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/minValue", + "valueInteger": 60 + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/maxValue", + "valueInteger": 100 + } + ] + } + ] } ] } ] } - ] - } - ] -} """.trimIndent() @Language("JSON") val questionnaireResponseJson = """ { - "resourceType": "QuestionnaireResponse", - "item": [ - { - "linkId": "6.0.0", - "item": [ - { - "linkId": "6.2.0", + "resourceType": "QuestionnaireResponse", "item": [ { - "linkId": "6.2.1", - "answer": [ + "linkId": "6.0.0", + "item": [ { - "valueInteger": 90 + "linkId": "6.2.0", + "item": [ + { + "linkId": "6.2.1", + "answer": [ + { + "valueInteger": 90 + } + ] + } + ] } ] } ] } - ] - } - ] -} """.trimIndent() val iParser: IParser = FhirContext.forR4().newJsonParser() @@ -977,189 +979,189 @@ class ResourceMapperTest { val questionnaireJson = """ { - "resourceType": "Questionnaire", - "id": "client-registration-sample", - "status": "active", - "date": "2020-11-18T07:24:47.111Z", - "subjectType": [ - "Patient" - ], - "item": [ - { - "linkId": "PR", - "type": "group", - "item": [ - { - "linkId": "PR-name", - "type": "group", - "definition": "http://hl7.org/fhir/StructureDefinition/Patient#Patient.name", - "item": [ - { - "extension": [ - { - "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression", - "valueExpression": { - "language": "text/fhirpath", - "expression": "Patient.name.given", - "name": "patientName" - } - } - ], - "linkId": "PR-name-text", - "definition": "http://hl7.org/fhir/StructureDefinition/Patient#Patient.name.given", - "type": "string", - "text": "First Name" - }, - { - "extension": [ - { - "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression", - "valueExpression": { - "language": "text/fhirpath", - "expression": "Patient.name.family", - "name": "patientFamily" - } - } - ], - "linkId": "PR-name-family", - "definition": "http://hl7.org/fhir/StructureDefinition/datatypes#HumanName.family", - "type": "string", - "text": "Family Name" - } - ] - }, - { - "extension": [ - { - "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression", - "valueExpression": { - "language": "text/fhirpath", - "expression": "Patient.birthDate", - "name": "patientBirthDate" - } - } - ], - "linkId": "patient-0-birth-date", - "definition": "http://hl7.org/fhir/StructureDefinition/Patient#Patient.birthDate", - "type": "date", - "text": "Date of Birth" - }, - { - "linkId": "patient-0-gender", - "extension": [ - { - "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression", - "valueExpression": { - "language": "text/fhirpath", - "expression": "Patient.gender.value", - "name": "patientGender" - } - } + "resourceType": "Questionnaire", + "id": "client-registration-sample", + "status": "active", + "date": "2020-11-18T07:24:47.111Z", + "subjectType": [ + "Patient" ], - "definition": "http://hl7.org/fhir/StructureDefinition/Patient#Patient.gender", - "type": "string", - "text": "Gender" - }, - { - "linkId": "PR-telecom", - "type": "group", - "definition": "http://hl7.org/fhir/StructureDefinition/Patient#Patient.telecom", "item": [ { - "linkId": "PR-telecom-system", - "definition": "http://hl7.org/fhir/StructureDefinition/Patient#Patient.telecom.system", - "type": "string", - "text": "system", - "initial": [ + "linkId": "PR", + "type": "group", + "item": [ { - "valueString": "phone" - } - ], - "enableWhen": [ + "linkId": "PR-name", + "type": "group", + "definition": "http://hl7.org/fhir/StructureDefinition/Patient#Patient.name", + "item": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression", + "valueExpression": { + "language": "text/fhirpath", + "expression": "Patient.name.given", + "name": "patientName" + } + } + ], + "linkId": "PR-name-text", + "definition": "http://hl7.org/fhir/StructureDefinition/Patient#Patient.name.given", + "type": "string", + "text": "First Name" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression", + "valueExpression": { + "language": "text/fhirpath", + "expression": "Patient.name.family", + "name": "patientFamily" + } + } + ], + "linkId": "PR-name-family", + "definition": "http://hl7.org/fhir/StructureDefinition/datatypes#HumanName.family", + "type": "string", + "text": "Family Name" + } + ] + }, { - "question": "patient-0-gender", - "operator": "=", - "answerString": "ok" - } - ] - }, - { - "extension": [ + "extension": [ + { + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression", + "valueExpression": { + "language": "text/fhirpath", + "expression": "Patient.birthDate", + "name": "patientBirthDate" + } + } + ], + "linkId": "patient-0-birth-date", + "definition": "http://hl7.org/fhir/StructureDefinition/Patient#Patient.birthDate", + "type": "date", + "text": "Date of Birth" + }, { - "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression", - "valueExpression": { - "language": "text/fhirpath", - "expression": "Patient.telecom.value", - "name": "patientTelecom" - } - } - ], - "linkId": "PR-telecom-value", - "definition": "http://hl7.org/fhir/StructureDefinition/Patient#Patient.telecom.value", - "type": "string", - "text": "Phone Number" - } - ] - }, - { - "linkId": "PR-address", - "type": "group", - "definition": "http://hl7.org/fhir/StructureDefinition/Patient#Patient.address", - "item": [ - { - "extension": [ + "linkId": "patient-0-gender", + "extension": [ + { + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression", + "valueExpression": { + "language": "text/fhirpath", + "expression": "Patient.gender.value", + "name": "patientGender" + } + } + ], + "definition": "http://hl7.org/fhir/StructureDefinition/Patient#Patient.gender", + "type": "string", + "text": "Gender" + }, { - "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression", - "valueExpression": { - "language": "text/fhirpath", - "expression": "Patient.address.city", - "name": "patientCity" - } - } - ], - "linkId": "PR-address-city", - "definition": "http://hl7.org/fhir/StructureDefinition/Patient#Patient.address.city", - "type": "string", - "text": "City" - }, - { - "extension": [ + "linkId": "PR-telecom", + "type": "group", + "definition": "http://hl7.org/fhir/StructureDefinition/Patient#Patient.telecom", + "item": [ + { + "linkId": "PR-telecom-system", + "definition": "http://hl7.org/fhir/StructureDefinition/Patient#Patient.telecom.system", + "type": "string", + "text": "system", + "initial": [ + { + "valueString": "phone" + } + ], + "enableWhen": [ + { + "question": "patient-0-gender", + "operator": "=", + "answerString": "ok" + } + ] + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression", + "valueExpression": { + "language": "text/fhirpath", + "expression": "Patient.telecom.value", + "name": "patientTelecom" + } + } + ], + "linkId": "PR-telecom-value", + "definition": "http://hl7.org/fhir/StructureDefinition/Patient#Patient.telecom.value", + "type": "string", + "text": "Phone Number" + } + ] + }, { - "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression", - "valueExpression": { - "language": "text/fhirpath", - "expression": "Patient.address.country", - "name": "patientCity" - } + "linkId": "PR-address", + "type": "group", + "definition": "http://hl7.org/fhir/StructureDefinition/Patient#Patient.address", + "item": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression", + "valueExpression": { + "language": "text/fhirpath", + "expression": "Patient.address.city", + "name": "patientCity" + } + } + ], + "linkId": "PR-address-city", + "definition": "http://hl7.org/fhir/StructureDefinition/Patient#Patient.address.city", + "type": "string", + "text": "City" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression", + "valueExpression": { + "language": "text/fhirpath", + "expression": "Patient.address.country", + "name": "patientCity" + } + } + ], + "linkId": "PR-address-country", + "definition": "http://hl7.org/fhir/StructureDefinition/Patient#Patient.address.country", + "type": "string", + "text": "Country" + } + ] + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression", + "valueExpression": { + "language": "text/fhirpath", + "expression": "Patient.active", + "name": "patientActive" + } + } + ], + "linkId": "PR-active", + "definition": "http://hl7.org/fhir/StructureDefinition/Patient#Patient.active", + "type": "boolean", + "text": "Is Active?" } - ], - "linkId": "PR-address-country", - "definition": "http://hl7.org/fhir/StructureDefinition/Patient#Patient.address.country", - "type": "string", - "text": "Country" + ] } ] - }, - { - "extension": [ - { - "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression", - "valueExpression": { - "language": "text/fhirpath", - "expression": "Patient.active", - "name": "patientActive" - } - } - ], - "linkId": "PR-active", - "definition": "http://hl7.org/fhir/StructureDefinition/Patient#Patient.active", - "type": "boolean", - "text": "Is Active?" } - ] - } - ] -} """.trimIndent() val iParser: IParser = FhirContext.forR4().newJsonParser() @@ -1873,168 +1875,169 @@ class ResourceMapperTest { fun extract_choiceType_updateObservationFields() = runBlocking { @Language("JSON") val questionnaire = - """{ - "title": "Screener", - "status": "active", - "version": "0.0.1", - "publisher": "Fred Hersch (fredhersch@google.com)", - "resourceType": "Questionnaire", - "subjectType": [ - "Encounter" - ], - "extension": [ - { - "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemExtractionContext", - "valueExpression": { - "language": "application/x-fhir-query", - "expression": "Encounter", - "name": "encounter" - } - } - ], - "item": [ - { - "text": "Temperature", - "type": "group", - "linkId": "5.0.0", - "code": [ - { - "code": "8310-5", - "display": "Temperature", - "system": "http://loinc.org" - } - ], - "extension": [ - { - "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemExtractionContext", - "valueExpression": { - "language": "application/x-fhir-query", - "expression": "Observation", - "name": "temperature" - } - }, - { - "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", - "valueCodeableConcept": { - "coding": [ - { - "system": "http://hl7.org/fhir/questionnaire-item-control", - "code": "page", - "display": "Page" - } - ], - "text": "Page" - } - } - ], - "item": [ + """ { - "text": "Add instructions for capturing temperature", - "type": "display", - "linkId": "5.0.1" - }, - { - "type": "group", - "definition": "http://hl7.org/fhir/StructureDefinition/Observation#Observation.valueQuantity", + "title": "Screener", + "status": "active", + "version": "0.0.1", + "publisher": "Fred Hersch (fredhersch@google.com)", + "resourceType": "Questionnaire", + "subjectType": [ + "Encounter" + ], + "extension": [ + { + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemExtractionContext", + "valueExpression": { + "language": "application/x-fhir-query", + "expression": "Encounter", + "name": "encounter" + } + } + ], "item": [ { - "definition": "http://hl7.org/fhir/StructureDefinition/Observation#Observation.valueQuantity.value", - "text": "Record temperature", - "type": "decimal", - "linkId": "5.1.0", - "extension": [ - { - "url": "http://hl7.org/fhir/StructureDefinition/minValue", - "valueDecimal": 35.0 - }, + "text": "Temperature", + "type": "group", + "linkId": "5.0.0", + "code": [ { - "url": "http://hl7.org/fhir/StructureDefinition/maxValue", - "valueDecimal": 45.0 + "code": "8310-5", + "display": "Temperature", + "system": "http://loinc.org" } - ] - }, - { - "text": "Unit", - "type": "choice", - "linkId": "5.2.0", - "required": true, - "definition": "http://hl7.org/fhir/StructureDefinition/Observation#Observation.valueQuantity.code", + ], "extension": [ + { + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemExtractionContext", + "valueExpression": { + "language": "application/x-fhir-query", + "expression": "Observation", + "name": "temperature" + } + }, { "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", "valueCodeableConcept": { "coding": [ { "system": "http://hl7.org/fhir/questionnaire-item-control", - "code": "drop-down", - "display": "Drop down" + "code": "page", + "display": "Page" } ], - "text": "Drop down" + "text": "Page" } } ], - "answerOption": [ + "item": [ { - "valueCoding": { - "code": "F", - "display": "F" - } + "text": "Add instructions for capturing temperature", + "type": "display", + "linkId": "5.0.1" }, { - "valueCoding": { - "code": "C", - "display": "C" - } + "type": "group", + "definition": "http://hl7.org/fhir/StructureDefinition/Observation#Observation.valueQuantity", + "item": [ + { + "definition": "http://hl7.org/fhir/StructureDefinition/Observation#Observation.valueQuantity.value", + "text": "Record temperature", + "type": "decimal", + "linkId": "5.1.0", + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/minValue", + "valueDecimal": 35.0 + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/maxValue", + "valueDecimal": 45.0 + } + ] + }, + { + "text": "Unit", + "type": "choice", + "linkId": "5.2.0", + "required": true, + "definition": "http://hl7.org/fhir/StructureDefinition/Observation#Observation.valueQuantity.code", + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "drop-down", + "display": "Drop down" + } + ], + "text": "Drop down" + } + } + ], + "answerOption": [ + { + "valueCoding": { + "code": "F", + "display": "F" + } + }, + { + "valueCoding": { + "code": "C", + "display": "C" + } + } + ] + } + ] } ] } ] } - ] - } - ] -} """.trimIndent() @Language("JSON") val response = """ { - "resourceType": "QuestionnaireResponse", - "item": [ - { - "linkId": "5.0.0", - "item": [ - { - "linkId": "5.0.1" - }, - { + "resourceType": "QuestionnaireResponse", "item": [ { - "linkId": "5.1.0", - "answer": [ + "linkId": "5.0.0", + "item": [ { - "valueDecimal": 36 - } - ] - }, - { - "linkId": "5.2.0", - "answer": [ + "linkId": "5.0.1" + }, { - "valueCoding": { - "code": "F", - "display": "F" - } + "item": [ + { + "linkId": "5.1.0", + "answer": [ + { + "valueDecimal": 36 + } + ] + }, + { + "linkId": "5.2.0", + "answer": [ + { + "valueCoding": { + "code": "F", + "display": "F" + } + } + ] + } + ] } ] } ] } - ] - } - ] -} """.trimIndent() val iParser: IParser = FhirContext.forR4().newJsonParser() val temperatureQuestionnaire = @@ -2376,6 +2379,246 @@ class ResourceMapperTest { ) } + @Test + fun `extract() should perform definition based extraction for custom extensions with valueAsPrimitive`(): + Unit = runBlocking { + @Language("JSON") + val questionnaire = + """ + { + "resourceType": "Questionnaire", + "subjectType": [ + "Encounter" + ], + "extension": [ + { + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemExtractionContext", + "valueExpression": { + "language": "application/x-fhir-query", + "expression": "Encounter", + "name": "encounter" + } + } + ], + "item": [ + { + "linkId": "1", + "definition": "http://fhir.org/guides/who/anc-cds/StructureDefinition/anc-encounter#Encounter.contactNumber", + "text": "ANC contact number", + "type": "integer" + } + ] + } + """.trimIndent() + + @Language("JSON") + val response = + """ + { + "resourceType": "QuestionnaireResponse", + "item": [ + { + "linkId": "1", + "answer": [ + { + "valueInteger": 9 + } + ] + } + ] + } + """.trimIndent() + val iParser: IParser = FhirContext.forR4().newJsonParser() + val questionnaireObj = + iParser.parseResource(Questionnaire::class.java, questionnaire) as Questionnaire + val temperatureQuestionnaireResponse = + iParser.parseResource(QuestionnaireResponse::class.java, response) as QuestionnaireResponse + val bundle = ResourceMapper.extract(questionnaireObj, temperatureQuestionnaireResponse) + val encounter = bundle.entry.single().resource as Encounter + + assertThat(encounter).isNotNull() + assertThat( + encounter.getExtensionByUrl( + "http://fhir.org/guides/who/anc-cds/StructureDefinition/anc-encounter#Encounter.contactNumber" + ) + .valueAsPrimitive + .value + ) + .isEqualTo(9) + } + + @Test + fun `extract() should perform definition based extraction for custom extensions with valueCodeableConcept`(): + Unit = runBlocking { + @Language("JSON") + val questionnaire = + """ + { + "resourceType": "Questionnaire", + "extension": [ + { + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemExtractionContext", + "valueExpression": { + "name": "patient", + "language": "application/x-fhir-query", + "expression": "Patient" + } + } + ], + "item": [ + { + "type": "group", + "linkId": "patient-other-details", + "item": [ + { + "type": "choice", + "linkId": "tribe", + "text": "Tribe", + "definition": "http://hl7.org/fhir/StructureDefinition/Patient#Patient.extension:tribe" + + } + ] + } + ] + } + """.trimIndent() + + @Language("JSON") + val response = + """ + { + "resourceType": "QuestionnaireResponse", + "item": [ + { + "linkId": "patient-other-details", + "item": [ + { + "linkId": "tribe", + "answer": [ + { + "valueCoding": { + "code": "hausa", + "display": "Hausa" + } + } + ] + } + ] + } + ] + } + """.trimIndent() + val iParser: IParser = FhirContext.forR4().newJsonParser() + val questionnaireObj = + iParser.parseResource(Questionnaire::class.java, questionnaire) as Questionnaire + val temperatureQuestionnaireResponse = + iParser.parseResource(QuestionnaireResponse::class.java, response) as QuestionnaireResponse + val bundle = ResourceMapper.extract(questionnaireObj, temperatureQuestionnaireResponse) + val patient = bundle.entry.single().resource as Patient + + assertThat(patient).isNotNull() + val coding = + patient.getExtensionByUrl( + "http://hl7.org/fhir/StructureDefinition/Patient#Patient.extension:tribe" + ) + .value as + Coding + assertThat(coding.code).isEqualTo("hausa") + assertThat(coding.display).isEqualTo("Hausa") + } + + @Test + fun `extract() should perform definition based extraction for Complex DataType with custom extensions`(): + Unit = runBlocking { + @Language("JSON") + val questionnaire = + """ + { + "resourceType": "Questionnaire", + "extension": [ + { + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemExtractionContext", + "valueExpression": { + "name": "patient", + "language": "application/x-fhir-query", + "expression": "Patient" + } + } + ], + "item": [ + { + "type": "group", + "linkId": "patient-basic-details", + "text": "Patient details", + "item": [ + { + "linkId": "patient-name", + "type": "group", + "definition": "http://build.fhir.org/ig/WorldHealthOrganization/smart-anc/StructureDefinition-anc-patient-definitions.html#Patient.name", + "item": [ + { + "type": "string", + "definition": "http://build.fhir.org/ig/WorldHealthOrganization/smart-anc/StructureDefinition-anc-patient-definitions.html#Patient.name.middle", + "linkId": "middle-name", + "text": "Middle name" + } + ] + } + ] + } + ] + } + """.trimIndent() + + @Language("JSON") + val response = + """ + { + "resourceType": "QuestionnaireResponse", + "item": [ + { + "linkId": "patient-basic-details", + "item": [ + { + "linkId": "patient-name", + "item": [ + { + "linkId": "middle-name", + "answer": [ + { + "valueString": "TestName" + } + ] + } + ] + } + ] + } + ] + } + """.trimIndent() + val iParser: IParser = FhirContext.forR4().newJsonParser() + val questionnaireObj = + iParser.parseResource(Questionnaire::class.java, questionnaire) as Questionnaire + val temperatureQuestionnaireResponse = + iParser.parseResource(QuestionnaireResponse::class.java, response) as QuestionnaireResponse + val bundle = ResourceMapper.extract(questionnaireObj, temperatureQuestionnaireResponse) + val patient = bundle.entry.single().resource as Patient + + assertThat(patient).isNotNull() + assertThat( + patient + .name + .first() + .getExtensionByUrl( + "http://build.fhir.org/ig/WorldHealthOrganization/smart-anc/StructureDefinition-anc-patient-definitions.html#Patient.name.middle" + ) + .valueAsPrimitive + .valueAsString + ) + .isEqualTo("TestName") + } + private fun String.toDateFromFormatYyyyMmDd(): Date? = SimpleDateFormat("yyyy-MM-dd").parse(this) class TransformSupportServices(private val outputs: MutableList) : diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDatePickerViewHolderFactoryTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDatePickerViewHolderFactoryTest.kt index e92a129105..4b6d80dfdd 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDatePickerViewHolderFactoryTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDatePickerViewHolderFactoryTest.kt @@ -65,10 +65,7 @@ class QuestionnaireItemDatePickerViewHolderFactoryTest { ) ) - assertThat( - viewHolder.itemView.findViewById(R.id.text_input_edit_text).text.toString() - ) - .isEqualTo("") + assertThat(viewHolder.dateInputView.text.toString()).isEqualTo("") } @Test @@ -106,10 +103,7 @@ class QuestionnaireItemDatePickerViewHolderFactoryTest { answersChangedCallback = { _, _, _ -> }, ) ) - assertThat( - viewHolder.itemView.findViewById(R.id.text_input_edit_text).text.toString() - ) - .isEqualTo("11/19/20") + assertThat(viewHolder.dateInputView.text.toString()).isEqualTo("11/19/20") } @Test @@ -127,10 +121,7 @@ class QuestionnaireItemDatePickerViewHolderFactoryTest { answersChangedCallback = { _, _, _ -> }, ) ) - assertThat( - viewHolder.itemView.findViewById(R.id.text_input_edit_text).text.toString() - ) - .isEqualTo("2020/11/19") + assertThat(viewHolder.dateInputView.text.toString()).isEqualTo("2020/11/19") } @Test @@ -148,10 +139,7 @@ class QuestionnaireItemDatePickerViewHolderFactoryTest { answersChangedCallback = { _, _, _ -> }, ) ) - assertThat( - viewHolder.itemView.findViewById(R.id.text_input_edit_text).text.toString() - ) - .isEqualTo("11/19/20") + assertThat(viewHolder.dateInputView.text.toString()).isEqualTo("11/19/20") } @Test @@ -166,7 +154,7 @@ class QuestionnaireItemDatePickerViewHolderFactoryTest { ) viewHolder.bind(item) - viewHolder.itemView.findViewById(R.id.text_input_edit_text).text = "11/19/2020" + viewHolder.dateInputView.text = "11/19/2020" val answer = item.answers.singleOrNull()?.value as DateType @@ -186,7 +174,7 @@ class QuestionnaireItemDatePickerViewHolderFactoryTest { answersChangedCallback = { _, _, _ -> }, ) viewHolder.bind(item) - viewHolder.itemView.findViewById(R.id.text_input_edit_text).text = "2020/11/19" + viewHolder.dateInputView.text = "2020/11/19" val answer = item.answers.singleOrNull()?.value as DateType assertThat(answer.day).isEqualTo(19) @@ -210,7 +198,7 @@ class QuestionnaireItemDatePickerViewHolderFactoryTest { ) viewHolder.bind(questionnaireItem) - viewHolder.itemView.findViewById(R.id.text_input_edit_text).text = "11/19/" + viewHolder.dateInputView.text = "11/19/" val answer = questionnaireItem.answers.singleOrNull()?.value assertThat(answer).isNull() } @@ -285,12 +273,59 @@ class QuestionnaireItemDatePickerViewHolderFactoryTest { ) ) - assertThat(viewHolder.itemView.findViewById(R.id.text_input_edit_text).isEnabled) - .isFalse() + assertThat(viewHolder.dateInputView.isEnabled).isFalse() + } + + @Test + fun `bind multiple times with different QuestionnaireItemViewItem should show proper date`() { + setLocale(Locale.US) + + viewHolder.bind( + QuestionnaireItemViewItem( + Questionnaire.QuestionnaireItemComponent().apply { text = "Question?" }, + QuestionnaireResponse.QuestionnaireResponseItemComponent() + .addAnswer( + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent() + .setValue(DateType(2020, 10, 19)) + ), + validationResult = NotValidated, + answersChangedCallback = { _, _, _ -> }, + ) + ) + assertThat(viewHolder.dateInputView.text.toString()).isEqualTo("11/19/20") + + viewHolder.bind( + QuestionnaireItemViewItem( + Questionnaire.QuestionnaireItemComponent().apply { text = "Question?" }, + QuestionnaireResponse.QuestionnaireResponseItemComponent() + .addAnswer( + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent() + .setValue(DateType(2021, 10, 19)) + ), + validationResult = NotValidated, + answersChangedCallback = { _, _, _ -> }, + ) + ) + assertThat(viewHolder.dateInputView.text.toString()).isEqualTo("11/19/21") + + viewHolder.bind( + QuestionnaireItemViewItem( + Questionnaire.QuestionnaireItemComponent().apply { text = "Question?" }, + QuestionnaireResponse.QuestionnaireResponseItemComponent(), + validationResult = NotValidated, + answersChangedCallback = { _, _, _ -> }, + ) + ) + assertThat(viewHolder.dateInputView.text.toString()).isEmpty() } private fun setLocale(locale: Locale) { Locale.setDefault(locale) context.resources.configuration.setLocale(locale) } + + private val QuestionnaireItemViewHolder.dateInputView: TextView + get() { + return itemView.findViewById(R.id.text_input_edit_text) + } } diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDateTimePickerViewHolderFactoryTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDateTimePickerViewHolderFactoryTest.kt index 4bbedbc944..633665825f 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDateTimePickerViewHolderFactoryTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDateTimePickerViewHolderFactoryTest.kt @@ -16,7 +16,6 @@ package com.google.android.fhir.datacapture.views -import android.widget.EditText import android.widget.FrameLayout import android.widget.TextView import com.google.android.fhir.datacapture.R @@ -75,14 +74,8 @@ class QuestionnaireItemDateTimePickerViewHolderFactoryTest { ) ) - assertThat( - viewHolder.itemView.findViewById(R.id.date_input_edit_text).text.toString() - ) - .isEqualTo("") - assertThat( - viewHolder.itemView.findViewById(R.id.time_input_edit_text).text.toString() - ) - .isEqualTo("") + assertThat(viewHolder.dateInputView.text.toString()).isEqualTo("") + assertThat(viewHolder.timeInputView.text.toString()).isEqualTo("") } @Test @@ -100,14 +93,8 @@ class QuestionnaireItemDateTimePickerViewHolderFactoryTest { ) ) - assertThat( - viewHolder.itemView.findViewById(R.id.date_input_edit_text).text.toString() - ) - .isEqualTo("2/5/20") - assertThat( - viewHolder.itemView.findViewById(R.id.time_input_edit_text).text.toString() - ) - .isEqualTo("1:30 AM") + assertThat(viewHolder.dateInputView.text.toString()).isEqualTo("2/5/20") + assertThat(viewHolder.timeInputView.text.toString()).isEqualTo("1:30 AM") } @Test @@ -125,7 +112,7 @@ class QuestionnaireItemDateTimePickerViewHolderFactoryTest { ) viewHolder.bind(itemViewItem) - viewHolder.itemView.findViewById(R.id.date_input_edit_text).text = "11/19/2020" + viewHolder.dateInputView.text = "11/19/2020" val answer = itemViewItem.answers.singleOrNull()?.value as DateTimeType assertThat(answer.day).isEqualTo(19) @@ -149,7 +136,7 @@ class QuestionnaireItemDateTimePickerViewHolderFactoryTest { ) viewHolder.bind(itemViewItem) - viewHolder.itemView.findViewById(R.id.date_input_edit_text).text = "2020/11/19" + viewHolder.dateInputView.text = "2020/11/19" val answer = itemViewItem.answers.singleOrNull()?.value as DateTimeType assertThat(answer.day).isEqualTo(19) @@ -171,7 +158,7 @@ class QuestionnaireItemDateTimePickerViewHolderFactoryTest { answersChangedCallback = { _, _, _ -> }, ) viewHolder.bind(itemViewItem) - viewHolder.itemView.findViewById(R.id.date_input_edit_text).text = "2020/11/" + viewHolder.dateInputView.text = "2020/11/" assertThat(itemViewItem.answers.singleOrNull()).isNull() } @@ -187,7 +174,7 @@ class QuestionnaireItemDateTimePickerViewHolderFactoryTest { ) viewHolder.bind(itemViewItem) - viewHolder.itemView.findViewById(R.id.date_input_edit_text).text = "11/19/" + viewHolder.dateInputView.text = "11/19/" assertThat(viewHolder.itemView.findViewById(R.id.time_input_layout).isEnabled) .isFalse() @@ -204,7 +191,7 @@ class QuestionnaireItemDateTimePickerViewHolderFactoryTest { ) viewHolder.bind(itemViewItem) - viewHolder.itemView.findViewById(R.id.date_input_edit_text).text = "11/19/2020" + viewHolder.dateInputView.text = "11/19/2020" assertThat(viewHolder.itemView.findViewById(R.id.time_input_layout).isEnabled) .isTrue() @@ -267,9 +254,65 @@ class QuestionnaireItemDateTimePickerViewHolderFactoryTest { ) ) - assertThat(viewHolder.itemView.findViewById(R.id.date_input_edit_text).isEnabled) - .isFalse() - assertThat(viewHolder.itemView.findViewById(R.id.time_input_edit_text).isEnabled) - .isFalse() + assertThat(viewHolder.dateInputView.isEnabled).isFalse() + assertThat(viewHolder.timeInputView.isEnabled).isFalse() + } + + @Test + fun `bind multiple times with separate QuestionnaireItemViewItem should show proper date and time`() { + + viewHolder.bind( + QuestionnaireItemViewItem( + Questionnaire.QuestionnaireItemComponent().apply { text = "Question?" }, + QuestionnaireResponse.QuestionnaireResponseItemComponent() + .addAnswer( + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent() + .setValue(DateTimeType(Date(2020 - 1900, 1, 5, 1, 30, 0))) + ), + validationResult = NotValidated, + answersChangedCallback = { _, _, _ -> }, + ) + ) + + assertThat(viewHolder.dateInputView.text.toString()).isEqualTo("2/5/20") + assertThat(viewHolder.timeInputView.text.toString()).isEqualTo("1:30 AM") + + viewHolder.bind( + QuestionnaireItemViewItem( + Questionnaire.QuestionnaireItemComponent().apply { text = "Question?" }, + QuestionnaireResponse.QuestionnaireResponseItemComponent() + .addAnswer( + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent() + .setValue(DateTimeType(Date(2021 - 1900, 1, 5, 2, 30, 0))) + ), + validationResult = NotValidated, + answersChangedCallback = { _, _, _ -> }, + ) + ) + + assertThat(viewHolder.dateInputView.text.toString()).isEqualTo("2/5/21") + assertThat(viewHolder.timeInputView.text.toString()).isEqualTo("2:30 AM") + + viewHolder.bind( + QuestionnaireItemViewItem( + Questionnaire.QuestionnaireItemComponent().apply { text = "Question?" }, + QuestionnaireResponse.QuestionnaireResponseItemComponent(), + validationResult = NotValidated, + answersChangedCallback = { _, _, _ -> }, + ) + ) + + assertThat(viewHolder.dateInputView.text.toString()).isEmpty() + assertThat(viewHolder.timeInputView.text.toString()).isEmpty() } + + private val QuestionnaireItemViewHolder.dateInputView: TextView + get() { + return itemView.findViewById(R.id.date_input_edit_text) + } + + private val QuestionnaireItemViewHolder.timeInputView: TextView + get() { + return itemView.findViewById(R.id.time_input_edit_text) + } } diff --git a/demo/src/main/assets/screener-questionnaire.json b/demo/src/main/assets/screener-questionnaire.json index edafc3eafa..1d5e6cc14c 100644 --- a/demo/src/main/assets/screener-questionnaire.json +++ b/demo/src/main/assets/screener-questionnaire.json @@ -15,7 +15,11 @@ "expression": "Encounter", "name": "encounter" } - } + }, + { + "url" : "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-entryMode", + "valueCode" : "prior-edit" + } ], "item": [ { @@ -292,7 +296,6 @@ { "linkId": "2.1.0", "type": "group", - "required": true, "extension": [ { "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemExtractionContext", @@ -381,7 +384,6 @@ { "linkId": "2.2.0", "type": "group", - "required": true, "extension": [ { "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemExtractionContext", @@ -461,7 +463,6 @@ { "linkId": "2.3.0", "type": "group", - "required": true, "extension": [ { "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemExtractionContext", diff --git a/demo/src/main/java/com/google/android/fhir/demo/EditPatientFragment.kt b/demo/src/main/java/com/google/android/fhir/demo/EditPatientFragment.kt index 61faee2096..07b48eb3be 100644 --- a/demo/src/main/java/com/google/android/fhir/demo/EditPatientFragment.kt +++ b/demo/src/main/java/com/google/android/fhir/demo/EditPatientFragment.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Google LLC + * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -56,7 +56,7 @@ class EditPatientFragment : Fragment(R.layout.add_patient_fragment) { } viewModel.isPatientSaved.observe(viewLifecycleOwner) { if (!it) { - Toast.makeText(requireContext(), R.string.message_input_missing, Toast.LENGTH_SHORT).show() + Toast.makeText(requireContext(), R.string.inputs_missing, Toast.LENGTH_SHORT).show() return@observe } Toast.makeText(requireContext(), R.string.message_patient_updated, Toast.LENGTH_SHORT).show() diff --git a/demo/src/main/res/values/strings.xml b/demo/src/main/res/values/strings.xml index 9a0fa0ecc0..46fd993148 100644 --- a/demo/src/main/res/values/strings.xml +++ b/demo/src/main/res/values/strings.xml @@ -46,7 +46,7 @@ Edit Patient Submit Syncing\u2026 - Inputs are missing. + Patient is updated. Inputs are missing. Resources are saved. diff --git a/workflow/testdata/group-measure/patients-bundle.json b/workflow/testdata/group-measure/patients-bundle.json index d7fd6be016..b766b8d401 100644 --- a/workflow/testdata/group-measure/patients-bundle.json +++ b/workflow/testdata/group-measure/patients-bundle.json @@ -561,7 +561,7 @@ } ], "gender": "male", - "birthDate": "2021-09-08", + "birthDate": "2022-09-08", "address": [ { "district": "Rahim yar khan" @@ -938,4 +938,4 @@ } } ] -} \ No newline at end of file +}